ruvnet/ruflo · error · MCPClientError
Failed to execute MCP tool '${toolName}': ${error instanceof
Error message
Failed to execute MCP tool '${toolName}': ${error instanceof Error ? error.message : String(error)} What it means
Wrapping error thrown by callMCPTool() when anything inside the try block (tool.handler execution, the content-boundary guardrail, or even the authorize step) throws and is not already a more specific MCPClientError. The original error is preserved on the `.cause` property and its message is inlined so the surface message is self-contained. This is the generic 'the tool blew up' boundary for downstream consumers.
Source
Thrown at v3/@claude-flow/cli/src/mcp-client.ts:272
// 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 6b01dc5a68)
Solutions
- Read `.cause` (or `.toolName`) on the MCPClientError to find the original stack and the failing tool.
- Reproduce with the same input against the tool directly (bypassing callMCPTool) to confirm the error is in the handler, not the dispatch path.
- Check the backing resource: database file perms/locks, native module presence, memory headroom.
- If the underlying error is transient (network, lock contention), retry with backoff; otherwise fix the handler input/state.
Example fix
// before — caller loses the original error
try { await callMCPTool('memory_store', input); }
catch (e) { console.log(e.message); }
// after — surface .cause and .toolName
try { await callMCPTool('memory_store', input); }
catch (e) {
if (e.name === 'MCPClientError') {
console.error(e.toolName, e.cause?.stack ?? e.message);
}
} Defensive patterns
Strategy: try-catch
Validate before calling
null
Type guard
function isMCPClientError(e: unknown): e is { name: 'MCPClientError'; toolName: string; cause?: Error; message: string } {
return e instanceof Error && (e as any).name === 'MCPClientError' && typeof (e as any).toolName === 'string';
} Try / catch
try {
return await callMCPTool(toolName, input, ctx);
} catch (e) {
if (isMCPClientError(e)) {
log.error({ tool: e.toolName, msg: e.message, stack: e.cause?.stack });
// optionally classify transient vs permanent from e.cause
}
throw e;
} Prevention
- Always inspect `.cause` and `.toolName` rather than just `.message`.
- Wrap each call with structured logging so handler failures are attributable to a tool.
- Distinguish transient causes (locks, network) from permanent ones (bad input) before retrying.
When it happens
Trigger: The tool handler threw (filesystem error, downstream API 500, native crash in sql.js/better-sqlite3, OOM in embeddings generation); the ADR-146 content-boundary guardrail in strict mode rejected a field; an optional-dependency load threw a non-MODULE_NOT_FOUND error inside the handler.
Common situations: The backing database file is locked or corrupt; a native module (better-sqlite3) failed to load for the current Node ABI; the tool's input was accepted by validation but the underlying operation failed at runtime; out-of-memory during large vector operations.
Related errors
- MCP server "${server.name}" is in cooldown (HTTP ${cd.status
- MCP server "${server.name}" returned HTTP ${httpStatus}: ${h
- HTTP transport failed: ${String(firstError instanceof Error
- Failed to fetch base servers: ${response.statusText}
- Failed to load WASM module
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/4128f16674f3d75e.
Report an issue: GitHub.