MCP(Model Context Protocol)是 AI Agent 调用外部工具的开放协议。通过 SDK,你可以定义 MCP Server、为 Agent 配置工具。连接管理、工具发现、OAuth、状态同步等运行时工作由底层 CLI 完成。
三种方式可以混用——在同一个会话里同时注册多个不同类型的服务器。
In-process 工具是最直接的扩展方式:定义一个普通的 async 函数,加上 schema 声明,就能被 Agent 调用。详细的工具创建 / schema / handler 行为见 Tools,本节只覆盖与 MCP server 装配相关的部分。
返回值形如
下列三个字段会被 SDK 真正消费,并通过 MCP 状态查询(TypeScript 的
业务失败请用错误标记(
Python handler 可以选择接收第二个参数
通过子进程的 stdin/stdout 与 MCP 服务器通信。NPM 上
远端 URL 不可达时同样不会让 query 挂掉,server 状态非
CLI 在向模型暴露 MCP 工具时统一加前缀:
例如服务器名
想让模型只看到部分工具,用
不传预授权列表仅意味着没有预授权规则——模型仍能看到/调用所有工具,只是写操作会按权限模式走审批流程。完整语义详见 Permissions 文档。
TypeScript 中运行时管理通过
为保证 prompt prefix 缓存稳定,推荐把 server 集合的变更在启动时一次完成:
超时后 SDK 会自动写一条
远端 MCP 服务器(HTTP/SSE)经常需要 OAuth。CLI 内置完整的 OAuth 2.0 + PKCE + Dynamic Client Registration(RFC 7591)实现。
宿主自己控制 OAuth 时机,在发首条用户消息之前完成:
Python SDK 还支持 inbound 路径:CLI 在握手中检测到 server 需要 OAuth 时,会通过 control_request 把
MCP
URL 模式异步完成:server 在自己的回调里收到用户授权后,会发
Python 注意事项:
宿主同样可以挂 hooks 观察 / 拦截 elicitation:
一图看懂
- In-Process:工具就是一个普通的 async 函数(JS / Python),运行在你自己的进程里。server 实例通过 SDK 的 control channel 与 CLI 通信,不会再起一个子进程。
- External:你在配置里声明子进程或远端 URL,CLI 负责连接、发现、调用。
三种接入方式
| 方式 | 配置项 type | 进程边界 | 适用场景 |
|---|---|---|---|
| In-Process | 'sdk'(由 createSdkMcpServer / create_sdk_mcp_server 创建) | 同进程 | 自定义业务工具,需要直接访问 host 状态 |
| Stdio | 'stdio'(可省略) | 子进程 | 已有 MCP 工具包(@modelcontextprotocol/server-*) |
| SSE / HTTP | 'sse' / 'http' | 远程 | 远端服务、SaaS 工具、需要 OAuth 的服务 |
💡 Python 中mcp_servers也可以传str/pathlib.Path:指向一个 JSON 配置文件路径,SDK 会以--mcp-config <path>透传给 CLI。
In-Process Server(推荐)
In-process 工具是最直接的扩展方式:定义一个普通的 async 函数,加上 schema 声明,就能被 Agent 调用。详细的工具创建 / schema / handler 行为见 Tools,本节只覆盖与 MCP server 装配相关的部分。
30 秒上手
完整签名
| 参数 | 说明 |
|---|---|
name(tool) | 工具名,全限定名将是 mcp__<server>__<name> |
description | 给模型看的说明,决定 AI 何时调用——写清楚 What/When |
inputSchema / input_schema | TypeScript 传 Zod raw shape(不是 z.object(...));Python 支持简单 dict / TypedDict / 完整 JSON Schema dict |
handler | 实际逻辑,返回 CallToolResult |
annotations | MCP 工具注解,详见下表 |
name(server) | server 名(决定工具前缀 mcp__<name>__) |
version | 默认 '1.0.0' |
tools | 工具列表 |
{ type: 'sdk', name, instance },直接塞进 MCP servers 配置即可。
⚠️ 不要复用同一个 server 配置跨多次 query():每次 query 会绑定独立的 transport。重复使用没有副作用,但你也不会得到「跨 query 共享状态」的能力——共享状态请放在 handler 闭包外的模块作用域里。
annotations 实际支持
下列三个字段会被 SDK 真正消费,并通过 MCP 状态查询(TypeScript 的 mcpServerStatus().tools[i].annotations、Python 的 get_mcp_status().mcpServers[i].tools[i].annotations)回传到宿主侧:
| 字段 | 作用 | 宿主侧读法 |
|---|---|---|
readOnlyHint | 声明工具是只读的。只读工具可以并发执行(同批次内不互相阻塞);TUI 工具详情会渲染 [read-only] 徽章 | annotations.readOnly |
destructiveHint | 声明工具会执行破坏性操作。TUI 工具详情会渲染 [destructive] 徽章 | annotations.destructive |
openWorldHint | 声明工具会触达外部世界(如联网搜索、调用第三方 API)。TUI 工具详情会渲染 [open-world] 徽章 | annotations.openWorld |
注意宿主侧字段名是去掉Hint后缀的:readOnlyHint→annotations.readOnly,依此类推。annotations对象只包含被显式设置的字段。 ⚠️ 这三个字段不会影响 auto 模式的权限决策。CLI 把 server 自声明的 annotation 视为不可验证的提示信息(server 可以随意 under-/over-declare),不会把它们带进权限管线,以免变相替 server 的自我标榜背书。要硬性拒绝某些工具,请用工具白名单或 hooks 拦截——annotation 仅用于宿主侧识别和 TUI 展示。
idempotentHint 和 title 目前不被 SDK 消费——传了不会报错,但 SDK 不会消费、也不会回传给宿主。如果你的应用需要这些信息,请在宿主侧自行维护映射。
💡 关于maxResultSizeChars:Python SDK 通过ToolAnnotations(maxResultSizeChars=...)把anthropic/maxResultSizeChars写到工具的_meta,CLI 据此放宽默认 50K 的返回长度限制(TS 通过同名 annotation 暴露,wire 一致)。
CallToolResult 结构
isError: true / is_error: True) 而不是抛异常——异常会终止整个 tool call,AI 拿不到信息;错误标记让 AI 知道「这个调用失败了,请换个办法」。Python 端与 TS 端的几处行为差异(resource_link 降级为文本、顶层 _meta 不透传等)见 Tools。
Handler 取消信号(Python)
Python handler 可以选择接收第二个参数 ToolInvocationContext,在 CLI 取消当前调用时通过 extra.signal 协作退出:
Stdio Server
通过子进程的 stdin/stdout 与 MCP 服务器通信。NPM 上 @modelcontextprotocol/server-* 系列都是 stdio 实现。
command 不可达或启动失败时不会拖垮整个 query——对应 server 的 status 会保持非 'connected',其它 server 不受影响。
SSE / HTTP Server
'connected',其它 server 不受影响。需要 OAuth 的远端服务请看 OAuth 认证。
工具命名与白名单
CLI 在向模型暴露 MCP 工具时统一加前缀:
my_tools、工具名 greet,模型看到的工具名是 mcp__my_tools__greet。Server 名允许含连字符等特殊字符(my-tools → mcp__my-tools__<tool>)。
tools:限制模型可见的工具集合
想让模型只看到部分工具,用 tools。CLI 会把所有未列出的内置工具加进 disallow 列表,等于"白名单"语义:
⚠️ 不传 tools 等于全部放开:所有内置工具 + 所有已连接 MCP server 的工具都会暴露给模型。生产环境建议显式列出,按需收口。
预授权列表(不是可见性白名单)
allowedTools / allowed_tools 把列出的工具加入"自动放行"规则——调用时跳过权限弹窗,但不会把没列出来的工具藏起来。常用于让低风险的 MCP 工具免审批:
进程类 server 白名单
allowedMcpServerNames / allowed_mcp_server_names 只过滤进程类(stdio/sse/http)服务器,不影响 in-process 服务器。配合 strictMcpConfig: true / strict_mcp_config=True 可以拒绝 CLI 加载本地额外配置:
⚠️ 不传该白名单等于全部放开:所有声明的进程类 server 都会连接;想收口必须显式列出。in-process server 始终不受此字段影响。
运行时管理
TypeScript 中运行时管理通过 query() 返回的 Query 对象;Python 中一次性 query() 迭代器无法在中途变更 server 或鉴权,必须使用 QoderSDKClient。所有方法都通过 control channel 与 CLI 通信,行为是异步且幂等的。
⚠️ 缓存原则:MCP server 配置 / 鉴权状态变更会重建 tools 列表,会话中途变更会破坏 prompt prefix 缓存。SDK 提供"查询状态 + 首条消息前完成鉴权"的方法;server 集合本身请通过 options 在启动时一次性配置,必要时重启会话。
查询状态
💡 MCP 握手发生在 CLI 完成initialize之后、第一次用户消息之前。在初始化结果返回之后再去查 status 才能拿到真实结果——握手 IO 可能要几百毫秒,建议轮询直到connected再用。
订阅状态变化
- TypeScript:MCP 状态采用拉取而非推送——调用
await q.mcpServerStatus(),需要轮询时在自己代码里做。 - Python:除拉取外,还可以在 options 上挂
on_mcp_status_change回调,每次状态变化都会被调用一次;或消费消息流过滤system/mcp_status_change。回调和消息流是同一份 payload。
变更 server 集合
为保证 prompt prefix 缓存稳定,推荐把 server 集合的变更在启动时一次完成:
| 你想做的事 | TypeScript | Python |
|---|---|---|
| 增加 / 删除 / 替换 servers | 在 options.mcpServers 里配置;需要变更集合时重启 query() | 启动时配置 mcp_servers;运行时可用 client.set_mcp_servers(servers) 全量替换(返回 {added, removed, errors}) |
| 仅启用部分进程类 server | allowedMcpServerNames 白名单 | allowed_mcp_server_names 白名单 |
| 重连某个 server | 重启 query() | client.reconnect_mcp_server(name),常用于从 'failed' 状态恢复 |
| 启用 / 禁用某个 server | 重启 query() | client.toggle_mcp_server(name, enabled);禁用会断开连接并下线其工具 |
| 退出某个 server 的登录 | 重启 query() 时不带该 token | client.mcp_clear_auth(name) |
⚠️ Python 的这几个运行时方法都会触发 tools 列表重建,因此都会破坏 prompt prefix 缓存。生产环境优先在启动时配齐,把这些 API 留给调试和本地开发场景。
控制请求超时
control_cancel_request,并 reject 当前请求。
OAuth 认证
远端 MCP 服务器(HTTP/SSE)经常需要 OAuth。CLI 内置完整的 OAuth 2.0 + PKCE + Dynamic Client Registration(RFC 7591)实现。
⚠️ 缓存原则:OAuth 完成后 CLI 会重连 server、重新发现 tools,会话中途完成鉴权必然破坏 prompt prefix 缓存。建议在首条用户消息发出之前完成鉴权,tools 列表稳定下来后再开聊。
💡 本节只覆盖 CLI 主导的 OAuth:CLI 自己做 metadata discovery、PKCE、token 交换、token 持久化。还有另一条服务器主导的鉴权链路——server 用 MCP elicitation/create 让 client 跳转去某个 URL 完成授权(典型例子:GitHub MCP)。两条链路独立,不会同时触发。详见 Elicitation:服务器请求用户输入。
宿主主动驱动鉴权(outbound)
宿主自己控制 OAuth 时机,在发首条用户消息之前完成:
| 方法(TypeScript / Python) | 用途 | 调用时机 |
|---|---|---|
mcpAuthenticate(name, redirectUri?) / mcp_authenticate(name, redirect_uri=None) | 拉起 OAuth;返回 { authUrl?, requiresUserAction }。静默续期成功时 requiresUserAction: false,无需 UI | 首条用户消息前 |
mcpSubmitOAuthCallbackUrl(name, url) / mcp_submit_oauth_callback_url(name, callback_url) | 提交完整回调 URL(含 code/state) | 首条用户消息前 |
inject_mcp_token(name, token)(仅 Python) | 宿主自己跑完整个 OAuth,把 OAuthToken 直接塞回 CLI | 首条用户消息前 |
mcp_clear_auth(name)(仅 Python) | 删除 CLI 存的 OAuth 凭据,相当于「sign out」 | 任意时刻;下次工具调用会触发重新鉴权 |
redirectUri / redirect_uri 可选,覆盖默认 OAuth 回调目标(Electron 自定义协议、企业内网回调地址等)。
CLI 默认把 token 存到系统 Keychain(macOS / Linux Secret Service),回退到 ~/.qoder-cn/mcp-oauth-tokens.json(0o600 权限 + 跨进程锁)。
Inbound:on_mcp_oauth_required 回调(Python)
Python SDK 还支持 inbound 路径:CLI 在握手中检测到 server 需要 OAuth 时,会通过 control_request 把 McpOAuthRequest 推给 SDK,SDK 调用宿主的 on_mcp_oauth_required 回调。宿主返回以下任一种 resolution:
| 返回类型 | 含义 |
|---|---|
OAuthToken 或 {"token": OAuthToken} | 宿主自己跑完整个 OAuth 流程,直接给 CLI 注入 token |
{"callbackUrl": "..."} | 宿主只拿到完整的回调 URL(含 code / state),CLI 解析并换 token |
{"code": "...", "state": "..."} | 宿主自己解出了 code,直接交还 CLI |
None | 拒绝,CLI 把该 server 标为 failed |
Elicitation:服务器请求用户输入
MCP elicitation/create 是 server → client 方向的请求,用来让 client 在用户面前展示一段交互。SDK 把这种请求通过 onElicitation(TypeScript)/ on_elicitation(Python)暴露给宿主。
两种模式
| 模式 | 触发场景 | 典型用途 |
|---|---|---|
'form' | 服务器要一段结构化输入,请求带 requestedSchema(MCP 受限子集的 JSON Schema) | API key 录入、配置项填写、二次确认 |
'url' | 服务器让用户去某个 URL 完成操作,请求带 url + elicitationId | 服务器自带 OAuth、设备码激活、账号关联 |
notifications/elicitation/complete——SDK 投影为 elicitation complete 消息推到消息流里。
⚠️ qoderclicn 当前在 MCP capability 声明里只送elicitation: {}(等价于{ form: {} }),因此目前只有 form 模式真正能从远端 server 抵达 client。URL 模式协议层完整,但需要 CLI 显式声明elicitation.urlcapability——后续随 CLI 版本演进,届时该路径会自动接通。
回调签名
- 字段名遵循 TS SDK 的 camelCase(
serverName / elicitationId / requestedSchema / displayName),CLI 的 snake_case payload 由 SDK 自动转换。 - 返回
None等价于{"action": "cancel"};未设置回调时,SDK 按默认契约自动答 cancel。 - 也可以返回
mcp.types.ElicitResultPydantic 模型(SDK 会model_dump)。
signal 在 q.close() / 中断时会 abort,长流程要检查。
form 模式示例
url 模式示例(配合 elicitation_complete)
💡 不要在 elicitation 回调里 await 浏览器回跳。URL 模式的设计是:回调立刻accept(=用户已开始流程),CLI 不阻塞 control 通道;真正"完成"信号来自后续的elicitation_complete消息。如果你 await 整个 OAuth 跳转,会触发 control 请求超时。
与 OAuth 链路的边界
- CLI 主导 OAuth(
mcpAuthenticate/mcp_authenticate等):token 落 qoderclicn Keychain;MCP 状态在needs-auth时驱动;不触发 elicitation 回调。 - 服务器主导 elicit URL:token 在 server 内部;MCP 状态不会标
needs-auth;靠 elicitation 回调接住、靠elicitation_complete消息收尾。
elicitation/create 即可——发了就是服务器主导。
Hook 通道
宿主同样可以挂 hooks 观察 / 拦截 elicitation:
| Hook 事件 | 时机 | 说明 |
|---|---|---|
Elicitation | server 请求到达时 | TypeScript 中行为优先于 onElicitation,可自动 accept / decline / cancel(短路 UI)或放行;Python 中是只读观察通道,决策走 on_elicitation |
ElicitationResult | 用户响应之后 | TypeScript 中可改写 action / content 或 block;Python 中只读观察 |
Notification(type=elicitation_complete) | URL 模式完成通知到达时 | 触发 IDE / 系统通知 |
Options 速查
| 字段(TypeScript / Python) | 默认 | 说明 |
|---|---|---|
mcpServers / mcp_servers | – | 服务器名 → 配置;Python 还可传 JSON 配置文件路径 |
allowedMcpServerNames / allowed_mcp_server_names | – | 进程类服务器白名单(不影响 in-process);不传等于全部放开 |
strictMcpConfig / strict_mcp_config | false | 禁止 CLI 从用户配置文件再加载额外 MCP |
tools / tools | – | 模型可见工具白名单;不传等于全部内置 + MCP 工具都可见 |
allowedTools / allowed_tools | – | 预授权列表(跳过权限弹窗,不控制可见性);不传等于无预授权规则 |
disallowedTools / disallowed_tools | – | 明确拒绝的工具,优先于 allow |
controlRequestTimeoutMs / control_request_timeout_ms | 60_000 | control 请求超时(含 mcp 系列),0 禁用 |
onElicitation / on_elicitation | – | MCP server 主动请求用户输入时触发(form / url 两种模式) |
on_mcp_oauth_required(仅 Python) | – | CLI 检测到 server 需要 OAuth 时触发 |
on_mcp_status_change(仅 Python) | – | 每次 server 状态变化时触发;等价于过滤 system/mcp_status_change 流 |
运行时方法速查
| 方法(TypeScript / Python) | 说明 | 调用时机 |
|---|---|---|
mcpServerStatus() / get_mcp_status() | 拿当前所有 MCP 服务器状态 | 任意时刻 |
mcpAuthenticate(...) / mcp_authenticate(...) | 主动启动 OAuth;返回 { authUrl?, requiresUserAction } | 首条用户消息前 |
mcpSubmitOAuthCallbackUrl(...) / mcp_submit_oauth_callback_url(...) | 提交 OAuth 回调 | 首条用户消息前 |
set_mcp_servers(servers)(仅 Python) | 全量替换 MCP server 配置;返回 {added, removed, errors} | 任意时刻(会破坏前缀缓存) |
reconnect_mcp_server(name)(仅 Python) | 重连指定 server | 任意时刻 |
toggle_mcp_server(name, enabled)(仅 Python) | 启用 / 禁用 server | 任意时刻 |
inject_mcp_token(name, token)(仅 Python) | 宿主自己跑完整 OAuth 后注入 token | 首条用户消息前 |
mcp_clear_auth(name)(仅 Python) | 删除已存的 OAuth 凭据 | 任意时刻 |
TypeScript 中 server 集合的增删改请通过options.mcpServers(启动时配置)+ 重启query()完成。
类型参考
McpServerStatus.status 枚举:
| 值 | 含义 |
|---|---|
'pending' | 已注册,未开始连接 |
'connecting' | 正在握手 |
'connected' | 已连接,工具可调用 |
'failed' | 连接失败(看 error 字段) |
'needs-auth' | 需要 OAuth,请走认证流程 |
'disabled' | 被禁用(CLI 内部配置或外部状态决定) |
最佳实践
- 描述写给 AI 看:工具的
description决定 AI 何时选用它。说清楚「做什么、什么时候用、不该用于什么」。 - 字段带描述:TypeScript 的 Zod 字段一定要带
.describe(...),Python 用Annotated[type, "..."],AI 用这些信息构造调用参数。 - 失败用错误标记,不抛异常:让 AI 看见结果。异常会让模型一脸懵,且可能触发重试。
- 优先只读 +
readOnlyHint:写操作要谨慎,搭配权限回调或 hooks 二次确认。 - 服务器名简短:会出现在工具前缀里,太长的名字浪费 token。
- In-process 共享状态放模块作用域:handler 是闭包,但每次 query 仍会 reuse 同一个 server 实例。
- OAuth 在首条用户消息前完成:会话中途完成鉴权必然破坏 prompt prefix 缓存。
- MCP 状态按需拉取:TypeScript 用
mcpServerStatus()轮询;Python 可选get_mcp_status()或on_mcp_status_change回调。 - 设置合理的 control 请求超时:远端服务器握手可能上秒,默认 60s 通常够;OAuth 等待用户操作时要调大;CI 环境记得显式给。
- strict MCP config 用于隔离:避免用户本地的
settings.json/.mcp.json里声明的 MCP 服务器干扰你的应用。