☰
zcf 项目 MCP 服务配置模块重构实战:业务配置与 i18n 完全分离的 TDD 实现
2026/10/10 2:34:28 网站建设 项目流程
  • 开发工具
  • CLI
  • AI 应用

【免费下载链接】zcf

Zero-Config Code Flow for Claude code & Codex

项目地址:https://gitcode.com/gh_mirrors/zc/zcf
点击查看免费下载

ZCF(Zero-Config Code Flow)是一套面向 Claude Code 与 Codex 的零配置初始化工具链,其中内置的 MCP(Model Context Protocol)服务配置是用户最常接触的能力之一。本文将基于仓库内.zcf/plan/history/mcp-services-refactor.md重构计划,完整复盘"把MCP_SERVICES从constants.ts中剥离、让业务配置与多语言文本彻底分离"这一重构工程,涵盖目标架构、配置数据结构、i18n 翻译体系、TDD 三阶段实施步骤以及完整调用链,读者可以借此掌握一种可复制的"配置模块 + i18n 分离 + 测试驱动"的重构方法论。

一、重构背景:业务配置与多语言文本的"混居"困境

在重构之前,ZCF 的 MCP 服务配置MCP_SERVICES直接内嵌在src/constants.ts中。这意味着同一个数组里同时承载了两类完全不同的职责:

  • 业务配置:服务 ID、启动命令、参数、环境变量、是否需要 API Key 等与运行逻辑强相关的数据;
  • 多语言文本:服务的展示名称(name)、描述(description)等纯展示层内容。

这种"混居"带来的直接问题在于:

  1. 职责耦合:修改文案需要改动业务配置,调整配置又容易误伤翻译,constants.ts日益膨胀;
  2. i18n 无法接入:名称与描述是硬编码的单一语言字符串,无法跟随系统语言动态切换;
  3. 可维护性差:新增一个 MCP 服务需要同时理解两套语义,容易埋下错误。

值得注意的是,ZCF 项目中并非首次遇到这种问题——workflows.ts(工作流配置)早已完成了同样的分离改造,形成了成熟可复用的模式。因此本次重构的上下文非常清晰:参照src/config/workflows.ts的既有模式,为 MCP 服务配置建立同样的"纯业务配置 + 翻译文件"双轨结构(相关上下文记录见 mcp-services-refactor.md)。

二、选定方案:完全分离式架构

