ruvnet/ruflo · error · MCPClientError

MCP tool not found

Error message

MCP tool not found: ${toolName}

What it means

callMCPTool() resolves toolName against the in-process TOOL_REGISTRY populated at CLI startup; if the map has no entry under that exact name it throws MCPClientError('MCP tool not found'). This is a pre-dispatch failure — the tool never ran — and it is distinct from policy denial (224) and handler execution failure (225). The registry only contains tools registered locally, not every tool that exists on remote MCP servers.

Solutions

  1. Print the exact registered names with listMCPTools() (mcp-client.ts:374) or `npx @claude-flow/cli@latest mcp tools` and copy the name verbatim
  2. Check casing and separators — the registry is a Map keyed by exact string ('memory_search' not 'MemorySearch' or 'memorySearch')
  3. Confirm the plugin/extension that registers the tool is installed and enabled before the call (`plugins list`)
  4. Align versions: update @claude-flow/cli to the release whose docs you are following, since tool names change across major versions

Example fix

// before — hallucinated/misspelled name
await callMCPTool('memry_search', { query: 'auth' });

// after — verify against the registry, then call
const names = listMCPTools().map(t => t.name);
if (!names.includes('memory_search')) {
  throw new Error(`Unknown tool. Available: ${names.join(', ')}`);
}
await callMCPTool('memory_search', { query: 'auth' });
Defensive patterns

Strategy: validation

Validate before calling

import { listMCPTools } from './mcp-client';

const available = new Set(listMCPTools().map(t => t.name));
if (!available.has(toolName)) {
  throw new Error(
    `Tool '${toolName}' not registered. Available: ${[...available].join(', ')}`
  );
}
const result = await callMCPTool(toolName, input);

Type guard

import { listMCPTools } from './mcp-client';
const registeredTools = new Set(listMCPTools().map(t => t.name));
const isRegisteredTool = (name: string): boolean => registeredTools.has(name);

Try / catch

try { await callMCPTool(name, input); }
catch (e) {
  if (e instanceof MCPClientError && e.message.startsWith('MCP tool not found')) {
    // pre-dispatch failure: surface available names, do NOT retry
    throw new Error(`'${name}' unknown. Registered: ${listMCPTools().map(t => t.name).join(', ')}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling callMCPTool('memory_serch') with a typo or wrong casing; invoking a tool that lives on a separately configured remote MCP server instead of the local registry; calling a tool provided by a plugin that failed to load or was disabled at startup; using a tool name from newer docs while running an older CLI where the tool was renamed or did not exist yet.

Common situations: Agents composing tool names dynamically from LLM output that hallucinates or miscapitalizes; version drift between the installed @claude-flow/cli and tutorial docs; plugin registration failing silently so the tool count is lower than expected; migrating scripts between environments where a different plugin set is installed.

Related errors


AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-08-18). Data as JSON: /api/errors/8c9209df0ec7dd65. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-client.ts:253

 * });
 *
 * // Initialize swarm
 * const swarm = await callMCPTool('swarm_init', {
 *   topology: 'hierarchical-mesh',
 *   maxAgents: 15
 * });
 * ```
 */
export async function callMCPTool<T = unknown>(
  toolName: string,
  input: Record<string, unknown> = {},
  context?: Record<string, unknown>
): Promise<T> {
  // Look up tool in registry
  const tool = TOOL_REGISTRY.get(toolName);

  if (!tool) {
    throw new MCPClientError(
      `MCP tool not found: ${toolName}`,
      toolName
    );
  }

  try {
    // ADR-324: one policy chokepoint for every local CLI/MCP invocation.
    // Policy administration is not exempt: authorization calls the engine
    // 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

View on GitHub (pinned to 2602b642d9)