为 oh-my-openagent 添加内置 arXiv MCP:基于 createBuiltinMcps 三层 MCP 体系的完整改造指南
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
本篇技术指南围绕 oh-my-openagent(OmO)项目中"新增一个内置远程 MCP(arXiv 论文检索)"的完整代码改造展开,覆盖从新建arxiv.ts静态导出、扩展McpNameSchema枚举、在createBuiltinMcps工厂中注册、同步测试用例与模块文档的五个文件级改动。读者读完将掌握内置 MCP 的注册机制、disabled_mcps过滤语义、无鉴权 MCP 的标准实现范式,以及如何在当前仓库中落地一次"最小化、外科手术式"的内置能力扩展。
一、背景:OmO 的三层 MCP 体系与"内置远程 MCP"
在 oh-my-openagent 中,MCP 服务器被组织为三层体系(见 src/mcp 模块说明):
| 层级 | 来源 | 机制 |
|---|---|---|
| Tier 1:内置 | src/mcp/(即packages/omo-opencode/src/mcp/) | 远程 HTTP MCP + 本地 stdio MCP,由createBuiltinMcps()统一创建 |
| Tier 2:Claude Code | .mcp.json | ${VAR}环境变量展开,经claude-code-mcp-loader加载 |
| Tier 3:技能内嵌 | SKILL.md YAML | 由SkillMcpManager管理(stdio + HTTP) |
本文涉及的改造全部落在 Tier 1。当前createBuiltinMcps工厂(见 index.ts)注册了 4 个内置 MCP:
| 名称 | 类型 | 端点 | 鉴权 | 用途 |
|---|---|---|---|---|
websearch | remote | mcp.exa.ai(默认)或mcp.tavily.com | EXA_API_KEY(可选) | 网页搜索 |
context7 | remote | mcp.context7.com/mcp | CONTEXT7_API_KEY(可选) | 库文档检索 |
grep_app | remote | mcp.grep.app | 无 | GitHub 代码搜索 |
lsp | local(stdio) | packages/lsp-tools-mcpCLI | 无 | 代码诊断、定义跳转、符号等 |
其中grep_app是所有"无鉴权远程 MCP"的标准范例——它不依赖环境变量、不需要配置工厂,直接静态导出配置对象(见 grep-app.ts)。新增的 arXiv MCP 正是要复制这一模式,将内置 MCP 数量从 4 提升到 5。
二、新增src/mcp/arxiv.ts:无鉴权静态导出范式
第一步是新建src/mcp/arxiv.ts文件,完整代码如下:
export const arxiv = { type: "remote" as const, url: "https://mcp.arxiv.org", enabled: true, oauth: false as const, }该对象的四个字段与RemoteMcpConfig类型(定义于 index.ts)逐一对应:
type: "remote" as const:声明这是一个远程 HTTP MCP,区别于lsp这类本地 stdio MCP(LocalMcpConfig);url:MCP 服务器端点地址;enabled: true:默认启用,只有在disabled_mcps中显式列出时才被过滤;oauth: false as const:明确无 OAuth 流程。若未来需要鉴权,可参考headers字段(如context7的Authorization: Bearer头)。
⚠️ 合并前阻塞点:文档明确指出,https://mcp.arxiv.org是一个占位 URL,真实端点需要在合并前验证。如果不存在官方托管的 arXiv MCP,备选方案包括:社区托管的 MCP 服务器,或基于 arXiv REST API(export.arxiv.org/api/query)自建包装服务。这一不确定性是本次改造唯一的合并阻塞项——在端点被验证之前,该改动不应合入主分支。
选择静态导出而非配置工厂的原因:arXiv API 是公开的,不需要 API Key,因此无需像websearch那样依据环境变量动态构造配置(对比 websearch.ts 中 Tavily/Exa 的 provider 分支),也无需像context7那样做CONTEXT7_API_KEY的可选鉴权头归一化(对比 context7.ts)。
三、扩展McpNameSchema:把arxiv纳入内置名称枚举
第二步修改src/mcp/types.ts,将arxiv加入 zod 枚举 Schema:
import { z } from "zod" -export const McpNameSchema = z.enum(["websearch", "context7", "grep_app"]) +export const McpNameSchema = z.enum(["websearch", "context7", "grep_app", "arxiv"]) export type McpName = z.infer<typeof McpNameSchema> export const AnyMcpNameSchema = z.string().min(1) export type AnyMcpName = z.infer<typeof AnyMcpNameSchema>这段代码的作用(对应现仓库 types.ts 的实现):
McpNameSchema是一个 zod 枚举,约束了内置 MCP 的合法名称集合——任何配置引用不在枚举内的内置名称都会在校验阶段失败;McpName是由z.infer推导出的联合类型("websearch" | "context7" | "grep_app" | "arxiv"),在工厂、加载器等代码中提供编译期类型安全;AnyMcpNameSchema(任意非空字符串)与AnyMcpName与之互补,用于放开对自定义/第三方 MCP 名称的限制——这正是测试用例中"忽略未知名称"行为的类型基础。
新增arxiv意味着:名称集合从 3 个扩到 4 个,任何引用McpName类型或依赖McpNameSchema解析的代码路径都会同步感知到新成员。
四、在createBuiltinMcps工厂中注册
第三步修改src/mcp/index.ts,改动包含一个 import 和一个注册块:
import { createWebsearchConfig } from "./websearch" import { context7 } from "./context7" import { grep_app } from "./grep-app" +import { arxiv } from "./arxiv" import type { OhMyOpenCodeConfig } from "../config/schema" -export { McpNameSchema, type McpName } from "./types" +export { McpNameSchema, type McpName } from "./types" type RemoteMcpConfig = { type: "remote" url: string enabled: boolean headers?: Record<string, string> oauth?: false } export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenCodeConfig) { const mcps: Record<string, RemoteMcpConfig> = {} if (!disabledMcps.includes("websearch")) { mcps.websearch = createWebsearchConfig(config?.websearch) } if (!disabledMcps.includes("context7")) { mcps.context7 = context7 } if (!disabledMcps.includes("grep_app")) { mcps.grep_app = grep_app } + if (!disabledMcps.includes("arxiv")) { + mcps.arxiv = arxiv + } + return mcps }结合当前仓库 index.ts 的实现,可以提炼出createBuiltinMcps的注册语义:
- 过滤机制:工厂接收
disabledMcps: string[](对应配置中的disabled_mcps),通过includes逐一判断;只过滤内置名称,对playwright、custom等未知名称静默忽略; - 注册顺序:
websearch→context7→grep_app→arxiv,返回的mcps对象键序即此顺序,测试中Object.keys(result).toHaveLength()断言依赖这一约定; - 默认启用:不传
disabledMcps时(= []默认值),全部内置 MCP 注册; - 配置注入:
websearch是唯一消费config的条目(provider 选择),arxiv与context7、grep_app一样直接静态导出、不依赖 config。
五、同步测试:数量断言修正与新用例
第四步修改src/mcp/index.test.ts。由于内置 MCP 总数变化,所有断言Object.keys(result)长度的既有用例必须同步 +1(3 → 4、2 → 3),同时新增一个针对arxiv的过滤用例。完整改动如下:
describe("createBuiltinMcps", () => { test("should return all MCPs when disabled_mcps is empty", () => { // given const disabledMcps: string[] = [] // when const result = createBuiltinMcps(disabledMcps) // then expect(result).toHaveProperty("websearch") expect(result).toHaveProperty("context7") expect(result).toHaveProperty("grep_app") - expect(Object.keys(result)).toHaveLength(3) + expect(result).toHaveProperty("arxiv") + expect(Object.keys(result)).toHaveLength(4) }) test("should filter out disabled built-in MCPs", () => { // given const disabledMcps = ["context7"] // when const result = createBuiltinMcps(disabledMcps) // then expect(result).toHaveProperty("websearch") expect(result).not.toHaveProperty("context7") expect(result).toHaveProperty("grep_app") - expect(Object.keys(result)).toHaveLength(2) + expect(result).toHaveProperty("arxiv") + expect(Object.keys(result)).toHaveLength(3) }) test("should filter out all built-in MCPs when all disabled", () => { // given - const disabledMcps = ["websearch", "context7", "grep_app"] + const disabledMcps = ["websearch", "context7", "grep_app", "arxiv"] // when const result = createBuiltinMcps(disabledMcps) // then expect(result).not.toHaveProperty("websearch") expect(result).not.toHaveProperty("context7") expect(result).not.toHaveProperty("grep_app") + expect(result).not.toHaveProperty("arxiv") expect(Object.keys(result)).toHaveLength(0) }) test("should ignore custom MCP names in disabled_mcps", () => { // given const disabledMcps = ["context7", "playwright", "custom"] // when const result = createBuiltinMcps(disabledMcps) // then expect(result).toHaveProperty("websearch") expect(result).not.toHaveProperty("context7") expect(result).toHaveProperty("grep_app") - expect(Object.keys(result)).toHaveLength(2) + expect(result).toHaveProperty("arxiv") + expect(Object.keys(result)).toHaveLength(3) }) test("should handle empty disabled_mcps by default", () => { // given // when const result = createBuiltinMcps() // then expect(result).toHaveProperty("websearch") expect(result).toHaveProperty("context7") expect(result).toHaveProperty("grep_app") - expect(Object.keys(result)).toHaveLength(3) + expect(result).toHaveProperty("arxiv") + expect(Object.keys(result)).toHaveLength(4) }) test("should only filter built-in MCPs, ignoring unknown names", () => { // given const disabledMcps = ["playwright", "sqlite", "unknown-mcp"] // when const result = createBuiltinMcps(disabledMcps) // then expect(result).toHaveProperty("websearch") expect(result).toHaveProperty("context7") expect(result).toHaveProperty("grep_app") - expect(Object.keys(result)).toHaveLength(3) + expect(result).toHaveProperty("arxiv") + expect(Object.keys(result)).toHaveLength(4) }) + test("should filter out arxiv when disabled", () => { + // given + const disabledMcps = ["arxiv"] + + // when + const result = createBuiltinMcps(disabledMcps) + + // then + expect(result).toHaveProperty("websearch") + expect(result).toHaveProperty("context7") + expect(result).toHaveProperty("grep_app") + expect(result).not.toHaveProperty("arxiv") + expect(Object.keys(result)).toHaveLength(3) + }) + // ... existing tavily test unchanged })这组用例覆盖了工厂的全部行为面:空禁用列表返回全部、单名禁用、全量禁用、忽略自定义名称、默认参数、未知名称过滤,以及新成员arxiv的专属禁用路径。数量断言是这套测试的"注册完整性哨兵"——任何未来新增内置 MCP 而忘记更新断言时,测试会立即失败,从而强制开发者同步维护注册表。
六、模块文档同步:src/mcp/AGENTS.md
第五步更新src/mcp/AGENTS.md,让模块文档与代码保持"生成即同步"的一致性:
-# src/mcp/ — 3 Built-in Remote MCPs +# src/mcp/ — 4 Built-in Remote MCPs **Generated:** 2026-03-06 ## OVERVIEW -Tier 1 of the three-tier MCP system. 3 remote HTTP MCPs created via `createBuiltinMcps(disabledMcps, config)`. +Tier 1 of the three-tier MCP system. 4 remote HTTP MCPs created via `createBuiltinMcps(disabledMcps, config)`. ## BUILT-IN MCPs | Name | URL | Env Vars | Tools | |------|-----|----------|-------| | **websearch** | `mcp.exa.ai` (default) or `mcp.tavily.com` | `EXA_API_KEY` (optional), `TAVILY_API_KEY` (if tavily) | Web search | | **context7** | `mcp.context7.com/mcp` | `CONTEXT7_API_KEY` (optional) | Library documentation | | **grep_app** | `mcp.grep.app` | None | GitHub code search | +| **arxiv** | `mcp.arxiv.org` | None | arXiv paper search | ... ## FILES | File | Purpose | |------|---------| | `index.ts` | `createBuiltinMcps()` factory | -| `types.ts` | `McpNameSchema`: "websearch" \| "context7" \| "grep_app" | +| `types.ts` | `McpNameSchema`: "websearch" \| "context7" \| "grep_app" \| "arxiv" | | `websearch.ts` | Exa/Tavily provider with config | | `context7.ts` | Context7 with optional auth header | | `grep-app.ts` | Grep.app (no auth) | +| `arxiv.ts` | arXiv paper search (no auth) |注意文档中"Env Vars"一列为None,再次确认arxiv属于无鉴权、零环境变量的 MCP。当前仓库的 src/mcp 模块说明 采用的正是这张表格的结构(Name / Type / Endpoint / Env Vars / Tools),新增条目只需照表补一行即可。
七、改动总览与影响面评估
本次改造共涉及 5 个文件,总计约 37 行新增/修改,属于刻意保持的"最小化、外科手术式"改动:
| 文件 | 改动行数 | 类型 |
|---|---|---|
src/mcp/arxiv.ts | +6(新建) | 创建 |
src/mcp/types.ts | 修改 1 行 | 修改 |
src/mcp/index.ts | +5(import + 注册块) | 修改 |
src/mcp/index.test.ts | 约 20 行(数量修正 + 新用例) | 修改 |
src/mcp/AGENTS.md | 约 6 行 | 修改 |
影响面可以分三层评估:
- 编译期:
McpNameSchema与McpName类型变更会传播到所有依赖该枚举的模块;types.ts从 index.ts 中被 re-export(export { McpNameSchema, type McpName } from "./types"),因此任何import { McpName } from "./mcp"的调用方都会感知新成员; - 运行时:
createBuiltinMcps默认返回 5 个内置 MCP(若沿用文档假定的 4 远程 + lsp 的基线则为 5),disabled_mcps新增"arxiv"关键字;用户可在配置中通过disabled_mcps: ["arxiv"]关闭该功能; - 测试与文档:长度断言强制同步,模块文档表格同步更新,防止注册表与文档漂移。
八、合并前的验收清单
基于文档标注的阻塞点与仓库既有实践,合并前应完成:
- 验证端点真实性:确认
https://mcp.arxiv.org是否存在可用的托管 MCP 服务;若不存在,评估社区托管服务器或基于export.arxiv.org/api/query的自建包装方案,并将最终 URL 写入arxiv.ts; - 跑通测试套件:执行
src/mcp/index.test.ts全部用例,确认 6 个既有用例(数量修正后)与 1 个新用例全部通过; - 类型检查:确认
McpNameSchema枚举扩展后无类型错误、无遗漏的 exhaustiveness 检查; - 文档核验:
AGENTS.md的 Built-in 表格、FILES 表格与代码实际状态一致。
同类实现还可参考 senpi 侧的 builtin-mcps 组件——它在 senpi 扩展 API 中以registerMcpServer注册context7与grep_app,并引入lifecycle: "lazy"与exposure: "search"的延迟暴露策略(避免低频工具常驻占用约 1.7K prompt tokens 的 schema 开销)。若未来 arXiv 检索被判定为低频工具,可借鉴该模式将其同样调整为按名延迟暴露。
【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考