shareAI-lab/learn-claude-code · error · ValueError
Missing MCP handlers: {', '.join(missing)}
Error message
Missing MCP handlers: {', '.join(missing)} What it means
MCPClient.register() requires a handler for every tool name in tool_defs; any tool without an entry in the handlers dict is listed as missing in the error. This keeps the catalog and dispatch table consistent — a listed tool must be callable.
Source
Thrown at s14_mcp_plugin/code.py:176
# -- New in s14: MCP discovery and dispatch --
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] = {}
mcp_tool_policies: dict[str, str] = {}
_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9_-]")
# Authorization comes from host configuration, never server descriptions.View on GitHub (pinned to 985456f4ad)
Solutions
- Add a handler keyed by the exact tool name string.
- Derive handlers and defs from one source of truth (e.g. a registry decorator) so they cannot drift.
- Copy the name string from the error message to fix casing/typo mismatches.
Example fix
// before
server.register(
[{'name': 'search'}, {'name': 'get_version'}],
handlers={'search': do_search}) // missing get_version
// after
server.register(
[{'name': 'search'}, {'name': 'get_version'}],
handlers={'search': do_search, 'get_version': do_get_version}) Defensive patterns
Strategy: validation
Validate before calling
def handlers_cover_defs(tool_defs: list[dict], handlers: dict) -> bool:
return all(t['name'] in handlers for t in tool_defs)
assert handlers_cover_defs(defs, handlers) before register(defs, handlers) Try / catch
try:
server.register(defs, handlers)
except ValueError as exc:
if str(exc).startswith('Missing MCP handlers'):
missing = str(exc).rsplit(':', 1)[1].split(',')
raise RuntimeError(f'wire handlers for {missing}') from exc
raise Prevention
- Derive defs and handlers from a single registry (decorator pattern) so they cannot drift.
- After adding a tool def, run a smoke register() in CI to catch missing handlers.
- Use the exact name strings from the error message to fix key mismatches.
When it happens
Trigger: register([{'name': 'search'}, {'name': 'get_version'}], handlers={'search': fn}) → 'Missing MCP handlers: get_version'; handler dict keys with typos or different casing than the def names.
Common situations: Adding a new tool def but forgetting its handler; renaming a tool in defs but not the handlers dict; building handlers via vars() or filtering that drops one entry.
Related errors
- Every MCP tool needs a non-empty name
- Duplicate MCP tool name on server {self.name!r}
- MCP tool name is longer than 64 characters: {prefixed}
- Invalid input schema for {origin}
- Every MCP tool needs a non-empty name
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/f59453ce5295fb21.
Report an issue: GitHub.