Skip to main content
MCP(Model Context Protocol)是 AI Agent 调用外部工具的开放协议。通过 SDK,你可以定义 MCP Server、为 Agent 配置工具。连接管理、工具发现等运行时工作由底层 CLI 完成。

一图看懂

  • In-Process:工具就是一个 JS 函数,运行在你自己的进程里。McpServer 实例通过 SDK 的 control channel 与 CLI 通信,不会再起一个子进程。
  • External:你在配置里声明子进程或远端 URL,CLI 负责连接、发现、调用。

三种接入方式

三种方式可以混用——在同一个 query() 里同时注册多个不同类型的服务器。

In-Process Server(推荐)

In-process 工具是最直接的扩展方式:定义一个普通的 async 函数,加上 Zod schema,就能被 Agent 调用。

30 秒上手

tool() 完整签名

annotations 实际支持

下列三个字段会被 SDK 真正消费,并通过 mcpServerStatus().tools[i].annotations 回传到宿主侧:
注意宿主侧字段名是去掉 Hint 后缀的:readOnlyHintannotations.readOnly,依此类推。annotations 对象只包含被显式设置的字段。 ⚠️ 这三个字段不会影响 auto 模式的权限决策。CLI 把 server 自声明的 annotation 视为不可验证的提示信息(server 可以随意 under-/over-declare),不会把它们带进权限管线,以免变相替 server 的自我标榜背书。要硬性拒绝某些工具,请用 allowedTools 白名单或 hooks 拦截——annotation 仅用于宿主侧识别(mcpServerStatus)和 TUI 展示。
idempotentHinttitle 目前不支持——传了不会报错,但 SDK 不会消费、也不会回传给宿主。如果你的应用需要这些信息,请在宿主侧自行维护映射。

CallToolResult 结构

业务失败请用 isError: true 而不是抛异常——异常会终止整个 tool call,AI 拿不到信息;isError 让 AI 知道「这个调用失败了,请换个办法」。

createSdkMcpServer() 完整签名

返回值形如 { type: 'sdk', name, instance },直接塞进 options.mcpServers 即可。
⚠️ 不要复用同一个 server 配置跨多次 query():每次 query 会绑定独立的 transport。重复使用没有副作用,但你也不会得到「跨 query 共享状态」的能力——共享状态请放在 handler 闭包外的模块作用域里。

多工具示例


Stdio Server

通过子进程的 stdin/stdout 与 MCP 服务器通信。NPM 上 @modelcontextprotocol/server-* 系列都是 stdio 实现。

SSE / HTTP Server

需要 OAuth 的远端服务请看 OAuth 认证

工具命名与白名单

CLI 在向模型暴露 MCP 工具时统一加前缀:
例如服务器名 my_tools、工具名 greet,模型看到的工具名是 mcp__my_tools__greet

tools:限制模型可见的工具集合

想让模型只看到部分工具,用 tools。CLI 会把所有未列出的内置工具加进 disallow 列表,等于”白名单”语义:
⚠️ 不传 tools 等于全部放开:所有内置工具 + 所有已连接 MCP server 的工具都会暴露给模型。生产环境建议显式列出,按需收口。

allowedTools:预授权(不是可见性白名单)

allowedTools 把列出的工具加入”自动放行”规则——调用时跳过权限弹窗,但不会把没列出来的工具藏起来。常用于让低风险的 MCP 工具免审批:
不传 allowedTools 仅意味着没有预授权规则——模型仍能看到/调用所有工具,只是写操作会按 permissionMode 走审批流程。完整语义详见 Permissions 文档

allowedMcpServerNames:进程类 server 白名单

只过滤进程类(stdio/sse/http)服务器,不影响 in-process 服务器。配合 strictMcpConfig: true 可以拒绝 CLI 加载本地额外配置:
⚠️ 不传 allowedMcpServerNames 等于全部放开:所有声明的进程类 server 都会连接;想收口必须显式列出。in-process server 始终不受此字段影响。

运行时管理(Query API)

query() 返回的 Query 对象上挂了几把 MCP 钥匙。所有方法都通过 control channel 与 CLI 通信,行为是异步且幂等的。
⚠️ 缓存原则:MCP server 配置 / 鉴权状态变更会重建 tools 列表,会话中途变更会破坏 prompt prefix 缓存。SDK 提供”查询状态 + 首条消息前完成鉴权”的方法;server 集合本身请通过 options.mcpServers 在启动时一次性配置,必要时重启 query()

查询状态

💡 MCP 握手发生在 CLI 完成 initialize 之后、第一次用户消息之前。在 initializationResult() 已经返回之后再去查 status 才能拿到真实结果——握手 IO 可能要几百毫秒,建议用 pollUntil 等到 connected 再用。

订阅状态变化

MCP 状态采用拉取而非推送:调用 await q.mcpServerStatus() 即可。需要轮询时在自己代码里做。

改 server 集合?请走进程级配置

为保证 prompt prefix 缓存稳定,server 集合的变更在启动时一次完成:

控制请求超时

超时后 SDK 会自动写一条 control_cancel_request,并 reject 当前 Promise。

