shareAI-lab/learn-claude-code · error · ValueError

Missing MCP handlers: {', '.join(missing)}

Error message

Missing MCP handlers: {', '.join(missing)}

What it means

Raised by MCPClient.register when one or more tool definitions in tool_defs have a 'name' that has no matching key in the handlers dict. The server refuses to start registration because a model could select that tool and there would be no Python callable to execute. It is a ValueError thrown during setup, before any tool call happens.

Source

Thrown at s15_integrated_harness/code.py:2437

# tools are merged into the normal tool pool with mcp__server__tool names.
class MCPClient:
    """Small in-process stand-in for MCP tools/list and tools/call."""

    def __init__(self, name: str):
        self.name = name
        self.tools: list[dict] = []
        self._handlers: dict[str, callable] = {}

    def register(self, tool_defs: list[dict],
                 handlers: dict[str, callable]):
        names = [tool.get("name") for tool in tool_defs]
        if any(not isinstance(name, str) or not name for name in names):
            raise ValueError("Every MCP tool needs a non-empty name")
        if len(set(names)) != len(names):
            raise ValueError(f"Duplicate MCP tool name on server {self.name!r}")
        missing = [name for name in names if name not in handlers]
        if missing:
            raise ValueError(f"Missing MCP handlers: {', '.join(missing)}")
        self.tools = list(tool_defs)
        self._handlers = dict(handlers)

    def call_tool(self, tool_name: str, args: dict) -> str:
        handler = self._handlers.get(tool_name)
        if not handler:
            return f"MCP error: unknown tool '{tool_name}'"
        try:
            return str(handler(**args))
        except Exception as exc:
            return f"MCP error: {type(exc).__name__}: {exc}"


mcp_clients: dict[str, MCPClient] = {}
_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9_-]")

# Authorization comes from host configuration, never server descriptions.
MCP_HOST_POLICY = {

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Read the error message: it lists the exact missing tool names; add handlers keyed by those names
  2. Derive both tables from one source of truth, e.g. build tool_defs from the handlers dict so names can never drift
  3. Add a unit test that calls register() for every server fixture so drift fails CI instead of runtime

Example fix

// before
client.register(
    tool_defs=[{"name": "search", ...}, {"name": "get_version", ...}],
    handlers={"search": do_search},  # get_version handler missing
)

// after
client.register(
    tool_defs=[{"name": "search", ...}, {"name": "get_version", ...}],
    handlers={"search": do_search, "get_version": do_get_version},
)
Defensive patterns

Strategy: validation

Validate before calling

def validate_registration(tool_defs, handlers):
    names = [t.get("name") for t in tool_defs]
    missing = [n for n in names if n not in handlers]
    extra = [k for k in handlers if k not in names]
    if missing or extra:
        raise ValueError(f"defs/handlers mismatch: missing={missing}, extra={extra}")

validate_registration(tool_defs, handlers)
client.register(tool_defs=tool_defs, handlers=handlers)

Type guard

def has_all_handlers(tool_defs: list[dict], handlers: dict) -> bool:
    return all(isinstance(t.get("name"), str) and t["name"] in handlers for t in tool_defs)

Try / catch

try:
    client.register(tool_defs, handlers)
except ValueError as exc:
    if "Missing MCP handlers" in str(exc):
        # names are listed in the message; fix tables and re-register
        raise SystemExit(f"registration misconfigured: {exc}") from exc
    raise

Prevention

When it happens

Trigger: Calling client.register(tool_defs=[{"name": "search", ...}], handlers={"query": fn}) — the tool is named 'search' but the handler key is 'query', so 'search' lands in the missing list. Also triggered by typos, case mismatches ('Search' vs 'search'), or forgetting to port a handler after adding a new tool_def.

Common situations: Growing an MCP server: a developer appends a new tool definition to the defs list but forgets to add the handler. Renaming a tool in one table but not the other. Copy-pasting a defs/handlers pair from another server where the names diverged.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/342ceada0142cc77. Report an issue: GitHub.