跳到主要内容

MCP 服务器(旗舰:两个工具 · 传输 · 上下文 · 安全)

30 秒导读: packages/mcp 是 Context7 里工程含量最高的一支——它是一个标准的 MCP(Model Context Protocol,模型上下文协议)服务器,把「查最新文档」这件事只暴露成两个工具给 AI 编码助手用。真正的难点不在调后端(那是 01 讲的),而在:用超长的工具描述当提示词去教模型「先解析库 ID、再查文档、每问最多 3 次」;让同一套逻辑同时跑在 stdio 和 HTTP 两种传输上;以及在多用户 HTTP 场景下把每个请求的身份、IP、会话互相隔离。本章讲透这四件事。


1. 这是什么(零基础也能懂)

一句话定义: 它是一个 MCP 服务器——一个专门给 AI(Cursor、Claude Code、Codex 等)调用的「工具后端」,让 AI 在回答「怎么用某个库」时能现查最新官方文档,而不是凭训练时的旧记忆瞎编。

解决什么问题 / 给谁用。 假设你在 Cursor 里让 AI「用 Next.js 15 的新 API 写个路由」。模型的训练数据可能停在 Next.js 13,于是它编了一个已经不存在的写法。Context7 MCP 就是插在中间的那层:AI 先问它「Next.js 现在怎么写」,它去 Context7 后端取回当前版本的文档片段,AI 再据此作答。用户是跑在 AI 编码助手里的模型,不是人直接用。

它能做什么(功能)。 对模型只暴露两个能力:

工具干什么
resolve-library-id把「Next.js」这种名字解析成 Context7 内部的库 ID(/vercel/next.js)
query-docs拿着库 ID + 一个具体问题,取回相关文档和代码示例

用起来什么样。 用户几乎无感——这是一段 AI 客户端与本服务器之间的 JSON-RPC 对话,大致是:

① 模型 → resolve-library-id { libraryName: "Next.js", query: "app router 里怎么做流式渲染" }
服务器 → "Available Libraries: - Context7-compatible library ID: /vercel/next.js ..."

② 模型 → query-docs { libraryId: "/vercel/next.js", query: "streaming in app router" }
服务器 → "<最新文档片段 + 代码示例>"

一句话直觉。 把这台服务器想成一个只会两句话的图书管理员:你先报书名让它查到书的索书号(resolve),再拿索书号问它某一页的具体内容(query)。它自己不写代码、不做重构——只负责把「最新的那一页」递给模型。

本章聚焦 packages/mcp 这个服务器进程本身。它和后端 HTTP API 怎么对话(端点、请求格式)是另一支 lib/api.ts 的独立实现,细节见 01-sdk-api-contract.md;本章只在 §7 点到它在 MCP 里的角色,不重复端点细节。


2. 顶层全景(它大概怎么转)

整个 packages/mcp 只有一个源文件 src/index.ts(入口 + 传输 + 路由)加一个 src/lib/(后端调用、加密、鉴权、会话)。核心是一个工厂函数 createMcpServer(),和一个按传输分叉的 main()

怎么读下面这张图: 从上往下是「一次工具调用」的生命周期;左右两条竖线是两种传输,它们在中间汇合到同一个 createMcpServer() 产出的 server 实例。

启动:main() index.ts:330

┌─────────────────┴──────────────────┐
--transport stdio --transport http
│ │
┌──────────▼──────────┐ ┌───────────▼───────────┐
│ 全局单会话 │ │ express + CORS 每请求 │
│ stdioApiKey / │ │ /mcp /mcp/oauth │
│ stdioSessionId │ │ 抽 API key、算 IP、 │
│ (index.ts:87-90) │ │ 建/续会话(Redis) │
└──────────┬──────────┘ └───────────┬───────────┘
│ │
│ 每个请求包一层上下文 │
│ requestContext.run(ctx, …) │
│ (AsyncLocalStorage) │
└─────────────────┬───────────────────┘

installTransportArgAliasing(transport) ← 在 Zod 校验前纠偏参数名