OAuth 认证

远端 MCP 服务器(HTTP/SSE)经常需要 OAuth。CLI 内置完整的 OAuth 2.0 + PKCE + Dynamic Client Registration(RFC 7591)实现。
⚠️ 缓存原则:OAuth 完成后 CLI 会重连 server、重新发现 tools,会话中途完成鉴权必然破坏 prompt prefix 缓存。所以只支持”主动驱动”模式鉴权——在首次 streamInput 之前完成所有鉴权,tools 列表稳定下来后再发首条用户消息。
💡 本节只覆盖 CLI 主导的 OAuth:CLI 自己做 metadata discovery、PKCE、token 交换、token 持久化。还有另一条服务器主导的鉴权链路——server 用 MCP elicitation/create 让 client 跳转去某个 URL 完成授权(典型例子:GitHub MCP)。两条链路独立,不会同时触发:服务器主导时 mcpServerStatus() 不会标 needs-authmcpAuthenticate 不该调,host 改用 onElicitation 接住请求。详见 Elicitation:服务器请求用户输入
宿主自己控制 OAuth 时机,在发首条用户消息之前完成:
redirectUri 可选,覆盖默认 OAuth 回调目标(Electron 自定义协议、企业内网回调地址等)。 CLI 默认把 token 存到系统 Keychain(macOS / Linux Secret Service),回退到 ~/.qoder/mcp-oauth-tokens.json(0o600 权限 + 跨进程锁)。

Elicitation:服务器请求用户输入

MCP elicitation/createserver → client 方向的请求,用来让 client 在用户面前展示一段交互。SDK 把这种请求通过 Options.onElicitation 暴露给宿主。

两种模式

URL 模式异步完成:server 在自己的回调里收到用户授权后,会发 notifications/elicitation/complete — SDK 投影为 SDKElicitationCompleteMessage 推到 Query 的消息流里。

回调签名

signalq.close() / 中断时会 abort,长流程要检查。

form 模式示例

url 模式示例(配合 elicitation_complete)

💡 不要在 onElicitation 里 await 浏览器回跳。URL 模式的设计是:回调立刻 accept(=用户已开始流程),CLI 不阻塞 control 通道;真正”完成”信号来自后续的 elicitation_complete 消息。如果你 await 整个 OAuth 跳转,会触发 control 请求超时(controlRequestTimeoutMs)。

与 OAuth 链路的边界

  • CLI 主导 OAuth(mcpAuthenticate / mcpSubmitOAuthCallbackUrl):token 落 qodercli Keychain;mcpServerStatus()needs-auth 时驱动;不触发 onElicitation
  • 服务器主导 elicit URL:token 在 server 内部;mcpServerStatus() 不会标 needs-auth;onElicitation 接住、靠 system/elicitation_complete 收尾。
两条链路不互斥但也不重叠:同一个 server 通常只走其中一条。不知道某个 server 走哪条时:看它是否在握手时给客户端发 elicitation/create 即可——发了就是服务器主导。

Hook 通道

宿主同样可以在 settings.json 里挂 hooks 拦截 elicitation,行为优先于 onElicitation:
⚠️ qodercli 0.2.x 在 MCP capability 声明里只送 elicitation: {}(空对象,兼容 Spring AI Java MCP SDK)。MCP SDK 服务端会把这个等价解释为 { form: {} },因此目前只有 form 模式真正能从远端 server 抵达 client。URL 模式协议层完整,但需要 CLI 显式声明 elicitation.url 才能让 server 端的 elicitInput({ mode: 'url' }) 通过校验——后续随 CLI 版本演进。

Options 速查

Query 上的方法

server 集合的增删改请通过 options.mcpServers(启动时配置)+ 重启 query() 完成,详见 改 server 集合?请走进程级配置

类型参考

McpServerStatus 的 status 枚举:

最佳实践

  1. 描述写给 AI 看tool()description 决定 AI 何时选用它。说清楚「做什么、什么时候用、不该用于什么」。
  2. 字段 .describe():Zod 字段一定要带 .describe(...),AI 用这些信息构造调用参数。
  3. 失败用 isError,不抛异常:让 AI 看见结果。异常会让模型一脸懵,且可能触发重试。
  4. 优先只读 + readOnlyHint:写操作要谨慎,搭配 canUseTool 或 hooks 二次确认。
  5. 服务器名简短:会出现在工具前缀里,太长的名字浪费 token。
  6. In-process 共享状态放模块作用域:handler 是闭包,但每次 query 仍会 reuse 同一个 server 实例。
  7. OAuth 在首次 streamInput 前完成:用 mcpAuthenticate + mcpSubmitOAuthCallbackUrl。会话中途完成鉴权必然破坏 prompt prefix 缓存。
  8. MCP 状态用 mcpServerStatus():push 通道已下线;按需轮询即可。
  9. 设置合理的 controlRequestTimeoutMs:远端服务器握手可能上秒,默认 60s 通常够,CI 环境记得显式给。
  10. strictMcpConfig 用于隔离:避免用户本地的 settings.json / .mcp.json 里声明的 MCP 服务器干扰你的应用。

完整示例