{"record":{"id":"ff7bc6727327a658","repo":"shareAI-lab/learn-claude-code","slug":"every-mcp-tool-needs-a-non-empty-name","errorCode":null,"errorMessage":"Every MCP tool needs a non-empty name","messagePattern":"Every MCP tool needs a non-empty name","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s14_mcp_plugin/code.py","lineNumber":171,"sourceCode":"    \"edit_file\": run_edit,\n    \"glob\": run_glob,\n}\n\n\n# -- New in s14: MCP discovery and dispatch --\n\nclass MCPClient:\n    \"\"\"Small in-process stand-in for MCP tools/list and tools/call.\"\"\"\n\n    def __init__(self, name: str):\n        self.name = name\n        self.tools: list[dict] = []\n        self._handlers: dict[str, callable] = {}\n\n    def register(self, tool_defs: list[dict], handlers: dict[str, callable]):\n        names = [tool.get(\"name\") for tool in tool_defs]\n        if any(not isinstance(name, str) or not name for name in names):\n            raise ValueError(\"Every MCP tool needs a non-empty name\")\n        if len(set(names)) != len(names):\n            raise ValueError(f\"Duplicate MCP tool name on server {self.name!r}\")\n        missing = [name for name in names if name not in handlers]\n        if missing:\n            raise ValueError(f\"Missing MCP handlers: {', '.join(missing)}\")\n        self.tools = list(tool_defs)\n        self._handlers = dict(handlers)\n\n    def call_tool(self, tool_name: str, args: dict) -> str:\n        handler = self._handlers.get(tool_name)\n        if not handler:\n            return f\"MCP error: unknown tool '{tool_name}'\"\n        try:\n            return str(handler(**args))\n        except Exception as exc:\n            return f\"MCP error: {type(exc).__name__}: {exc}\"\n\n","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s14_mcp_plugin/code.py#L153-L189","documentation":"MCPClient.register() requires every tool definition in tool_defs to carry a 'name' that is a non-empty string. A missing 'name' key (tool.get returns None), an empty string, or a non-string value (number, dict) fails fast at registration instead of producing a broken tools/list catalog.","triggerScenarios":"register([{'description': 'x'}], handlers={...}) (no name); register([{'name': ''}], ...); register([{'name': 42}], ...); tool defs loaded from JSON/YAML where the name key was misspelled ('Name', 'tool').","commonSituations":"Hand-written tool definition dicts; config files with typo'd keys; dynamic tool defs generated from a loop where a field is None.","solutions":["Give every tool def a non-empty string 'name' key.","Validate defs against a schema (e.g. jsonschema) before register() when loading from config.","Log the offending tool_defs payload when registration fails to spot the malformed entry."],"exampleFix":"// before\nserver.register([{'nam': 'search', ...}], handlers)\n\n// after\nserver.register([{'name': 'search', 'description': '...', 'inputSchema': {...}}], handlers)","handlingStrategy":"validation","validationCode":"def tool_defs_wellformed(tool_defs: list[dict]) -> bool:\n    return all(\n        isinstance(t.get('name'), str) and t['name']\n        for t in tool_defs\n    )\n\nassert tool_defs_wellformed(defs) before server.register(defs, handlers)","typeGuard":"from typing import TypeGuard\n\ndef is_named_tool_def(value: dict) -> TypeGuard[dict]:\n    return isinstance(value.get('name'), str) and len(value['name']) > 0","tryCatchPattern":null,"preventionTips":["Author tool defs next to their handlers in one registry so names are never omitted.","Schema-validate defs loaded from JSON/YAML before register()."],"tags":["mcp","validation","tool-registration"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}