一图看懂
- In-Process:工具就是一个 Python async 函数,运行在你自己的进程里。
create_sdk_mcp_server产物通过 SDK 的 control channel 与 CLI 通信,不会再起一个子进程。 - External:你在配置里声明子进程或远端 URL,CLI 负责连接、发现、调用。
三种接入方式
三种方式可以混用——在同一个
query() / QoderSDKClient 里同时注册多个不同类型的服务器。
💡mcp_servers也可以传str/pathlib.Path:指向一个 JSON 配置文件路径,SDK 会以--mcp-config <path>透传给 CLI。
In-Process Server(推荐)
In-process 工具是最直接的扩展方式:定义一个普通的async 函数,用装饰器声明 schema,就能被 Agent 调用。详细的 @tool() / schema / handler 行为见 tools.md,本节只覆盖与 MCP server 装配相关的部分。
30 秒上手
@tool() / create_sdk_mcp_server() 完整签名
返回值
McpSdkServerConfig 形如 {"type": "sdk", "name": ..., "instance": ...},直接塞进 options.mcp_servers 即可。
annotations 实际支持
下列三个字段会被 SDK 真正消费,并通过get_mcp_status().mcpServers[i].tools[i].annotations 回传到宿主侧:
注意宿主侧字段名是去掉Hint后缀的:readOnlyHint→annotations.readOnly,依此类推。annotations对象只包含被显式设置的字段。 ⚠️ 这三个字段不会影响 auto 模式的权限决策。CLI 把 server 自声明的 annotation 视为不可验证的提示信息(server 可以随意 under-/over-declare),不会把它们带进权限管线,以免变相替 server 的自我标榜背书。要硬性拒绝某些工具,请用allowed_tools白名单或 hooks 拦截——annotation 仅用于宿主侧识别(get_mcp_status)和 TUI 展示。
idempotentHint 和 title 目前不被 SDK 消费——传了不会报错,但既不影响 CLI 行为、也不会出现在 get_mcp_status() 的回传里。如果你的应用需要这些信息,请在宿主侧自行维护映射。
💡 关于maxResultSizeChars:Python SDK 通过ToolAnnotations(maxResultSizeChars=...)把anthropic/maxResultSizeChars写到工具的_meta,CLI 据此放宽默认 50K 的返回长度限制。该字段是 Python 端的增量能力(TS 通过同名 annotation 暴露,wire 一致)。
handler 返回值
is_error: True 而不是抛异常。完整的 content 类型说明、Python 端与 TS 端的几处行为差异(resource_link 降级为文本、顶层 _meta 不透传、binary embedded resource 被 skip)见 Tools Reference - CallToolResult。
Handler 取消信号
handler 可以选择接收第二个参数ToolInvocationContext,在 CLI 取消当前调用时通过 extra.signal 协作退出:
⚠️ 不要复用同一个 server 配置跨多次 query():每次 query 会绑定独立的 transport。重复使用没有副作用,但你也不会得到「跨 query 共享状态」的能力——共享状态请放在 handler 闭包外的模块作用域里。
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 的工具都会暴露给模型。生产环境建议显式列出,按需收口。
allowed_tools:预授权(不是可见性白名单)
allowed_tools 把列出的工具加入”自动放行”规则——调用时跳过权限弹窗,但不会把没列出来的工具藏起来。常用于让低风险的 MCP 工具免审批:
allowed_tools 仅意味着没有预授权规则——模型仍能看到/调用所有工具,只是写操作会按 permission_mode 走审批流程。完整语义详见 Permissions 文档。
allowed_mcp_server_names:进程类 server 白名单
只过滤进程类(stdio/sse/http)服务器,不影响 in-process server。配合 strict_mcp_config=True 可以拒绝 CLI 加载本地额外配置:
⚠️ 不传 allowed_mcp_server_names 等于全部放开:所有声明的进程类 server 都会连接;想收口必须显式列出。in-process server 始终不受此字段影响。
运行时管理(QoderSDKClient)
query() 是一次性的迭代器,无法在中途变更 server 或鉴权。运行时管理 MCP 必须使用 QoderSDKClient,它把状态查询、OAuth、增删 server、reconnect / toggle 等都暴露为公开方法。
⚠️ 缓存原则:MCP server 配置 / 鉴权状态变更会重建 tools 列表,会话中途变更会破坏 prompt prefix 缓存。SDK 提供「查询状态 + 首条消息前完成鉴权」的方法;server 集合本身请通过options.mcp_servers在启动时一次性配置,必要时新建一个QoderSDKClient。
查询状态
💡 MCP 握手发生在 CLI 完成initialize之后、第一次用户消息之前。QoderSDKClient.connect()已经等到 initialize 返回;握手 IO 可能要几百毫秒,必要时自己轮询get_mcp_status()直到connected。
订阅状态变化
两种方式任选其一: 方式 1(推荐):在 options 上挂on_mcp_status_change 回调,每次状态变化都会被调用一次。
system/mcp_status_change。回调和消息流是同一份 payload,回调只是免去过滤的便利。
运行时增删 server / 重连 / 启停
⚠️ 这三个方法都会触发 tools 列表重建,因此都会破坏 prompt prefix 缓存。生产环境优先在启动时配齐 mcp_servers,把这些 API 留给调试和本地开发场景。
控制请求超时
control_cancel_request,并 reject 当前 Future。
OAuth 认证
远端 MCP 服务器(HTTP/SSE)经常需要 OAuth。CLI 内置完整的 OAuth 2.0 + PKCE + Dynamic Client Registration(RFC 7591)实现。Python SDK 暴露 inbound(CLI 主动让宿主完成 OAuth) 与 outbound(宿主主动触发 OAuth) 两条路径,可按宿主形态选择。⚠️ 缓存原则:OAuth 完成后 CLI 会重连 server、重新发现 tools,会话中途完成鉴权必然破坏 prompt prefix 缓存。建议在首次用户消息发出之前完成鉴权,tools 列表稳定下来后再开聊。
💡 本节只覆盖 CLI 主导的 OAuth:CLI 自己做 metadata discovery、PKCE、token 交换、token 持久化。还有另一条服务器主导的鉴权链路——server 用 MCP elicitation/create 让 client 跳转去某个 URL 完成授权。两条链路独立,不会同时触发。详见 Elicitation:服务器请求用户输入。
Inbound:on_mcp_oauth_required 回调
CLI 在握手中检测到 server 需要 OAuth 时,会通过 control_request 把 McpOAuthRequest 推给 SDK,SDK 调用宿主的 on_mcp_oauth_required 回调。宿主返回以下任一种 resolution:
Outbound:宿主主动驱动鉴权
宿主自己 UI 有「Sign in」入口时,可以主动调用:redirect_uri 可选,覆盖默认 OAuth 回调目标(Electron 自定义协议、企业内网回调地址等)。
CLI 默认把 token 存到系统 Keychain(macOS / Linux Secret Service),回退到 ~/.qoder/mcp-oauth-tokens.json(权限 0o600 + 跨进程锁)。
Elicitation:服务器请求用户输入
MCPelicitation/create 是 server → client 方向的请求,用来让 client 在用户面前展示一段交互(form 模式收结构化输入;url 模式让用户去某个 URL 完成操作)。
✅ Python SDK 现已对齐 TS SDK:QoderAgentOptions.on_elicitation接收一个返回ElicitationResult的异步回调,签名与 TS 版本一致。未设置回调时,SDK 仍按默认契约自动答{"action": "cancel"}。Elicitation/ElicitationResult两类 hook 事件仍会并行触发,作为只读观察通道。
⚠️ 当前 CLI 不 advertiseelicitation.urlcapability。server 端elicit({mode: 'url'})会被 CLI 直接拒绝(MCP error -32602: Client does not support URL-mode elicitation requests),因此 URL 模式 elicit 不会到达 SDK,system/elicitation_complete通知在当前 CLI 上也不会触发。等 CLI 开启 URL capability 后该路径会自动接通。
用 on_elicitation 应答 elicit
- 字段名遵循 TS SDK 的 camelCase(
serverName / elicitationId / requestedSchema / displayName),CLI 的 snake_case payload 由 SDK 自动转换。 - 返回
None等价于{"action": "cancel"},方便宿主在 fallback 路径里直接放弃。 - 也可以返回
mcp.types.ElicitResultPydantic 模型(SDK 会model_dump)。
观察 elicitation(hook 通道)
on_elicitation 落地后,Elicitation / ElicitationResult hook 仍然会并行触发——它们是只读观察通道,不承担决策。
与 OAuth 链路的边界
- CLI 主导 OAuth(
mcp_authenticate/inject_mcp_token/on_mcp_oauth_required):token 落 qodercli Keychain;get_mcp_status()在needs-auth时驱动;不触发 Elicitation hook。 - 服务器主导 elicit:token 在 server 内部;
get_mcp_status()不会标needs-auth;由on_elicitation回调决策(未注册时 SDK 自动 cancel)。
Options 速查
QoderSDKClient 上的方法
类型参考
McpServerStatus.status 枚举(McpServerConnectionStatus):
最佳实践
- 描述写给 AI 看:
@tool的description决定 AI 何时选用它。说清楚「做什么、什么时候用、不该用于什么」。 - 参数加
Annotated:在简单 dict / TypedDict 中给字段写Annotated[type, "..."],AI 用这些信息构造调用参数。 - 失败用
is_error: True,不抛异常:让 AI 看见结果。完整对比见 工具使用指南 - SDK 如何处理 tool 返回的错误。 - 优先只读 +
readOnlyHint:写操作要谨慎,搭配can_use_tool或 hooks 二次确认。 - server 名简短:会出现在工具前缀里,太长的名字浪费 token。
- In-process 共享状态放模块作用域:handler 是闭包,但每次 query 仍会 reuse 同一个 server 实例。
- OAuth 在首条 user message 前完成:用
mcp_authenticate+mcp_submit_oauth_callback_url,或on_mcp_oauth_requiredinbound 回调,或inject_mcp_token。会话中途完成鉴权必然破坏 prompt prefix 缓存。 - MCP 状态走
get_mcp_status()或on_mcp_status_change:push 通道(status change message)保留,按需选一种即可。 - 设置合理的
control_request_timeout_ms:远端 server 握手可能上秒,默认 60s 通常够;OAuth 等待用户操作时要调大;CI 环境记得显式给。 strict_mcp_config用于隔离:避免用户本地的~/.qoder/settings.json/.mcp.json里声明的 MCP server 干扰你的应用。