Skip to content

fix(mcp): preserve original tool name for external MCP servers - #6803

Open
Neuromediator wants to merge 1 commit into
crewAIInc:mainfrom
Neuromediator:fix/mcp-external-tool-original-name
Open

fix(mcp): preserve original tool name for external MCP servers#6803
Neuromediator wants to merge 1 commit into
crewAIInc:mainfrom
Neuromediator:fix/mcp-external-tool-original-name

Conversation

@Neuromediator

Copy link
Copy Markdown

AI-generated contribution. Authored with Claude Code and labelled llm-generated per
CONTRIBUTING.md. Reviewed and verified against a live MCP server before submission.

Problem

Tool names are sanitized during discovery over streamable HTTP, and the sanitized name is
then sent back to the server as the name to invoke. Any server-side tool whose name contains a
hyphen is therefore unreachable.

Concretely, with Context7:

agent = Agent(..., mcps=["https://mcp.context7.com/mcp"])

resolve-library-id is discovered, then invoked as resolve_library_id, and the server
responds with an unknown-tool error.

Reproduced against the live server on main:

BEFORE   agent-facing='mcp_context7_com_mcp_resolve_library_id'  sent-to-server='resolve_library_id'  ✗
AFTER    agent-facing='mcp_context7_com_mcp_resolve_library_id'  sent-to-server='resolve-library-id'  ✓

Cause

_discover_mcp_tools keys its schema dict by sanitize_tool_name(tool.name) and discards the
original:

schemas[sanitize_tool_name(tool.name)] = {
    "description": getattr(tool, "description", ""),
    "args_schema": args_schema,
}

By the time _resolve_external builds the wrapper, the original name no longer exists, so the
sanitized key is passed as MCPToolWrapper(tool_name=...). That parameter's own docstring
describes it as "Original name of the tool on the MCP server", and _run forwards it verbatim
to session.call_tool.

Fix

The native path already solves this — _resolve_native reads original_name off the tool
definition and passes original_tool_name= to MCPNativeTool. This applies the same pattern to
the external path so the two are consistent:

  1. _discover_mcp_tools keeps original_name alongside the sanitized key.
  2. MCPToolWrapper.__init__ gains original_tool_name: str | None = None, mirroring
    MCPNativeTool.__init__, and stores original_tool_name or tool_name.
  3. _resolve_external passes it through.

Eight added lines of source.

Compatibility

  • Agent-facing names are unchanged. Sanitization still governs the name exposed for LLM
    function calling; only the name used to invoke the tool changes. There is a test asserting
    no hyphen leaks into tool.name.
  • original_tool_name defaults to tool_name, so existing callers and schemas cached by a
    previous version behave exactly as before.
  • url#tool selection still compares sanitized names, covered by a test.

Tests

New lib/crewai/tests/mcp/test_tool_resolver_external.py — 11 tests covering discovery,
resolution, wrapper construction and the warning path. There was previously no external-path
test module.

Verified the tests are meaningful by reverting only the source changes:

without fix   5 failed, 6 passed
with fix     11 passed

The 6 that pass either way are the invariants the fix must not break.

Checks run locally:

uv run pytest lib/crewai/tests/mcp/ -q        51 passed
uv run pytest lib/crewai/tests/tools/ -q     279 passed
uv run ruff check <changed files>             All checks passed
uv run ruff format --diff <changed files>     already formatted
uv run mypy <changed files>                   Success: no issues found

Tool names are sanitized during discovery over streamable HTTP, and the
sanitized name was then sent back to the server as the name to invoke, so
any server-side tool containing a hyphen became unreachable. Context7's
`resolve-library-id` was requested as `resolve_library_id`.

`_discover_mcp_tools` keyed its schema dict by `sanitize_tool_name(tool.name)`
and discarded the original, so `_resolve_external` had nothing else to pass as
`MCPToolWrapper(tool_name=...)` -- a parameter whose own docstring described it
as "Original name of the tool on the MCP server" and which `_run` forwards
verbatim to `session.call_tool`.

