Skip to content

Prefix compressed MCP namespaces so tool names start with a letter - #287

Merged
anticomputer merged 1 commit into
mainfrom
anticomputer/mcp-tool-name-leading-digit
Jul 31, 2026
Merged

anticomputer merged 1 commit into
mainfrom
anticomputer/mcp-tool-name-leading-digit

Conversation

@anticomputer

@anticomputer anticomputer commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The bug

compress_name returns a bare sha256 prefix, and that value is prepended to the name of every tool an MCP server exposes:

tool_copy.name = f"{self.namespace}{tool.name}"

A hex digest starts with a digit ten times in sixteen, so a server whose name hashes unluckily produces tool names like 8fb2adfa03efcontainer_shell_exec.

Gemini documents that a function name "must start with a letter or an underscore", and enforces it by rejecting the entire request. So one unlucky server name disables every tool in the run, not just that server's.

Probed against CAPI, sending the same minimal request twice and changing only the tool name:

model endpoint 8fb2adfa03ef… ns8fb2adfa03ef…
gemini-3.6-flash chat completions 400 invalid_request_body ok
claude-sonnet-5 chat completions ok ok
gpt-5-mini chat completions ok ok
gpt-5.4 chat completions ok ok
gpt-5.6-sol responses ok ok
grok-4.5 responses ok ok

Only Gemini rejects it. OpenAI and Anthropic document the laxer ^[a-zA-Z0-9_-]{1,64}$, which permits a leading digit, and they do accept it in practice.

That narrowness is what makes this expensive to diagnose. The same taskflow works on most models and fails on one; the error names no tool and no server; and whether you hit it at all depends on the sha256 of a server name nobody chose for its hash. I found it on a multi-model task where the Gemini branch was the only one that died, and the obvious readings — entitlements, a malformed tool schema, the wrong endpoint — were all wrong.

The fix

Prefix the digest with a fixed ns so the result always starts with a letter. The namespace is opaque and is stripped with removeprefix, so nothing else changes; the name grows from 12 characters to 14, well inside the 64-character limit the compression exists to respect. compress_name is also used for handoff agent names in runner.py, which get the same treatment.

Conforming to the strictest of the three documented patterns costs two characters and removes a class of failure that is invisible until a specific server name meets a specific provider.

Tests

Adds compress_name coverage for the leading character (including ContainerShell, one of the names that actually triggers this), stability, distinctness, and the 64-character budget. Full suite passes: 556 passed, 2 skipped.

Copilot AI review requested due to automatic review settings July 30, 2026 20:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an interoperability bug in MCP tool-name namespacing where compress_name() could produce a namespace starting with a digit (hex digest prefix), which can yield invalid tool/function names for some providers and cause entire requests to be rejected.

Changes:

  • Prefix compress_name() output with a fixed "ns" so namespaced tool names always start with a letter.
  • Add unit tests covering compress_name() leading-character safety, stability/distinctness, and headroom under the 64-character tool-name limit.
Show a summary per file
File Description
src/seclab_taskflow_agent/mcp_utils.py Adds a constant prefix to compressed namespaces so generated tool names begin with a letter.
tests/test_mcp_utils.py Adds regression/unit coverage for compress_name() properties relevant to provider tool-name validation.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Low

@anticomputer
anticomputer force-pushed the anticomputer/mcp-tool-name-leading-digit branch from e7f17dc to ffa58e7 Compare July 30, 2026 20:12
compress_name returned a bare sha256 prefix, so the namespace prepended
to every tool of a server began with a digit ten times in sixteen,
producing names like `8fb2adfa03efcontainer_shell_exec`.

Gemini documents that a function name must start with a letter or an
underscore and enforces it by rejecting the whole request with 400
invalid_request_body, so one unlucky server name disabled every tool in
the run rather than that server's alone. OpenAI and Anthropic document
the laxer ^[a-zA-Z0-9_-]{1,64}$ and accept a leading digit, which is why
this looked like an intermittent, model-specific failure.

Prefix the digest with a fixed "ns". Adds compress_name tests for the
leading character, stability, distinctness and the 64-character budget.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
Copilot AI review requested due to automatic review settings July 30, 2026 20:33
@anticomputer
anticomputer force-pushed the anticomputer/mcp-tool-name-leading-digit branch from ffa58e7 to b8f6b78 Compare July 30, 2026 20:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/seclab_taskflow_agent/mcp_utils.py:66

  • The docstring says this function returns a "hex digest", but it now returns a prefixed string (e.g. "ns" + hex). Tweaking the wording will keep the documentation accurate and avoid confusion for callers/tests that read it literally.
    Returns:
        A lowercase hex digest of ``COMPRESSED_NAME_LENGTH`` characters, behind
        a fixed prefix so the result always starts with a letter.

tests/test_mcp_utils.py:154

  • This test asserts the first character is a letter, but the documented Gemini constraint is "letter or underscore". Allowing "_" keeps the test aligned with the stated requirement and avoids an unnecessary future test break if the prefix ever changes to an underscore (still valid for Gemini).
    assert compress_name(server_name)[0].isalpha()
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Low

@anticomputer
anticomputer merged commit 3c26fc8 into main Jul 31, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants