【Bug已解决】unsupported call: mcp__<server>__<tool>for MCP tools when used with custom Responses API provider (Apps SDK + custom model) 解决方案
原始报错线索:
unsupported call: mcp__<server>__<tool>for MCP tools when used with custom Responses API provider (Apps SDK + custom model)(当使用自定义的 Responses API provider 时,调用 MCP 工具会报unsupported call,因为该 provider 没有声明支持这种命名空间工具)。
一、现象长什么样
用户配了一个「自定义 Responses API provider」(比如自己搭的模型网关)。然后:
- 想用一个 MCP 工具,名字形如
mcp__filesystem__read; - 调用被 provider 拒绝:
unsupported call: mcp__filesystem__read; - 换成官方内置 provider,同样的工具却能调;
- 说明:自定义 provider 的「支持工具清单」里没有
mcp__前缀这套命名,于是拒掉。 根因是工具名带提供方特定的命名空间前缀,切换 provider 时前缀/能力没被适配和声明,目标 provider 不认识就拒绝。
二、背景:工具命名空间与能力声明
模型通过「工具名」调用外部能力。不同 provider 对工具名的约定不同:
- 内置 provider 认识
mcp__<server>__<tool>这种 MCP 命名空间; - 自定义 provider 可能只认识裸名
read,或自己另一套前缀; - 还有的 provider 要求工具先在
tools/list(或等价声明)里登记,未登记即unsupported。 问题出在调用方用了 A 的命名,发给了按 B 约定校验的 provider,中间缺一道「命名空间翻译 + 能力校验」。
三、为什么报 unsupported call:根因
3.1 命名空间前缀未翻译
mcp__filesystem__read是给内置 provider 看的;自定义 provider 要的是read或fs_read。没翻译直接发 → 不认识。
3.2 provider 能力未声明
自定义 provider 的「支持工具集」是空的或不含该工具,调用前没校验就发出去 → 被拒。
3.3 工具名含保留字符
mcp__双下划线可能是某些系统的保留/分隔符,自定义 provider 解析器把它当非法名。
3.4 缺少适配层
调用方直接拼 MCP 名发给 provider,没有「provider 适配器」做转换与兜底。
四、最小可运行复现(命名不匹配被拒)
下面演示「按 A 命名发给 B provider」如何被拒:
class Provider: def __init__(self, name, supported_prefixes): self.name = name self.supported_prefixes = supported_prefixes # 该 provider 认哪些前缀 def call(self, tool_name): prefix = tool_name.split("__")[0] if "__" in tool_name else "" if prefix and prefix not in self.supported_prefixes: raise ValueError(f"unsupported call: {tool_name}") return f"执行 {tool_name}" builtin = Provider("builtin", {"mcp", "native"}) custom = Provider("custom", {"native"}) # 自定义 provider 不认 mcp if __name__ == "__main__": print(builtin.call("mcp__filesystem__read")) # 成功 try: custom.call("mcp__filesystem__read") # 自定义 provider 拒 except ValueError as e: print("被拒:", e) # unsupported call: mcp__filesystem__read自定义 provider 不在supported_prefixes里,直接拒——正是 Bug 表现。
五、解决方案一:provider 能力声明(声明它支持什么)
每个 provider 明确声明「我支持哪些工具命名空间 / 哪些工具」,调用前对照:
def provider_capabilities(provider_id): """返回该 provider 支持的工具前缀/集合。真实环境来自配置或握手。""" caps = { "builtin": {"prefixes": {"mcp", "native"}, "tools": set()}, "custom_a": {"prefixes": {"native"}, "tools": {"read", "write"}}, } return caps.get(provider_id, {"prefixes": set(), "tools": set()}) def is_supported(provider_id, tool_name): caps = provider_capabilities(provider_id) if tool_name in caps["tools"]: return True prefix = tool_name.split("__")[0] if "__" in tool_name else "" return prefix in caps["prefixes"] if __name__ == "__main__": print(is_supported("custom_a", "mcp__filesystem__read")) # False print(is_supported("custom_a", "read")) # True声明 + 校验,调用前就知道会不会被拒(解决 3.2)。
六、解决方案二:命名空间适配层(翻译工具名)
切换 provider 时,把 MCP 命名翻译成目标 provider 认的名字:
def translate_tool_name(tool_name, target_provider): """把 MCP 命名空间工具名,翻译为目标 provider 认识的格式。""" if "__" not in tool_name: return tool_name parts = tool_name.split("__") # mcp__server__tool -> 不同 provider 不同映射 if target_provider == "custom_a": # 剥掉 mcp__server__ 前缀,留裸名(假设自定义 provider 按裸名路由) if parts[0] == "mcp": return parts[-1] # filesystem__read -> read(取末段) return tool_name if __name__ == "__main__": print(translate_tool_name("mcp__filesystem__read", "custom_a")) # read print(translate_tool_name("mcp__filesystem__read", "builtin")) # 原样适配层把mcp__filesystem__read翻成read再发给自定义 provider,它就能认识了(解决 3.1/3.3)。
七、解决方案三:调用前校验 + 友好降级
组合「能力校验 + 翻译」,调用前若仍不支持,给出明确原因而非神秘unsupported:
def safe_call(provider_id, tool_name): translated = translate_tool_name(tool_name, provider_id) if not is_supported(provider_id, translated): return {"ok": False, "reason": f"provider {provider_id} 不支持工具 {tool_name}(已尝试翻译为 {translated})", "suggestion": "请在自定义 provider 中登记该工具,或改用内置 provider"} return {"ok": True, "tool": translated} if __name__ == "__main__": print(safe_call("custom_a", "mcp__filesystem__read")) # {'ok': False, 'reason': '...不支持...', 'suggestion': '...'}翻译后仍不支持,返回结构化原因 + 建议,用户不再对着unsupported call发懵。
八、跨 provider 一致性
- 单一工具注册表:所有 provider 共享一份「逻辑工具 → 各 provider 物理名」映射;
- 握手声明能力:provider 接入时上报支持的工具/前缀,调用方缓存;
- 保留字符约定:工具名避免
mcp__这类被多系统当保留的分隔符,或明确其语义; - 失败可重试到其他 provider:某 provider 不支持,能否路由到支持的。
九、排查清单
「自定义 provider 下工具报 unsupported call」,按下面排查:
- 工具名是否带 provider 专属前缀(如
mcp__)?目标 provider 认吗(第四节); - provider 是否声明了支持的工具/前缀(第五节);
- 是否有命名空间适配层?切换 provider 时翻译工具名(第六节);
- 调用前是否校验?不支持是否给原因而非神秘报错(第七节);
- 保留字符冲突吗?
__是否被目标 provider 当非法; - 能否路由到其他支持的 provider;
- provider 握手是否上报能力;
- 日志是否打印翻译前后工具名。
十、小结
「自定义 provider 下工具调用报 unsupported call」的根因是工具命名空间前缀在 provider 切换时未翻译、且目标 provider 能力未声明/未校验,导致不认识的名字被拒。通用修复:
- 能力声明:每个 provider 声明支持的工具前缀/集合,调用前对照;
- 命名空间适配:切换 provider 时把
mcp__server__tool翻译成目标认的名字(第六节); - 校验 + 友好降级:翻译后仍不支持,返回结构化原因与建议;
- 共享映射 + 握手:逻辑工具到各 provider 物理名的统一映射。 一句话:工具名从来不是「全局唯一」的,它绑定在某个 provider 的命名约定上;跨 provider 调用必须有一层『命名空间适配 + 能力校验』,把 A 的语言翻译成 B 的语言,并在翻译后仍不通时给出明确原因。把适配层做成调用链的标配,自定义 provider 就能和内置 provider 一样顺畅使用 MCP 工具——这与第 122 篇工具路由、第 134 篇能力协商、第 131 篇 MCP 暴露,共同体现「跨提供方的能力必须有统一寻址与适配」。