The native path already solved this: `_resolve_native` reads `original_name`
from the tool definition and passes `original_tool_name=` to `MCPNativeTool`.
This applies the same pattern to the external path, so the two stay consistent.

Sanitized names are still what agents see, so LLM function-calling constraints
are unaffected; only the name used to invoke the tool changes.

`original_tool_name` defaults to `tool_name`, so existing callers and any
schemas cached by a previous version keep working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Neuromediator

Copy link
Copy Markdown
Author

Per CONTRIBUTING.md, this PR requires the llm-generated label — it was authored with Claude Code. I attempted to apply it at creation time but the API rejects label changes from non-collaborators (403: Must have admin rights to Repository).

Flagging it explicitly here so it isn't mistaken for an undisclosed AI contribution. Could a maintainer add the label? Happy to make any changes you'd like.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

MCP tool discovery now stores original server names with sanitized identifiers. MCPToolWrapper accepts the original name and uses it for external tool calls. Tests cover discovery, filtering, fallback, precedence, and warnings.

Changes

MCP tool name preservation

Layer / File(s) Summary
Wrapper and discovery name contract
lib/crewai/src/crewai/tools/mcp_tool_wrapper.py, lib/crewai/src/crewai/mcp/tool_resolver.py
MCP discovery uses sanitized names for schemas and lookup while storing original server names. MCPToolWrapper accepts an optional original name and falls back to tool_name.
External resolution and validation
lib/crewai/src/crewai/mcp/tool_resolver.py, lib/crewai/tests/mcp/test_tool_resolver_external.py
External resolution passes original names to wrappers. Tests cover sanitization, filtering, fallback, precedence, and missing-tool warnings.

Sequence Diagram(s)

sequenceDiagram
  participant MCPServer
  participant ToolResolver
  participant MCPToolWrapper
  MCPServer->>ToolResolver: return tool with original name
  ToolResolver->>ToolResolver: sanitize name for schema lookup
  ToolResolver->>MCPToolWrapper: pass sanitized and original names
  MCPToolWrapper->>MCPServer: invoke original tool name
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes preserving original tool names for external MCP servers.
Description check ✅ Passed The description directly explains the external MCP invocation bug, the fix, compatibility behavior, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/tests/mcp/test_tool_resolver_external.py`:
- Around line 109-120: Extend test_hyphenated_name_is_used_for_server_calls to
execute the resolved tool through the fake transport, capture the name supplied
to ClientSession.call_tool, and assert it is "resolve-library-id". Keep the
existing wrapper-state assertion, but verify the RPC uses the original
hyphenated name rather than a sanitized name.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9acf1fc0-005b-45ce-95f8-d0addc9e1073

📥 Commits

Reviewing files that changed from the base of the PR and between c5b9d9a and 3b31a82.

📒 Files selected for processing (3)
  • lib/crewai/src/crewai/mcp/tool_resolver.py
  • lib/crewai/src/crewai/tools/mcp_tool_wrapper.py
  • lib/crewai/tests/mcp/test_tool_resolver_external.py

Comment on lines +109 to +120
def test_hyphenated_name_is_used_for_server_calls(self, resolver):
tools = self._resolve_one(
resolver,
{
"description": "Resolve a library id",
"args_schema": None,
"original_name": "resolve-library-id",
},
)

assert len(tools) == 1
assert tools[0].original_tool_name == "resolve-library-id"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Verify the MCP RPC argument.

Line 120 checks wrapper state. It does not verify the name passed to ClientSession.call_tool.

Execute the resolved wrapper with the fake transport. Record the call_tool name. Assert that it equals "resolve-library-id". This will detect a regression that sends the sanitized name to the MCP server.

As per coding guidelines, “Write unit tests for new functionality that focus on behavior rather than implementation details.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/mcp/test_tool_resolver_external.py` around lines 109 - 120,
Extend test_hyphenated_name_is_used_for_server_calls to execute the resolved
tool through the fake transport, capture the name supplied to
ClientSession.call_tool, and assert it is "resolve-library-id". Keep the
existing wrapper-state assertion, but verify the RPC uses the original
hyphenated name rather than a sanitized name.

Source: Coding guidelines

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.

1 participant