ruvnet/ruflo · error · MCPClientError
Failed to execute MCP tool
Error message
Failed to execute MCP tool '${toolName}': ${error instanceof Error ? error.message : String(error)} What it means
callMCPTool() wraps any rejection from a registered tool's handler in MCPClientError("Failed to execute MCP tool 'X': <inner message>") and preserves the original error as `cause`. It means the tool was found and policy allowed it, but execution inside the handler failed — the wrapper adds context, the root cause is one level down. This is the generic counterpart to the pre-dispatch failures 223 (not found) and 224 (policy denied).
Solutions
- Inspect error.cause (Node 16.9+) or the embedded inner message — fix the root error, not the wrapper
- Reproduce the handler directly with the same input (call the tool's exported handler or the CLI subcommand) to remove dispatch layers
- If the inner message mentions content/guardrail boundaries, check CLAUDE_FLOW_STRICT_GUARDRAIL and sanitize the data feeding the tool
- Log the full chain (console.error(err, { cause: err.cause })) so the root error reaches your issue tracker
Example fix
// before — only the wrapper message is seen
try { await callMCPTool('memory_store', input); }
catch (e) { console.log(e.message); } // 'Failed to execute...: EACCES'
// after — surface the root cause
try { await callMCPTool('memory_store', input); }
catch (e) {
const root = e.cause ?? e;
console.error('tool failed:', root.message, root.stack);
} Defensive patterns
Strategy: try-catch
Type guard
const isMCPClientError = (e: unknown): e is MCPClientError => e instanceof MCPClientError || (e instanceof Error && (e as MCPClientError).toolName !== undefined);
Try / catch
try {
return await callMCPTool(toolName, input, context);
} catch (e) {
if (e instanceof MCPClientError) {
const root = e.cause ?? e; // the real failure is one level down
console.error(`tool ${e.toolName} failed:`, root.message, root.stack);
// deterministic causes (bad input) should not be retried blindly
}
throw e;
} Prevention
- Always inspect .cause — the wrapper message alone hides the root error
- Log the full chain (err + err.cause) to your tracker
- Distinguish pre-dispatch failures (223/224) from handler failures before choosing retry vs fix
When it happens
Trigger: Any handler-level fault: arguments that passed schema but violate runtime expectations (e.g. bad file paths), downstream service outages (AgentDB/ONNX unavailable), filesystem permission errors, JSON parse errors on tool input, or nested throws from libraries the tool calls. Because the catch is around handler + guardrail, applyContentBoundaryGuardrail rejections also surface through this wrapper in strict mode.
Common situations: Debugging stalls at the wrapper message because logs truncate the chain; users retry the same call hoping it is transient when the cause is deterministic (bad path); strict-guardrail mode (CLAUDE_FLOW_STRICT_GUARDRAIL=true) newly rejecting results that contain injection-looking text, appearing as 'execution failed' after upgrade.
Related errors
- INTERNAL_ERROR
- at least one candidate is required
- candidate must ingest at least one vector
- Connection pool is shutting down
- Dangerous key segment rejected
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-08-18).
Data as JSON: /api/errors/4128f16674f3d75e.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-client.ts:280
// directly, so there is no recursive MCP dispatch. In enforce mode an
// administrator must explicitly allow policy.* actions or use the local
// CLI bootstrap path.
const decision = await authorizeMcpTool(toolName, input, context, classifyMcpTool(toolName));
if (decision.enforcedOutcome !== 'allowed') {
throw new Error(`policy-${decision.enforcedOutcome}:${decision.reason}; receipt=${decision.receiptId}`);
}
// Call the tool handler
const result = await tool.handler(input, context);
// ADR-146 P2: scan every tool result for indirect-injection before it
// returns to the caller. The screen is opt-in via env (default off in
// 3.10.34 — flip to default in v4) so existing pipelines keep their
// exact behaviour while the call site is exercised by tests and
// adopters. Telemetry from the screen lands in the shared
// GuardrailEvent sink (P5).
return applyContentBoundaryGuardrail(toolName, result) as T;
} catch (error) {
// Wrap and re-throw with context
throw new MCPClientError(
`Failed to execute MCP tool '${toolName}': ${error instanceof Error ? error.message : String(error)}`,
toolName,
error instanceof Error ? error : undefined
);
}
}
/**
* ADR-146 P2 — content-boundary screen on the MCP tool dispatch path.
*
* Default behaviour (3.10.34, legacy mode): returns the result unchanged.
* With `CLAUDE_FLOW_STRICT_GUARDRAIL=true`, scans every string field of the
* result; `reject` substitutes the field with a typed marker so the caller
* can surface the rejection. The class itself (`ToolOutputGuardrail`)
* shipped in ADR-131 P1; this call site is what closes #2149.
*
* Implementation note: we resolve the guardrail lazily so the cold-import
* cost of `@claude-flow/security` does not hit every CLI invocation. OnceView on GitHub (pinned to 2602b642d9)