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

  1. Inspect error.cause (Node 16.9+) or the embedded inner message — fix the root error, not the wrapper
  2. Reproduce the handler directly with the same input (call the tool's exported handler or the CLI subcommand) to remove dispatch layers
  3. If the inner message mentions content/guardrail boundaries, check CLAUDE_FLOW_STRICT_GUARDRAIL and sanitize the data feeding the tool
  4. 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

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


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. Once

View on GitHub (pinned to 2602b642d9)