Fix MCP lookup for Responses tool aliases

Resolve provider-safe Responses tool names back to their canonical MCP names inside the MCP handler before dispatch. Preserve canonical lookups and reject stale or unrelated aliases so local tool resolution can continue. Add regression coverage for Google Workspace-style names and document the lookup contract.

Alessandro committed Jul 16, 2026 at 02:29 UTC 6a7178af91c6d8f2114e359dcd4e368fb803b568
3 files changed +47 -1
helpers/mcp_handler.py
+11 -1
@@ -45,6 +45,7 @@ from helpers import dirty_json, media_artifacts
45 from helpers.print_style import PrintStyle
46 from helpers.tool import Tool, Response
47 from helpers.defer import DeferredTask
48 +from helpers.responses_tools import original_tool_name
49
50
51 MCP_MEDIA_TOKENS_ESTIMATE = 1500
@@ -1211,7 +1212,16 @@ class MCPConfig(BaseModel):
1212 if effective_config is not self:
1213 return effective_config.get_tool(agent, tool_name)
1214 if not self.has_tool(tool_name):
1214 - return None
1215 + get_data = getattr(agent, "get_data", None)
1216 + name_map_key = getattr(agent, "DATA_NAME_RESPONSES_TOOL_NAME_MAP", "")
1217 + tool_name = original_tool_name(
1218 + tool_name,
1219 + get_data(name_map_key)
1220 + if name_map_key and callable(get_data)
1221 + else None,
1222 + )
1223 + if not self.has_tool(tool_name):
1224 + return None
1225 return MCPTool(agent=agent, name=tool_name, method=None, args={}, message="", loop_data=None)
1226
1227 async def call_tool(
helpers/mcp_handler.py.dox.md
+1
@@ -82,6 +82,7 @@
82 - Project-scoped MCP servers overlay global servers by normalized name. The resulting `MCPConfig` cache key is derived from both config strings so project instances refresh when either scope changes.
83 - Server status and detail responses include `scope`, and MCP tools resolve through `MCPConfig.get_for_agent(agent)` before execution.
84 - MCP tool names are qualified as `server_name.tool_name`; server names are normalized without dots, and the tool portion may contain dots.
85 +- `MCPConfig.get_tool()` tries the supplied qualified name first, then restores an advertised Responses alias from the calling agent's name map; names that still do not identify an MCP tool return `None` unchanged for downstream local-tool resolution.
86 - Servers may define `disabled_tools` as a list of MCP tool names. Disabled tools are omitted from agent-facing prompts, status counts, `has_tool`, and calls, while detail views can still retrieve them through `get_all_tools()` with a `disabled` flag so users can re-enable them.
87 - Server-specific `init_timeout` and `tool_timeout` override global MCP client timeout settings for list-tools and call-tool operations.
88 - Local stdio server configs accept either strict MCP JSON (`command: "uvx", args: [...]`) or manager-style command lines (`command: "uvx package"`) and normalize them before spawning the process.
tests/test_mcp_handler_multimodal.py
+35
@@ -212,6 +212,41 @@ def test_mcp_config_preserves_dotted_tool_names(mcp_handler_module):
212 assert called == [("alpha.beta", {"value": 7})]
213
214
215 +def test_mcp_config_resolves_advertised_responses_alias(
216 + mcp_handler_module, monkeypatch
217 +):
218 + module, _tmp_path = mcp_handler_module
219 + canonical_name = "google_workspace.search_gmail_messages"
220 + native_name = "google_workspace_search_gmail_messages_ecb900b9"
221 +
222 + class _FakeServer:
223 + name = "google_workspace"
224 +
225 + def has_tool(self, tool_name):
226 + return tool_name == "search_gmail_messages"
227 +
228 + config = module.MCPConfig(servers_list=[])
229 + config.servers = [_FakeServer()]
230 + monkeypatch.setattr(
231 + module.MCPConfig,
232 + "get_for_agent",
233 + classmethod(lambda cls, _agent: config),
234 + )
235 +
236 + agent = SimpleNamespace(
237 + DATA_NAME_RESPONSES_TOOL_NAME_MAP="responses_tool_name_map",
238 + get_data=lambda key: (
239 + {native_name: canonical_name}
240 + if key == "responses_tool_name_map"
241 + else None
242 + ),
243 + )
244 +
245 + assert config.get_tool(agent, canonical_name).name == canonical_name
246 + assert config.get_tool(agent, native_name).name == canonical_name
247 + assert config.get_tool(agent, "local_tool") is None
248 +
249 +
250 def test_mcp_config_call_tool_releases_config_lock_before_await(
251 mcp_handler_module, monkeypatch
252 ):