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
- Read the error message: it lists the exact missing tool names; add handlers keyed by those names
- Derive both tables from one source of truth, e.g. build tool_defs from the handlers dict so names can never drift
- 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
- Single-source the tool tables: generate tool_defs from the handlers dict
- Unit-test register() for every server fixture in CI
- Lint that every 'name' literal in defs has a matching handler symbol
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
- Every MCP tool needs a non-empty name
- Duplicate MCP tool name on server {self.name!r}
- Missing MCP handlers: {', '.join(missing)}
- MCP tool name is longer than 64 characters: {prefixed}
- Invalid input schema for {origin}
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/342ceada0142cc77.
Report an issue: GitHub.