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

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Read `.cause` (or `.toolName`) on the MCPClientError to find the original stack and the failing tool.
  2. Reproduce with the same input against the tool directly (bypassing callMCPTool) to confirm the error is in the handler, not the dispatch path.
  3. Check the backing resource: database file perms/locks, native module presence, memory headroom.
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/4128f16674f3d75e. Report an issue: GitHub.