尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Qwen Code MCP 工具名 Provider 兼容性规范化:从 Gemini 字符集到全 Provider 安全命名

Qwen Code MCP 工具名 Provider 兼容性规范化:从 Gemini 字符集到全 Provider 安全命名 Qwen Code MCP 工具名 Provider 兼容性规范化从 Gemini 字符集到全 Provider 安全命名【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读本文围绕 Qwen Code 中 MCPModel Context Protocol工具名的 Provider 兼容性问题剖析其设计思路与源码实现为什么 Gemini 下合法的literature.search_pubmed会被 OpenAI 兼容 / Anthropic 兼容端点拒绝以及项目如何通过一条确定性的规范化规则同时保证注册名、权限持久化、重连查找、输出截断与历史会话恢复的一致性。读完本文你将掌握 Qwen Code 的工具名规范化算法字符替换、首字符兜底、FNV-1a 稳定哈希与 63 字符截断、旧名称的兼容策略以及对应的测试覆盖。背景MCP 工具名的跨 Provider 兼容问题Gemini 宽松字符集带来的隐患MCP 服务器中的工具名由服务器名 工具名组合而成。Qwen Code 传统上按mcp__${serverName}__${serverToolName}的格式拼接完整名称例如mcp__zybio__literature.search_pubmed这类名称中的.点号是 Gemini 接受的合法字符Gemini 端点可以正常解析。但问题在于同一个名称会被同时用于注册、权限持久化、重连查找、输出截断和恢复的历史会话等多个环节而更严格的 OpenAI 兼容端点与 Anthropic 兼容端点可能在工具真正执行之前就拒绝这种名称。拒绝发生在模型请求层面——一旦函数声明被拒工具连运行的机会都没有。单一名称、多环节复用的连锁约束在 docs/design/mcp-tool-name-provider-compatibility.md 中设计文档明确指出了核心难点同一个原始名称被独立地重建于多个环节工具注册registry权限规则持久化permission persistence会话重连时的工具查找reconnect lookup输出截断时的名称记录output truncation恢复的历史会话消息restored history。如果只修改发往 Provider 的请求层名称那么模型看到的名称就会与注册表registry中的键不一致导致权限命中失效、工具查找失败等一系列问题。因此正确的做法不是打补丁式地在请求层改名而是在源头统一规范化并让所有环节共享同一个注册名。设计方案一条确定性的 Provider 安全规范化规则规范化规则总览设计文档给出的规则可以概括为四条放行合法名称凡匹配^[A-Za-z][A-Za-z0-9_-]*$且长度不超过 63 字符的名称保持字节级不变。替换非法字符并兜底首字符不支持字符替换为下划线若结果不以字母开头则补tool_前缀。稳定短哈希消歧只要发生规范化或截断就追加一个稳定短哈希防止literature.search_pubmed与literature_search_pubmed规范化后撞名。统一上限 63 字符最终名称不超过 63 字符——这是 Gemini 与更严格的 OpenAI 兼容、Anthropic 兼容 Provider 都能接受的边界。这套规则的另一个关键取舍是不引入 Provider 专属的别名表alias table完全合法的既有名称保持逐字节不变因此 Gemini 行为与内置工具完全不受影响。源码中的核心实现规范化算法的核心实现在 packages/core/src/utils/tool-name-utils.ts入口函数是normalizeToolNameForProvider/** Maximum accepted function-name length across supported providers. */ const MAX_TOOL_NAME_LENGTH 63; const PROVIDER_SAFE_TOOL_NAME /^[A-Za-z][A-Za-z0-9_-]*$/; export function normalizeToolNameForProvider(name: string): string { if ( name.length MAX_TOOL_NAME_LENGTH PROVIDER_SAFE_TOOL_NAME.test(name) ) { return name; } const normalized sanitizeToolNameForProvider(name); const suffix _${stableToolNameHash(name)}; return ${normalized.slice(0, MAX_TOOL_NAME_LENGTH - suffix.length)}${suffix}; } /** Character-only normalization for code that needs an MCP server prefix. */ export function sanitizeToolNameForProvider(name: string): string { const sanitized name.replace(/[^A-Za-z0-9_-]/g, _); return /^[A-Za-z]/.test(sanitized) ? sanitized : tool_${sanitized}; }实现要点逐条对应设计规则快速路径长度 ≤ 63 且通过正则的名称直接原样返回零开销字符替换sanitizeToolNameForProvider将[^A-Za-z0-9_-]含.、空格、中文等统一替换为_首字符兜底替换结果若不以字母开头则加tool_前缀例如!#$%^*()这类纯非法字符名称哈希消歧stableToolNameHash使用 FNV-1a 算法见同文件第 50-56 行先对原始名称整体计算哈希再以_ 7 位 base36 哈希作为后缀最后截断主体部分使总长度恰为 63。注释明确说明选择 FNV-1a 是为了在不引入运行时 crypto 依赖的前提下保持别名稳定。确定性幂等与防碰撞哈希后缀保证了确定性和幂等性同一原始名称每次规范化结果一致稳定已规范化的名称再次经过normalizeToolNameForProvider会命中快速路径结果不变幂等literature.search_pubmed与literature_search_pubmed因原始字符串不同哈希不同因此不会碰撞。这正是设计文档中规范化或截断时必须追加稳定短哈希的用意——仅靠字符替换会导致不同原始名称映射到同一个规范名从而造成工具注册冲突。注册与全环节一致DiscoveredMCPTool的实现注册名的唯一来源MCP 工具的类封装在 packages/core/src/tools/mcp-tool.ts。DiscoveredMCPTool的构造函数中注册名统一由generateValidName生成第 979-994 行super( nameOverride ?? generateValidName(mcp__${serverName}__${serverToolName}), ${serverToolName} (${serverName} MCP Server), // ... );而generateValidName是对规范化函数的薄封装第 1366-1368 行/** Visible for testing */ export function generateValidName(name: string) { return normalizeToolNameForProvider(name); }关键点在于规范化只发生一次之后无论是权限检查、输出截断还是重连查找都用this.name即注册时得到的规范化名称而不是用原始 server/tool 名重新拼接。asFullyQualifiedTool第 1032-1051 行在名称冲突重命名场景下同样调用generateValidName保证派生副本与原始副本遵循同一命名规则。为什么只在源头改一处如果只改 Provider 请求层那么模型可见名称与注册表键会分叉。从源码结构看DiscoveredMCPTool的设计正是为了避免这种分叉构造函数把规范化结果固定为工具对象的name字段后续所有环节权限别名、禁用检查、调度器输出截断中的this.registeredToolName等均引用该字段从而实现全流程使用同一个注册名的设计目标。旧数据兼容权限、禁用工具与历史会话保留原始别名的permissionAliases规范化会改变名称这会让用户在旧版本中配置的权限规则失效。为此DiscoveredMCPTool提供了permissionAliasesmcp-tool.ts/** Keeps pre-normalization permission and disabled-tool entries effective. */ get permissionAliases(): readonly string[] { const legacyName generateLegacyMcpToolName( mcp__${this.serverName}__${this.serverToolName}, ); return legacyName this.name ? [] : [legacyName]; }它从原始 server 名与工具名重建规范化前的旧名称并作为别名暴露给权限与禁用检查逻辑。若别名与当前注册名相同即未发生规范化则不产生额外别名。旧版中间截断算法的兼容在引入哈希后缀之前超长名称使用中间截断算法处理。generateLegacyMcpToolNametool-name-utils.ts精确复刻了该算法/** Recreates the pre-provider-compatibility name for persisted settings. */ export function generateLegacyMcpToolName(name: string): string { let legacyName name.replace(/[^A-Za-z0-9_.-]/g, _); if (legacyName.length MAX_TOOL_NAME_LENGTH) { legacyName legacyName.slice(0, 28) ___ legacyName.slice(-32); } return legacyName; }注意该算法保留点号等字符字符集为[A-Za-z0-9_.-]因此mcp__zybio__literature.search_pubmed这类旧名称可以原样重建。设计文档特别说明旧版中间截断产生的历史名称本身就是 Provider 安全的且其中间被删除的部分无法可靠重建因此转换器不会猜测新的哈希式名称旧权限与禁用工具条目的精确兼容依赖的是 MCP 注册时通过generateLegacyMcpToolName得到的原始名称别名而不是扩大通配匹配范围。禁用工具检查的两个通道isToolDisabledpackages/core/src/tools/tool-registry.ts是禁用工具检查的唯一咽喉点内置工具与 MCP 发现工具都流经registerToolconst hasExactMatch disabledTools.has(name) || aliases.some((alias) disabledTools.has(alias)); if (hasExactMatch || !name.startsWith(mcp__)) { return hasExactMatch; } for (const disabledName of disabledTools) { if (normalizeMcpToolName(disabledName) name) { return true; } }这里包含两条兼容通道精确匹配配置中的禁用名与注册名或permissionAliases别名精确相等规范化匹配对配置中的每个禁用名调用normalizeMcpToolName后与当前注册名比较——这保证用户在旧版本中写入的原始未规范化禁用名仍然生效。normalizeMcpToolName仅在名称以mcp__开头时做规范化tool-name-utils.ts避免影响内置工具名称。历史会话恢复OpenAI / Anthropic 转换器的名称修复转换器中的统一入口规范化之前创建的会话其历史消息里保存的是旧版原始名称。恢复这类会话时若直接把literature.search_pubmed这样的名称发往严格 Provider同样会被拒绝。因此两个转换器在输出端统一调用normalizeMcpToolNameOpenAI 转换器packages/core/src/core/openaiContentGenerator/converter.tsname: normalizeMcpToolName(part.functionCall.name || ),Anthropic 转换器packages/core/src/core/anthropicContentGenerator/converter.tsname: normalizeMcpToolName(part.functionCall.name || ),设计文档要求恢复的 OpenAI 与 Anthropic 请求历史中的 MCP 名称同样被规范化这两个转换器正是落地之处——老会话在恢复后依然可发送。权限规则解析的对称处理权限规则解析器同样使用normalizeMcpToolNamepackages/core/src/permissions/rule-parser.tsnormalizeMcpToolName(pattern) normalizeMcpToolName(toolName)这样用户权限配置中形如mcp__zybio__literature.search_pubmed的旧规则在与规范化后的实际工具名比对时也能命中权限语义在升级后保持不变。验证体系测试覆盖与构建检查设计文档的 Verification 部分在仓库中有完整落地单元测试generateValidNamepackages/core/src/tools/mcp-tool.test.ts 覆盖了设计文档要求的全部场景测试场景断言要点简单合法名myFunction原样返回非法字符替换invalid-name with spaces匹配^[A-Za-z][A-Za-z0-9_-]*$且不保留空格点号 MCP 名mcp__zybio__literature.search_pubmed不含.、结果确定且幂等防碰撞点号名与下划线名规范化结果不同超长截断80 字符名称结果恰为 63 字符、确定、不与其他名称碰撞纯非法字符!#$%^*()结果仍满足 Provider 安全正则长度边界63 字符保持 6364 字符截断为 6380 字符截断为 63其中稳定同一输入多次调用结果一致、幂等规范化结果再规范化不变、防碰撞点号与下划线变体不同名三条性质是设计文档明确要求的。集成层面的验证MCP 工具测试DiscoveredMCPTool的测试覆盖注册、权限别名、禁用工具等路径同文件describe(DiscoveredMCPTool)分组历史转换测试OpenAI / Anthropic 转换器测试覆盖含点号 MCP 名的恢复历史场景对应 converter.ts 与 converter.ts 的测试文件构建与类型检查设计文档要求的 core 包 build 与 typecheck 作为 CI 前置门禁执行。设计取舍小结关注点决策理由合法名称字节级原样保留不影响 Gemini 行为与内置工具非法字符替换为_首字符兜底tool_满足^[A-Za-z][A-Za-z0-9_-]*$碰撞风险追加 FNV-1a 稳定哈希后缀确定性、幂等、零额外运行时依赖名称长度统一 63 字符上限所有目标 Provider 可接受Provider 专属别名表不引入避免维护成本与行为分叉旧权限/禁用条目原始名称别名 规范化比对精确兼容不扩大通配范围旧中间截断名称不猜测新哈希名中间部分不可重建靠注册期原始别名兜底历史会话转换器输出端统一normalizeMcpToolName升级前会话恢复后仍可发送这套方案的核心价值在于把名称规范化收敛为注册时的一次性确定性变换再通过别名与历史转换机制桥接旧数据从而在不牺牲 Gemini 兼容性的前提下让 Qwen Code 的 MCP 工具可以被更严格的 OpenAI 兼容与 Anthropic 兼容 Provider 接受。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表