重构计划明确选定了"方案 1:完全分离式",其核心设计如下:

  1. 创建src/config/mcp-services.ts:存放纯业务配置(不含任何 i18n 文本);
  2. 完善现有src/i18n/locales/*/mcp.json翻译文件:名称、描述、API Key 提示等展示文本全部迁移至翻译文件;
  3. 创建配置合并函数:提供统一接口,在运行时将业务配置与翻译动态合并,对外暴露完整的McpService对象。

整个改造要求保持现有功能完全兼容,并遵循 TDD(测试驱动开发)模式分三个阶段推进。重构完成后,src/constants.ts中不再保留任何 MCP 服务相关内容(当前仓库中搜索MCP_SERVICES已无任何结果,可证实清理完成)。

三、新配置模块深度解析

3.1 业务配置数据结构:McpServiceConfig

重构后的src/config/mcp-services.ts定义了全新的纯业务配置接口,刻意不包含任何展示字段:

export interface McpServiceConfig { id: string requiresApiKey: boolean apiKeyEnvVar?: string config: McpServerConfig }

字段说明:

字段类型说明
idstring服务唯一标识,也是写入.mcp.json时使用的 key
requiresApiKeyboolean是否需要用户提供 API Key
apiKeyEnvVarstring(可选)API Key 应注入的环境变量名,如EXA_API_KEY
configMcpServerConfig底层 MCP Server 的启动配置

注意:接口中没有name和description字段——测试 mcp-services.test.ts 第 10-24 行专门断言了这一点(expect(config).not.toHaveProperty('name')),从测试层面强制保证"纯业务配置"的纯度。

3.2 内置 MCP 服务全景

MCP_SERVICE_CONFIGS是重构后的核心配置数组,当前仓库中共注册 7 个服务,完整配置如下(源码见 mcp-services.ts):

ID启动方式命令/地址关键环境变量需要 API Key
context7stdionpx -y @upstash/context7-mcp@latest无❌
open-websearchstdionpx -y open-websearch@latestMODE=stdio、DEFAULT_SEARCH_ENGINE=duckduckgo、ALLOWED_SEARCH_ENGINES=duckduckgo,bing,brave❌
spec-workflowstdionpx -y @pimzino/spec-workflow-mcp@latest无❌
mcp-deepwikihttphttps://mcp.deepwiki.com/mcp无❌
Playwrightstdionpx -y @playwright/mcp@latest无❌
exastdionpx -y exa-mcp-server@latestEXA_API_KEY(占位值YOUR_EXA_API_KEY)✅
serenastdiouvx --from git+https://github.com/oraios/serena serena start-mcp-server --context ide-assistant --enable-web-dashboard false无❌

其中有几个细节值得注意:

  • open-websearch的配置最有代表性,通过三个环境变量默认启用 DuckDuckGo 搜索引擎,同时允许bing、brave作为备选,无需 API Key 即可开箱即用。测试 mcp-services.test.ts 对这段配置做了逐字段精确断言;
  • mcp-deepwiki是唯一一个http类型服务,只配置url,没有command/args/env。类型安全测试(同文件第 228-231 行)专门验证了 http 类型的服务必须无 command;
  • exa是唯一需要 API Key 的服务,apiKeyEnvVar指定为EXA_API_KEY,翻译文件中对应提供了apiKeyPrompt文案;
  • serena使用uvx而非npx启动,且--context参数在安装到 Codex 时会被动态替换为codex(见下文的调用链分析)。

3.3 配置合并函数:getMcpServices与getMcpService

src/config/mcp-services.ts的核心价值在于提供了两个异步合并函数,将"静态翻译 + 动态运行时语言"与"纯业务配置"统一缝合:

export async function getMcpServices(): Promise<McpService[]> { ensureI18nInitialized() // 1. 静态声明 i18n 键(兼容 i18n-ally 插件自动提取) const mcpServiceList = [ { id: 'context7', name: i18n.t('mcp:services.context7.name'), description: i18n.t('mcp:services.context7.description') }, // ... 每个服务对应一组 name/description 键 ] // 2. 将 MCP_SERVICE_CONFIGS 与翻译按 id 匹配合并 return MCP_SERVICE_CONFIGS.map((config) => { /* ... */ }) } export async function getMcpService(id: string): Promise<McpService | undefined> { const services = await getMcpServices() return services.find(service => service.id === id) }

合并逻辑的关键行为(源码第 136-158 行):

  • 以MCP_SERVICE_CONFIGS为主循环,按id从静态翻译列表中查找对应的name与description,未命中时用id兜底;
  • 仅当服务requiresApiKey === true且翻译中存在apiKeyPrompt时才附加该字段;
  • 若配置声明了apiKeyEnvVar,则一并注入返回对象。

调用ensureI18nInitialized()保证了在 i18n 未初始化时抛错而非静默返回空翻译,避免了"无文案可用"的隐性故障。

四、i18n 翻译体系:展示文本的完整迁移

4.1 翻译文件结构

名称与描述等展示文本全部迁移至按语言组织的 JSON 文件,由 i18next 的 fs-backend 按{{lng}}/{{ns}}.json路径加载:

  • 中文:src/i18n/locales/zh-CN/mcp.json
  • 英文:src/i18n/locales/en/mcp.json

mcp命名空间(namespace)的键设计采用扁平结构,以services.<服务id>.{name,description}为约定,例如:

{ "configureMcp": "是否配置 MCP 服务?", "selectMcpServices": "选择要安装的 MCP 服务", "services.context7.name": "Context7 文档查询", "services.context7.description": "查询最新的库文档和代码示例", "services.exa.apiKeyPrompt": "请输入 Exa API Key", "services.serena.name": "Serena 助手", "services.serena.description": "提供类似 IDE 的语义代码检索与编辑……" }

英文文件保持同样的键结构,仅替换文案内容。除了各服务的name/description外,命名空间还承载了安装流程提示语(configureMcp、selectMcpServices、mcpBackupSuccess、mcpConfigSuccess)以及 API Key 相关错误文案(apiKeyPrompt、apiKeyApprovalFailed、primaryApiKeySetFailed)。

4.2 键分隔符的设计细节

i18n 初始化配置(src/i18n/index.ts)中有一处关键设定:keySeparator: false, nsSeparator: ':'。这意味着翻译键禁用点号分隔、仅用冒号切分命名空间,因此mcp:services.context7.name中的services.context7.name是一整个扁平键,而非嵌套对象路径。这也是getMcpServices内核对serviceInfo.apiKeyPrompt !== \mcp.services.${config.id}.apiKeyPrompt`` 这一兜底判断的背景:当翻译缺失返回原始键名时,可以识别出来并用空值规避。

