Repository navigation
Sanitize MCP tool names for providers and map them back on dispatch - #694
Merged
dwash96 merged 2 commits intoSep 26, 2026
Merged
Conversation
Providers enforce the OpenAI function-name pattern
^(?:[A-Za-z0-9_-]{1,64})$ on tools[].function.name, so an MCP server that
advertises a dotted name (browser.fetch) makes every request fail with
"Invalid 'tools[1].function.name': string does not match pattern" even though
the server itself is healthy.
Sanitize the prefixed name when the tool list is built, compare sanitized names
when matching an incoming call to its server, and map the sanitized name back to
the name the server advertises before tools/call. Sanitization is idempotent, so
a caller that already holds the original name is unaffected.
chrisnestrud
force-pushed
the
fix/mcp-tool-name-sanitization
branch
from
September 26, 2026 00:14
f704959 to
baaecd2
Compare
dwash96
added a commit
that referenced
this pull request
Sep 26, 2026
Revert the beads/dolt entries the PR added to .gitignore so the merge touches code only. Replace the per-call scan in responses.original_tool_name() with module-level dicts that cache the original/sanitized MCP tool name pairs. register_tool_names() fills them from coder.mcp_tools inside get_tool_list(), so dispatching a tool call is now a dict lookup instead of iterating every advertised tool. sanitize_tool_name() also reuses its cache.
dwash96
added a commit
that referenced
this pull request
Sep 26, 2026
dwash96
added a commit
that referenced
this pull request
Sep 26, 2026
Revert the beads/dolt entries the PR added to .gitignore so the merge touches code only. Replace the per-call scan in responses.original_tool_name() with module-level dicts that cache the original/sanitized MCP tool name pairs. register_tool_names() fills them from coder.mcp_tools inside get_tool_list(), so dispatching a tool call is now a dict lookup instead of iterating every advertised tool. sanitize_tool_name() also reuses its cache.
Closed
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
OpenAI-style providers require
tools[].function.nameto match^[A-Za-z0-9_-]{1,64}$. cecli sends MCP tool names verbatim(
mcp/manager.py::_mcp_tools_to_openai_tools) and prefixes them with the servername, so a server that legitimately advertises a dotted name makes every chat
request fail:
A packaged MCP server that advertises
browser.fetchandbrowser.persisted_session.listis enough to block that provider laneentirely, even though the server itself is healthy.
Fix
helpers/responses.py: addsanitize_tool_name()plusoriginal_tool_name().prefix_tool_name()now sanitizes the composed{server}--{tool}name, so the tool list carries only provider-safe names.Sanitization is idempotent.
coders/base_coder.py: compare sanitized names when matching an incoming callto its server (
_find_mcp_server_for_tool), and map the sanitized name back tothe name the server advertises in
call_mcp_tool_from_session()beforetools/call. Both the base and agent coders route through that method.A caller that already holds the server's original name is unaffected: the
fallback returns the name unchanged.
Testing
tests/mcp/test_tool_name_sanitization.pycovers sanitization (includingidempotence and the 64-character limit), the names
get_tool_list()produces,server selection for a sanitized call, and the map-back on dispatch.
Verified end to end with the packaged
browser-mcpserver behind a localOpenAI-compatible endpoint that records the request payload:
browser--browser.fetch,browser--browser.persisted_session.listbrowser--browser_fetch,browser--browser_persisted_session_listand both tools dispatch to the server's original names;
browser.fetchforhttps://api.github.com/zenreturns HTTP 200 with the page text.Skipped: collision handling when two advertised names sanitize to the same
name. Providers reject duplicate function names, so it fails loudly rather than
silently dispatching to the wrong tool.