{"record":{"id":"44cadde05e46d5f0","repo":"github/copilot-sdk","slug":"invalid-kind-tool-name-must-not-be-empty","errorCode":null,"errorMessage":"invalid {kind} tool name: must not be empty","messagePattern":"invalid (.+?) tool name: must not be empty","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/copilot/_mode.py","lineNumber":26,"sourceCode":"\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom collections.abc import Iterable\nfrom typing import TYPE_CHECKING, Any, Literal\n\nif TYPE_CHECKING:\n    from .session import MemoryConfiguration\n\nCopilotClientMode = Literal[\"copilot-cli\", \"empty\"]\n\n_TOOL_NAME_REGEX = re.compile(r\"^[a-zA-Z0-9_-]+$\")\n\n\ndef _validate_tool_name(kind: str, name: str) -> None:\n    if not name:\n        raise ValueError(f\"invalid {kind} tool name: must not be empty\")\n    if name == \"*\":\n        return\n    if not _TOOL_NAME_REGEX.match(name):\n        raise ValueError(\n            f\"invalid {kind} tool name {name!r}: tool names must match \"\n            r\"/^[a-zA-Z0-9_-]+$/ or be the wildcard '*'\"\n        )\n\n\nclass ToolSet:\n    \"\"\"Builder for source-qualified tool filter patterns.\n\n    ``ToolSet`` accumulates entries like ``builtin:bash``, ``mcp:*``, or\n    ``custom:my_tool`` for use in\n    :class:`CopilotClient.create_session`'s ``available_tools`` /\n    ``excluded_tools`` parameters.\n\n    Tool classification (``builtin``/``mcp``/``custom``) is determined by the","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/python/copilot/_mode.py#L8-L44","documentation":"Tool names registered via ToolSet.add_builtin/add_custom/add_mcp must be non-empty strings. An empty name cannot match any tool and would produce ambiguous filter entries, so _validate_tool_name raises immediately. The wildcard \"*\" is allowed but only as a full string, not empty.","triggerScenarios":"Calling add_builtin(''), add_custom(''), or add_mcp('') — or passing a name that evaluates falsy (e.g. an empty string from an unparseable 'builtin:' prefix or a split() result) — into _validate_tool_name.","commonSituations":"Parsing tool identifiers like 'mcp:server:tool' with split(':') and picking the wrong segment; building names from env vars or config keys that are unset; string concatenation producing an empty suffix.","solutions":["Pass a non-empty tool name string to the add_* method","If the name comes from parsing, verify the segment index (e.g. name.split(':')[-1]) before registering","Fail fast at config load time: assert the tool name is non-empty before constructing a ToolSet","Use ToolSet().add_builtin('*') if the intent was to match all tools of a source"],"exampleFix":"// before\nprefix, _, tool = spec.partition(':')  # tool == '' when spec == 'builtin:'\ntoolset.add_builtin(tool)\n\n// after\nprefix, _, tool = spec.partition(':')\nif not tool:\n    raise ValueError(f\"missing tool name in {spec!r}\")\ntoolset.add_builtin(tool)","handlingStrategy":"validation","validationCode":"if not isinstance(name, str) or not name:\n    raise ValueError(f\"{kind} tool name must be a non-empty string\")","typeGuard":"def is_valid_tool_name(name) -> bool:\n    return isinstance(name, str) and bool(name)","tryCatchPattern":"try:\n    toolset.add_builtin(name)\nexcept ValueError as e:\n    logger.error(f\"bad tool name {name!r}: {e}\")\n    raise","preventionTips":["Assert tool names are non-empty at config load time","Check indices when splitting prefixed identifiers like 'mcp:server:tool'","Strip whitespace and source prefixes before registering tool names","Favor ToolSet builder methods over hand-assembled name strings"],"tags":["validation","python","tools","empty-string"],"backgroundTag":"empty-required-field","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}