4.3 多语言加载路径

翻译文件在开发与打包场景下通过多条候选路径定位(src/i18n/index.ts):开发期读取src/i18n/locales,npm 包安装后读取node_modules/zcf/dist/i18n/locales,生产构建读取./dist/i18n/locales,并依次探测直到找到zh-CN/common.json。所有命名空间在initI18n时逐一loadNamespaces预加载,确保getMcpServices执行时翻译立即可用。

五、类型系统:重构后的契约边界

重构并未改变公开类型契约,src/types/CLAUDE.md中记录了完整的类型定义:

export interface McpService { id: string name: { 'en': string, 'zh-CN': string } // 运行时被解析为 string description: { 'en': string, 'zh-CN': string } requiresApiKey: boolean apiKeyPrompt?: { 'en': string, 'zh-CN': string } apiKeyPlaceholder?: string apiKeyEnvVar?: string config: McpServerConfig } export interface McpServerConfig { type: 'stdio' | 'sse' // 当前仓库实际还使用 http 类型 command?: string args?: string[] url?: string env?: Record<string, string> }

McpServerConfig是写入 Claude/Codex 配置文件中mcpServers字段的基本单元(mcpServers: Record<string, McpServerConfig>)。重构前调用方若直接依赖McpService的字符串name/description,重构后仍能获得同样形状的对象——这正是"保持完全兼容"的类型层保证。类型安全测试 mcp-services.test.ts 对stdio必须携带command+args、http必须携带url的约束做了系统性校验。

六、TDD 三阶段实施流程

重构计划为工程落地设计了严谨的三阶段 TDD 节奏(详见 mcp-services-refactor.md 的"实施步骤"章节):

阶段 1:测试驱动的配置模块创建

  1. 先写测试用例:覆盖MCP_SERVICE_CONFIGS的结构约束(无硬编码 name/description)、服务 ID 全集、open-websearch与mcp-deepwiki的精确配置;
  2. 再建模块:创建src/config/mcp-services.ts,实现McpServiceConfig接口与MCP_SERVICE_CONFIGS;
  3. 实现合并函数直至测试通过:迭代实现getMcpServices/getMcpService,用红-绿-重构循环驱动。

阶段 2:测试驱动的 i18n 完善

  1. 编写翻译完整性测试:验证中英文服务数量一致、ID 列表一致、英文文案不含中文字符(测试中通过正则/[\u4E00-\u9FA5]/检测);
  2. 完善翻译文件:补齐zh-CN/mcp.json与en/mcp.json的全部键;
  3. 更新类型定义:同步McpService等类型的可选字段与注释。

阶段 3:测试驱动的代码迁移