┌───────────▼────────────┐
│ createMcpServer() │ index.ts:112
│ · server.instructions │ 何时用这台服务器
│ · resolve-library-id │ ┐ 两个工具,description
│ · query-docs │ ┘ = 教模型的提示词
└───────────┬────────────┘
│ 工具体内 getClientContext()

lib/api.ts → Context7 后端(见 01)
lib/encryption.ts 加密 IP 做遥测/限流
lib/auth/auth-prompt.ts 匿名用户 elicitation 引导登录

部件一句话职责:

部件干什么在哪
main()解析 CLI,按 --transport 分叉出 stdio / http 两条启动路径index.ts:330
createMcpServer()造出 server 实例,注册 instructions 和两个工具index.ts:112
两个工具的 description超长自然语言,模型「先 resolve 再 query、每问≤3 次」index.ts:138225
requestContextAsyncLocalStorage,把每个 HTTP 请求的身份/IP/会话隔离开index.ts:84
getClientContext()工具体内取「当前调用者是谁」:HTTP 读 ALS,stdio 读全局index.ts:95
installTransportArgAliasing在 Zod 校验前,把模型幻觉出的参数名改写回规范键index.ts:310
lib/api.ts独立于 SDK 的一套后端调用(search / context)lib/api.ts
lib/*(加密/jwt/会话/IP/工具/auth-prompt)遥测加密、令牌校验、会话存储、IP 提取、结果格式化、登录引导src/lib/

3. 旗舰重点:两个工具的描述如何「教」模型

这是本章的核心。MCP 工具的 description 字段不是给人看的注释,而是直接进模型上下文的提示词——模型读它来决定「要不要调、怎么调、调几次」。Context7 把大量心思花在这两段描述上。

3.1 为什么只有两个工具,且必须「先 resolve 再 query」

要解决的小问题: 模型不知道 Context7 内部的库 ID 长什么样(/vercel/next.js),它只知道「Next.js」。如果直接让它猜 ID,几乎必错。

思路: 拆成两步——先用名字换 ID,再用 ID 换文档。为了让模型照这个顺序走,描述里用大写强命令明说依赖关系:

// resolve-library-id 的 description,index.ts:138-170(节选)
"You MUST call this function before 'Query Documentation' tool to obtain a
valid Context7-compatible library ID UNLESS the user explicitly provides a
library ID in the format '/org/project' ..."

query-docs 侧也回扣同一条约束(index.ts:227):"You must call 'Resolve Context7 Library ID' tool first ... UNLESS the user explicitly provides a library ID"。两段描述互相引用、口径一致,是刻意的提示词设计:给模型一条清晰、无歧义的调用图。

3.2 用描述给模型「立规矩」:选择流程 + 调用预算

resolve-library-id 的描述不止说「怎么调」,还教模型怎么在多个结果里选、以及别调太多次:

  • 选择依据明确列成清单(index.ts:153-160):名字相似度、来源信誉(High/Medium/Low)、代码片段数、Benchmark 分数、相关性。
  • 调用预算写死上限(index.ts:170):"IMPORTANT: Do not call this tool more than 3 times per question."query-docs 同样有 "Do not call this tool more than 3 times per question."(index.ts:229)。

这条「每问≤3 次」是纯提示词层面的护栏——服务器不强制它(没有计数器),完全靠描述说服模型自律,避免它对一个问题反复刷接口。

3.3 用参数的 describe 继续「教」

连每个入参的 .describe() 都在教模型。两个值得注意的点:

教它写「好查询」。 query-docsquery 参数描述给了正反例(index.ts:236-240):

Good: 'How to set up authentication with JWT in Express.js' … Bad (too vague): 'auth' … Bad (too broad): 'routing and auth and caching in Next.js'

它明确要求一次只查一个概念,把宽问题拆成多次调用。这直接影响后端召回质量。

教它防数据泄漏。 两个工具的 query 描述都带同一句安全提示(index.ts:175239):"Do not include any sensitive or confidential information such as API keys, passwords, credentials, personal data, or proprietary code in your query." 因为查询会发到 Context7 后端,这句是在提示词层面做出网数据的自我审查

3.4 用 annotations 给客户端「贴标签」

除了自然语言,还有一组结构化的 annotations,给客户端 UI / 编排器读(不是给人):

annotation含义
readOnlyHinttrue只读,不改任何状态
destructiveHintfalse无破坏性
openWorldHinttrue与外部世界(后端 API)交互
idempotentHinttrue同参数重复调用等价

两个工具的 annotations 完全一样(index.ts:183-188242-247)。readOnlyHint: true 让支持自动批准的客户端可以免确认放行这类调用——因为它保证不会改坏任何东西。

3.5 server.instructions:什么时候该用这台服务器

比工具描述更高一层的是 server.instructions(index.ts:127-131)——它进的是模型的系统级上下文,回答的是「要不要用这台服务器」,而不是「怎么调某个工具」。

它做了两件事,都很克制:

  • 正面放宽触发面: 明说「哪怕是 React、Next.js 这种你以为很熟的库也要用,因为你的训练数据可能过时」——鼓励模型多用,别自作聪明跳过。
  • 反面划边界: "Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts." 把「查库文档」和「写业务逻辑」区分开,避免模型在不相关场景乱调。

何时该写 instructions? 当你想影响的是「这台服务器整体在什么场景登场」,就写在 instructions;当你想影响「某个具体工具怎么调」,就写在该工具的 description。Context7 恰好示范了这个分工:instructions 管选不选这台服务器,description 管这台服务器里两个工具的用法与预算


4. 双传输:同一套逻辑,两种「上场方式」

main()(index.ts:330)按 --transport 分叉。同一个 createMcpServer() 产出的 server,既能通过标准输入输出(stdio)跑在本地编辑器旁,也能通过 HTTP 跑成多用户的远程服务。

4.1 CLI 校验:三个 flag 的互斥规则

启动前,commander 解析 --transport / --port / --api-key(index.ts:36-42),然后做一串前置校验,把不合法的组合直接挡在门外:

规则代码为什么
--transport 只能是 stdio/httpindex.ts:51-57非法值直接 exit(1)
http 下禁用 --api-keyindex.ts:66-71HTTP 是多用户,key 应走请求头,不是进程级
stdio 下禁用 --portindex.ts:73-76stdio 不监听端口,给 port 无意义

注意校验用的是 process.argv.includes("--port") 这种**「用户到底传没传这个 flag」**的判断(index.ts:63-64),而不是看解析后的值——因为 --port 有默认值 3000(DEFAULT_PORT,index.ts:33),不能靠值区分「用户传的」和「默认的」。

4.2 stdio:最简单的一支,全局单会话

stdio 分支(index.ts:622-649)对应「本地编辑器直接 spawn 一个子进程」的场景:一个进程只服务一个客户端,所以状态可以是进程级全局

  • API key 来自 --api-keyCONTEXT7_API_KEY 环境变量,存进全局 stdioApiKey(index.ts:623)。
  • 整个进程只有一个 stdioSessionId,启动时 randomUUID() 一次(index.ts:624)。
  • 客户端信息(IDE 名、版本)在 MCP 初始化握手完成后,从 oninitialized 回调里读一次存进全局(index.ts:635-643)。
  • 收到 stdin end/closeSIGHUP 就退出(index.ts:626-628)——跟随父编辑器生命周期。

4.3 streamable-http:多用户的一支,每请求上下文

HTTP 分支(index.ts:333 起)是工程量最大的地方。它用 express 起服务,核心是 handleMcpRequest(index.ts:383)。几个关键决策:

CORS 全开 + 暴露会话头。 允许任意 Origin,并显式放行/暴露一批自定义头(MCP-Session-Id、几种 API key 头等,index.ts:339-353),让浏览器端 MCP 客户端能读到会话 ID。

拒绝 GET(返回 405)。 会话虽存在 Redis,但这台服务器不发服务器主动通知,所以 SSE 长连接没用还会拖垮 NGINX,直接 405(index.ts:391-397)。这是对 MCP Streamable HTTP 规范的一个刻意裁剪。

会话生命周期由路由自己管,不交给 SDK。 这是 HTTP 分支最巧的一处:

POST 且是 initialize 请求 → 新建会话:randomUUID() + sessionStore.create,回写 mcp-session-id 头 index.ts:459-462
POST 且带已有 session → sessionStore.refresh;不存在则 404 让客户端重新初始化 index.ts:463-477
DELETE 且带 session → sessionStore.delete index.ts:446-456
其它 → 400 Bad Request index.ts:478-484

判断「是不是初始化请求」用的是 SDK 的 isInitializeRequest(req.body)(index.ts:459463)。正因为会话由路由 + Redis 管,构造 StreamableHTTPServerTransport 时特意把 sessionIdGenerator: undefined(index.ts:496-499)——不让 SDK 自己生成会话。同时 enableJsonResponse: false 用 SSE 响应:SDK 会在解析完请求后立刻刷响应头,避免长耗时工具让某些客户端在 60s 无头时超时(注释在 index.ts:490-495)。

两个端点,一个开关。 /mcp 匿名(requireAuth=false),/mcp/oauth 强制鉴权(requireAuth=true),共用同一个 handleMcpRequest(index.ts:526-533)。鉴权时若 key 是 JWT,就走 validateJWT(见 §8.2);OAuth 发现所需的 .well-known/* 端点也在这一支里(index.ts:541-577)。

端口占用自动递增。 startServer 监听失败若为 EADDRINUSE,就 port+1 重试,最多 10 次(index.ts:601-619)。


5. 上下文隔离:一个 stdio 全局 vs HTTP 每请求

这是 stdio/http 双传输带来的核心难题:工具体内怎么知道「当前这次调用是谁发的」? stdio 只有一个用户,全局变量够用;HTTP 同时有很多用户,全局变量会串号。Context7 用 AsyncLocalStorage 把两者统一到一个入口 getClientContext()

思路: 定义一个 requestContext = new AsyncLocalStorage<ClientContext>()(index.ts:84)。HTTP 每请求都 requestContext.run(context, …) 包一层(index.ts:510-512),于是在这次请求异步链路里任何地方读 ALS 都能拿到只属于这个请求的上下文。stdio 没有 ALS,就回落到进程级全局(stdioApiKey / stdioClientInfo / stdioSessionId,index.ts:87-90)。

// index.ts:95-110,简化示意
function getClientContext(): ClientContext {
const ctx = requestContext.getStore(); // HTTP:每请求各一份
if (ctx) return ctx; // 拿到就用
return { // stdio:回落全局
apiKey: stdioApiKey,
clientInfo: stdioClientInfo,
transport: "stdio",
sessionId: stdioSessionId,
};
}

关键点: 工具体内永远只调 getClientContext()(index.ts:191250),完全不关心自己跑在哪种传输下——隔离逻辑被这一个函数吸收干净。HTTP 侧组装 context 时会填入加密前的真实 clientIp、从 header 抽出的 apiKey、从 User-Agent 解析的 clientInfo(index.ts:437-442);stdio 侧这些字段大多为空,只有全局那几个。

ClientContext 里还有一个可变字段 shouldPrompt(types.ts:44-46):它在请求过程中被后端信号回填(见 §8.4),是上下文对象「双向」用途的一个例子。


6. 参数名纠偏:在 Zod 校验前救回幻觉的键

要解决的小问题: 模型经常不照工具 schema 的字面键名传参,而是照描述里的措辞造一个。比如把 query 传成 userQuery,把 libraryId 传成 libraryIDcontext7CompatibleLibraryID。这些会在 Zod 校验时直接报错,工具根本没机会跑。

思路: 在 JSON-RPC 消息进入 SDK 校验之前,拦下来把别名改写回规范键。分两级别名表:

内容代码
GLOBAL_ALIASES所有工具通用:query ← userQuery / questionindex.ts:281-283
TOOL_ALIASES按工具名限定:query-docslibraryId ← context7CompatibleLibraryID / libraryID / libraryNameindex.ts:288-292

为什么 libraryName 只在 query-docs 上算别名?因为它对 resolve-library-id合法规范键——所以只能工具级纠偏,不能全局(index.ts:285-287 注释点破了这点)。

改写逻辑 applyAliases(index.ts:294-305):规范键已存在就跳过;否则按候选顺序找到第一个存在的别名,搬过去、删掉原键。

挂载时机是关键。 installTransportArgAliasing(index.ts:310-328)直接覆写 transport.onmessage,且必须server.connect(transport) 之前装(index.ts:507645)。因为 SDK 的 Protocol.connect() 会捕获已有的 onmessage 并把自己的分发链在其后——所以先装的钩子先跑,能赶在 SDK 校验前修好参数。它只处理 tools/call 消息,其它放行。

// index.ts:310-328,简化示意
transport.onmessage = (message) => {
if (message.method !== "tools/call") return; // 只管工具调用
const args = message.params?.arguments;
applyAliases(args, GLOBAL_ALIASES); // 先全局纠偏
const tool = message.params?.name;
if (tool in TOOL_ALIASES) applyAliases(args, TOOL_ALIASES[tool]); // 再按工具纠偏
};

7. lib/api.ts:独立于 SDK 的一套后端调用

工具体内不直接 fetch,而是走 lib/api.ts 的两个函数:searchLibraries(api.ts:110)对应 resolve-library-id,fetchLibraryContext(api.ts:144)对应 query-docs。它直连 CONTEXT7_API_BASE_URL(constants.ts:14,默认 https://context7.com/api),端点是 /v2/libs/search/v2/context

端点的完整请求/响应契约见 01-sdk-api-contract.md;这里只讲 MCP 这一支在调用外面多做的几件事。

给模型/人可读的错误文案。 parseErrorResponse(api.ts:15-38)先试着读后端返回的 message,读不到就按状态码给带行动指引的文案:

状态文案要点代码
429限流/超额;有 key → 去升级套餐,无 key → 去建免费 keyapi.ts:26-30
404库不存在,换个库 ID 再试api.ts:31-33
401key 无效;提示 key 应以 ctx7sk 前缀开头api.ts:34-36

企业网络支持:代理 + 自定义 CA。 模块加载时读 HTTPS_PROXY 等环境变量(api.ts:40-45),若是合法 http(s) 代理就 setGlobalDispatcher(new ProxyAgent(...))(undici,api.ts:71-85);若只配了 NODE_EXTRA_CA_CERTS,就 loadCustomCACerts() 把自定义证书叠加到系统默认 CA 上(api.ts:57-6986-95)。这让它能在企业 MITM 代理后面工作。

读「该提示登录」信号。 每次响应都过一遍 readPromptSignal(api.ts:97-101):若后端回了 X-Context7-Auth-Prompt: 1 头,就把 context.shouldPrompt = true——供 §8.4 的登录引导使用。信号由后端决定何时发(每会话至多一次),服务器自己不存抑制状态。


8. 安全与遥测:lib 里的其余零件

8.1 加密客户端 IP 再上报(encryption.ts)

后端要用客户端 IP 做遥测/限流,但明文 IP 不该随便上报。generateHeaders(encryption.ts:36)在给每个后端请求拼 header 时,把 IP 过一遍 encryptClientIp:用 AES-256-CBC(encryption.ts:7)、每次随机 16 字节 IV 加密,输出 iv:密文(encryption.ts:14-30),放进 mcp-client-ip 头。

密钥来自 CLIENT_IP_ENCRYPTION_KEY 环境变量,缺省用一个写死的 DEFAULT_ENCRYPTION_KEY(encryption.ts:5-6)。密钥格式不合法(非 64 位 hex)或加密出错时,回落到明文 IP而非崩溃(encryption.ts:15-1826-29)——遥测不该阻断主流程。同一函数还顺带贴上来源、版本、会话、传输类型等遥测头。

8.2 JWT 校验(jwt.ts)

HTTP 的 /mcp/oauth 端点会对 JWT 令牌验签。isJWT 只做最粗的形状判断——按 . 切成 3 段(jwt.ts:70-72)。validateJWT(jwt.ts:74)先解出 iss,再按签发方三选一走不同 JWKS:

签发方判断依据JWKS
Microsoft Entra v2iss 匹配 login.microsoftonline.com/<tenant>/v2.0 正则(jwt.ts:15)按 tenant 拉,可选校验 requiredScope
EMA(企业托管鉴权)iss === EMA_ISSUERContext7 自己的 JWKS
Clerk(默认)其它Clerk JWKS

失败时按异常类型给精确原因:过期 / 声明无效 / 签名无效 / 泛化「Invalid token」(jwt.ts:108-119)。Entra 的 tenant 配置带 5 分钟缓存,且对 404「未配置」也缓存,避免每次验签都打后端(jwt.ts:35-63)。

8.3 会话存储与 IP 提取

会话(sessionStore.ts)。 createSessionStore(sessionStore.ts:11)基于 Upstash Redis,key 前缀 #mcp#session#,TTL 7 天(sessionStore.ts:3)。设计上fail-open:会话 ID 只是日志关联/规范合规用的不透明标识,不是鉴权凭证,所以 Redis 挂了就记日志放行(sessionStore.ts:7-10)。refresh 用一次 ttl 调用同时判断「存不存在」和「剩多久」,只有快过期(<1 天)才写 expire,省写(sessionStore.ts:25-39)。

IP 提取(client-ip.ts)。 getClientIp(client-ip.ts:64)优先解析 X-Forwarded-For,从链里挑第一个公网 IP(pickClientIpFromForwardedList),isPrivateOrLocalIp(client-ip.ts:10)覆盖 RFC1918、CGNAT、loopback、link-local 和 IPv6 私有段,还会剥掉 ::ffff: 这种 IPv4-mapped 前缀。这样代理链里的内网地址不会被误当成客户端 IP。

结果格式化(utils.ts)。 formatSearchResults(utils.ts:66)把搜索结果拼成模型好读的纯文本(每条含库 ID、描述、片段数、信誉标签、Benchmark 分等);extractClientInfoFromUserAgent(utils.ts:89)用正则从 Cursor/2.2.44 (...) 这类 UA 里抽出 IDE 名与版本,喂给遥测头。

8.4 匿名用户的登录引导(auth-prompt.ts)

匿名用户有较低的限流。当后端发了 §7 那个信号(shouldPrompt=true)、且用户没带 key、且客户端声明支持 elicitation 能力时,maybeElicitAuthSignIn(auth-prompt.ts:66)会弹一个表单式 elicitation,引导用户去跑一条一键接入命令。

命令由 buildAuthCommand(auth-prompt.ts:15)按客户端类型拼:检测到 Cursor/Claude/Codex 等就带上对应 flag,如 npx ctx7 setup --claude --mcp -y(一键接入即 03-cli.mdctx7 setup)。弹窗给两个选项(auth-prompt.ts:44-45):去登录 / 继续匿名。

三个关键设计:

  • 走 elicitation 而非塞进工具结果。 提示是带外发给人的客户端 UI,不进模型读的 tool result,所以不会触发提示词注入防护(auth-prompt.ts:52-54 注释)。
  • fire-and-forget。 void ... .catch(() => {})(auth-prompt.ts:70-90),永不阻塞、永不拖垮外层工具响应。
  • 抑制状态在后端。 服务器不记「弹过没」,后端每会话至多发一次信号,服务器见信号才弹。

9. 巧妙之处(可借鉴)

  • 工具描述当提示词工程。 把「先 resolve 再 query」「每问≤3 次」「查询要单概念」「别放敏感信息」全写进 description/.describe(),用自然语言给模型立规矩,而非在代码里硬控(index.ts:138-170236-240)。
  • 一个 getClientContext() 吸收双传输差异。 工具体零感知传输,靠 ALS(HTTP)/ 全局(stdio)在一个函数里分流(index.ts:95-110)。
  • 参数纠偏钩子抢在 SDK 校验前。 利用 Protocol.connect() 链式 onmessage 的顺序,先装先跑,把模型幻觉的键名救回来(index.ts:307-328)。
  • 会话 fail-open + 单 TTL 调用。 把「不是鉴权凭证」的会话做成 Redis 挂了也不阻断,并用一次 ttl 兼判存在与续期时机(sessionStore.ts:7-1025-39)。
  • 遥测加密永不阻断主流程。 IP 加密失败回落明文而非抛错(encryption.ts:15-18)。

10. 边界与局限

  • 只做「查库文档」这一件事。 instructions 明确排除重构、写脚本、调业务逻辑、代码审查(index.ts:130)。
  • 「每问≤3 次」不强制。 纯靠描述说服模型,服务器无计数器,恶意/失控客户端可无视。
  • HTTP 不支持 GET/SSE 主动推送。 刻意 405(index.ts:391-397),没有服务器主动通知能力。
  • 会话不是安全边界。 只做日志关联与规范合规,鉴权靠 /mcp/oauth 的 JWT(sessionStore.ts:7-10)。
  • 默认加密密钥是公开常量。 不设 CLIENT_IP_ENCRYPTION_KEY 时用写死的 key(encryption.ts:5),自托管须自行覆盖。

11. 横向对比

  • 同项目其它章:后端契约见 01;CLI 与一键接入/OAuth 见 03;跨端共享的提示词与 AI-SDK 工具见 04;分发形态(Skills/Plugins/Rules)见 05;全景与阅读地图见 index
  • 本章相对同货架其它 agent 项目的取舍:Context7 是极窄面的 MCP——只两个只读工具、不做任何写操作,把复杂度全压在「传输 + 上下文隔离 + 提示词」上,而非工具数量上。

12. 代码地图(导航索引)

主题文件路径符号名
创建 server + 注册两工具packages/mcp/src/index.tscreateMcpServer
服务器整体用法提示packages/mcp/src/index.ts:127instructions(McpServer 第二参数)
工具1:解析库 IDpackages/mcp/src/index.ts:134registerTool("resolve-library-id", …)
工具2:查文档packages/mcp/src/index.ts:221registerTool("query-docs", …)
CLI 解析与 flag 互斥packages/mcp/src/index.ts:36program / allowedTransports
启动分叉 stdio/httppackages/mcp/src/index.ts:330main
HTTP 请求处理 + 会话生命周期packages/mcp/src/index.ts:383handleMcpRequest
每请求上下文隔离packages/mcp/src/index.ts:84requestContext(AsyncLocalStorage)
取当前调用者上下文packages/mcp/src/index.ts:95getClientContext
参数名纠偏表packages/mcp/src/index.ts:281GLOBAL_ALIASES / TOOL_ALIASES
纠偏改写 + 挂钩子packages/mcp/src/index.ts:294applyAliases / installTransportArgAliasing
后端调用:搜索packages/mcp/src/lib/api.ts:110searchLibraries
后端调用:取文档packages/mcp/src/lib/api.ts:144fetchLibraryContext
错误文案(401/404/429)packages/mcp/src/lib/api.ts:15parseErrorResponse
登录信号读取packages/mcp/src/lib/api.ts:97readPromptSignal
IP 加密 + 遥测头packages/mcp/src/lib/encryption.ts:36generateHeaders / encryptClientIp
JWT 校验(Clerk/Entra/EMA)packages/mcp/src/lib/jwt.ts:74validateJWT / isJWT
会话存储(Redis, fail-open)packages/mcp/src/lib/sessionStore.ts:11createSessionStore
客户端 IP 提取packages/mcp/src/lib/client-ip.ts:64getClientIp / isPrivateOrLocalIp
结果格式化 + UA 解析packages/mcp/src/lib/utils.ts:66formatSearchResults / extractClientInfoFromUserAgent
匿名用户登录引导packages/mcp/src/lib/auth/auth-prompt.ts:66maybeElicitAuthSignIn
常量与端点基址packages/mcp/src/lib/constants.ts:14CONTEXT7_API_BASE_URL / RESOURCE_URL