{"record":{"id":"f92772919b44c869","repo":"shareAI-lab/learn-claude-code","slug":"mcp-names-cannot-normalize-to-an-empty-string","errorCode":null,"errorMessage":"MCP names cannot normalize to an empty string","messagePattern":"MCP names cannot normalize to an empty string","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s14_mcp_plugin/code.py","lineNumber":207,"sourceCode":"\nmcp_clients: dict[str, MCPClient] = {}\nmcp_tool_policies: dict[str, str] = {}\n_DISALLOWED_CHARS = re.compile(r\"[^a-zA-Z0-9_-]\")\n\n# Authorization comes from host configuration, never server descriptions.\nMCP_HOST_POLICY = {\n    (\"docs\", \"search\"): \"allow\",\n    (\"docs\", \"get_version\"): \"allow\",\n    (\"deploy\", \"status\"): \"allow\",\n    (\"deploy\", \"trigger\"): \"confirm\",\n}\n\n\ndef normalize_mcp_name(name: str) -> str:\n    \"\"\"Replace characters outside the model tool-name alphabet.\"\"\"\n    normalized = _DISALLOWED_CHARS.sub(\"_\", name)\n    if not normalized:\n        raise ValueError(\"MCP names cannot normalize to an empty string\")\n    return normalized\n\n\ndef _mock_server_docs() -> MCPClient:\n    server = MCPClient(\"docs\")\n    server.register(\n        tool_defs=[\n            {\n                \"name\": \"search\",\n                \"description\": \"Search the documentation.\",\n                \"inputSchema\": {\n                    \"type\": \"object\",\n                    \"properties\": {\"query\": {\"type\": \"string\"}},\n                    \"required\": [\"query\"],\n                },\n                \"annotations\": {\"readOnlyHint\": True},\n            },\n            {","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s14_mcp_plugin/code.py#L189-L225","documentation":"normalize_mcp_name() replaces every character outside the model's tool-name alphabet with '_' (_DISALLOWED_CHARS substitution). If the input consists entirely of disallowed characters (or is empty), the normalized result is '' — not a usable tool-name component — so it raises ValueError instead of producing a garbage name like 'mcp_____'.","triggerScenarios":"normalize_mcp_name('') (empty server or tool name); normalize_mcp_name('***') or any all-punctuation string; a tool def whose name is only whitespace/CJK/emoji after stripping.","commonSituations":"Loading MCP server names or tool names from external config where a value is blank or purely symbolic; generated tool names from descriptions; trimming that empties a string.","solutions":["Ensure server and tool names contain at least one allowed character (letter/digit) before registration.","Skip and log tool defs with empty/all-symbol names instead of registering them.","Validate config values with a non-empty-after-normalization check at load time."],"exampleFix":"// before\nname = normalize_mcp_name(raw_name)  // ValueError if raw_name is '***'\n\n// after\nif not re.sub(r'[^A-Za-z0-9_-]', '_', raw_name or '').strip('_'):\n    continue  # skip unusable tool name\nname = normalize_mcp_name(raw_name)","handlingStrategy":"type-guard","validationCode":"import re\n\ndef normalizes_nonempty(name: str) -> bool:\n    return bool(re.sub(r'[^A-Za-z0-9_-]', '_', name or '').strip('_')) or bool(re.sub(r'[^A-Za-z0-9_-]', '_', name or ''))","typeGuard":"import re\nfrom typing import TypeGuard\n\ndef normalizable_mcp_name(value: object) -> TypeGuard[str]:\n    if not isinstance(value, str):\n        return False\n    return bool(re.sub(r'[^A-Za-z0-9_-]', '_', value))","tryCatchPattern":null,"preventionTips":["Require at least one alphanumeric in server and tool names at config load time.","Skip-and-log unusable tool defs instead of registering them.","Test normalization of generated names in CI."],"tags":["mcp","validation","name-normalization"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}