  1. 编写集成测试验证新旧兼容性:确认所有调用方拿到的McpService对象字段完整;
  2. 更新所有引用MCP_SERVICES的代码:将src/commands/init.ts、src/utils/features.ts、src/utils/mcp-selector.ts、src/utils/code-tools/codex-configure.ts统一切换到新模块;
  3. 清理src/constants.ts:删除旧的MCP_SERVICES常量。

重构计划预期的最终状态是:业务配置与 i18n 完全分离、现有功能完全兼容、代码更清晰易维护、测试覆盖率 100%。当前仓库中src/constants.ts已无任何 MCP 内容、src/i18n/locales/*/mcp.json与tests/config/mcp-services.test.ts均已落地,可确认三个阶段全部完成。

七、完整调用链:从选择器到配置落盘

重构的价值最终体现在调用方是否真正获益。梳理仓库源码,MCP 服务配置的完整消费链路如下:

7.1 统一选择器selectMcpServices

src/utils/mcp-selector.ts 是重构的最大受益者:它只需调用getMcpServices()拿到带翻译的服务列表,就能用 inquirer 的 checkbox 渲染多选菜单(${service.name} - ${service.description}),彻底摆脱了对翻译文件的直接依赖:

export async function selectMcpServices(): Promise<string[] | undefined> { ensureI18nInitialized() const mcpServices = await getMcpServices() const choices = mcpServices.map(service => ({ name: `${service.name} - ${ansis.gray(service.description)}`, value: service.id, selected: false, })) // inquirer checkbox prompt... }

7.2 安装流程init.ts的 MCP 步骤

src/commands/init.ts第 915-994 行展示了完整的安装逻辑:

  1. 调用selectMcpServices()获取用户勾选的服务 ID 列表(支持--skip-prompt跳过);
  2. 若已有 MCP 配置,先backupMcpConfig()备份(输出mcp:mcpBackupSuccess文案);
  3. 遍历每个选中服务,通过getMcpServices()取回完整对象;
  4. 针对serena做特殊处理:根据当前代码工具类型(Codex 或 Claude Code)动态调整--context参数值;
  5. 针对需要 API Key 的服务(如exa):弹出service.apiKeyPrompt输入框,调用buildMcpServerConfig注入密钥;
  6. mergeMcpServers与现有配置合并 →fixWindowsMcpConfig修复 Windows 命令包装 →writeMcpConfig落盘(成功输出mcp:mcpConfigSuccess)。

同样的流程也复用在src/utils/features.ts(configureMcpFeature)中,二者共享selectMcpServices与getMcpServices,消除了重复实现。

7.3 API Key 注入的实现细节

buildMcpServerConfig(src/utils/claude-config.ts)承担了密钥注入职责,其处理优先级是:

  1. 深拷贝基础配置,避免污染MCP_SERVICE_CONFIGS共享对象;
  2. 平台适配:在 Windows 上若命令需要包装则改写为cmd /c形式;
  3. 若提供了envVarName且配置带env:直接config.env[envVarName] = apiKey(如EXA_API_KEY)并提前返回——这是新式注入路径;
  4. 否则走旧式占位符替换:把YOUR_EXA_API_KEY等占位符替换进args和url。

测试 mcp-services.test.ts 验证了exa服务经getMcpService('exa')后同时携带apiKeyPrompt与apiKeyEnvVar: 'EXA_API_KEY',保证两条注入路径所需信息齐全。

7.4 Codex 侧的接入

src/utils/code-tools/codex-configure.ts也引入了新模块(import { getMcpServices ... }),说明这次重构同时为 Claude Code 与 Codex 两套目标工具的配置生成提供了统一的 MCP 服务数据源,印证了计划中"保持现有功能完全兼容"的达成。

八、测试保障:重构质量的量化证据

测试文件 tests/config/mcp-services.test.ts 是本次重构"测试先行"的直接产物,围绕四个维度构建了 10 余条用例:

测试维度核心断言
配置纯度每个配置项含id/requiresApiKey/config,且不含name/description
配置正确性服务 ID 全集、open-websearch的完整 env、mcp-deepwiki的 http 结构
翻译完整性中英文服务数量与 ID 列表一致、英文文案无中文字符、name/description 非空
查询行为getMcpService按 ID 命中、未知 ID 返回undefined、API Key 字段正确附加

在init.ts、init-validation.test.ts、init-param-validation.test.ts等调用方测试中,src/config/mcp-services已成为标准的 mock 目标(例如vi.mock('../../../src/config/mcp-services')),说明新模块已成为测试基础设施中的一等公民,任何调用方测试都可独立 mock 该配置源。

九、重构收益与可复用方法论

对照重构计划的"预期结果",本次改造的实际收益可以归纳为:

  1. 职责彻底分离:业务配置与 i18n 文本分居 mcp-services.ts 与 mcp.json,constants.ts回归纯粹;
  2. 零兼容性破坏:McpService/McpServerConfig公开契约不变,四个调用方平滑切换;
  3. 翻译体系统一:新增服务只需"业务配置数组加一条 + 两种语言 JSON 各加一组键",规则明确;
  4. 测试驱动落地:从"先写测试"到"集成验证"全程有测试护航,测试文件同时充当配置的活文档;
  5. 模式可复用:workflows.ts→mcp-services.ts证明该"配置 + i18n 分离 + 合并函数 + TDD"的路径在 ZCF 内已形成标准范式。

若要在自己的项目中复制这套模式,推荐路径是:先选定参照模块(如本项目的workflows.ts)→ 定义纯业务配置接口并先写纯度测试 → 迁移文案到命名空间 JSON → 实现合并函数直至全绿 → 逐个替换调用方并清理旧常量。

  • 开发工具
  • CLI
  • AI 应用

【免费下载链接】zcf

Zero-Config Code Flow for Claude code & Codex

项目地址:https://gitcode.com/gh_mirrors/zc/zcf
点击查看免费下载

相关推荐

上一篇:Unlock Music 完整指南:浏览器免费解锁 15 种以上加密音乐格式
下一篇:Bonsai-demo API密钥管理:5个BRAVE_API_KEY安全配置最佳实践(新手友好)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询