在 Claude Agent SDK 里接 MCP:和 CLI 里配 MCP 是两套写法
一、先说这篇要解决的那个具体麻烦
你在命令行里已经把 MCP 服务器调通了:claude mcp add 加进去,/mcp 里查到的状态是 connected,Claude 也确实在调那几个工具。然后你要把这套流程搬进程序里,用 Claude Agent SDK 写一个跑批任务或者一个后端服务。代码跑起来,模型说它没有那些工具;或者第一轮就是没有,第二轮又莫名其妙有了。
这不是玄学。Claude Code 官方文档把 SDK 侧的 MCP 单独放了一页(code.claude.com/docs/en/agent-sdk/mcp),页首就有一条提示:这一页讲的是 Agent SDK 的 MCP 配置,如果你要给 CLI 加服务器让它在每个项目里都加载,那要看 code.claude.com/docs/en/mcp 的 MCP installation scopes。两页讲的是两套入口。 命令行那套 --scope local / project / user 的概念,在 SDK 这边并不是你唯一要关心的东西。
这篇只落一件事:SDK 侧 MCP 服务器的声明位置,和它们各自的连接生命周期。 因为绝大多数「工具没出现」的情况,答案要么在声明位置上,要么在连接时机上。
二、前置条件
先把能核到的说清楚,核不到的也照实说。
- 包:TypeScript 侧是
@anthropic-ai/claude-agent-sdk,文档示例从它导入query;Python 侧是claude_agent_sdk,示例导入query、ClaudeAgentOptions、ResultMessage、SystemMessage、AssistantMessage。 - 版本:这一页没有写明使用 MCP 所需的最低 SDK 版本,官方文档没有说明这一点。涉及版本相关行为时,以官方文档最新内容为准。
- 权限:文档写得很直白——MCP 工具需要显式许可,否则「Claude 会看到工具可用,但没法调用它们」。所以配好服务器只是第一半,另一半是
allowedTools。 - stdio 类服务器的运行环境:文档的排查一节写明,用
npx拉起的服务器要确认包存在且 Node.js 在你的PATH里。这条在 Windows 上尤其值得先验一遍:宿主进程是在 PowerShell、cmd 还是 Git Bash 里启动的,会影响它继承到的PATH,而PATH里有没有 Node 正是文档点名要查的那一项。(后半句是通用运维经验,官方文档只写到「确认包存在且 Node.js 在 PATH 里」这一层,没有展开各 shell 的差异。) - 凭据:文档示例里的 shell 变量设置写的是 POSIX 形式,例如
export GITHUB_TOKEN=YOUR_GITHUB_PAT。这一页没有给 Windows 上 PowerShell 或 cmd 的等价写法,官方文档没有说明这一点;实际写代码时凭据是从进程环境读的(TypeScript 用process.env.API_TOKEN,Python 用os.environ["API_TOKEN"]),这部分是跨平台一致的,差异只在你怎么把变量塞进宿主进程。正文里请一律用<YOUR_API_KEY>这类占位,别把真 token 写进代码。
三、声明位置:两个,不是一个
位置一:调用 query() 时直接传 mcpServers
这是 SDK 的主入口。TypeScript:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "List files in my project",
options: {
mcpServers: {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
}
},
allowedTools: ["mcp__filesystem__*"]
}
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
Python 是同一份配置换个写法,键名从驼峰变成下划线:
options = ClaudeAgentOptions(
mcp_servers={
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/me/projects",
],
}
},
allowed_tools=["mcp__filesystem__*"],
)
代码块里的 /Users/me/projects 是官方文档示例中的占位路径,换成 <你的项目目录>。Windows 上这里要填 Windows 形式的路径,具体怎么转义取决于你用哪种字符串字面量,这一页没有给 Windows 路径的示例。
allowedTools 那一行不是可选装饰。文档给出的命名规则是 mcp__<server-name>__<tool-name>:一个叫 "github" 的服务器上的 list_issues 工具,全名就是 mcp__github__list_issues。通配符可以整台服务器放行:
allowedTools: [
"mcp__github__*", // All tools from the github server
"mcp__db__query", // Only the query tool from db server
"mcp__slack__send_message" // Only send_message from slack server
]
这里有一条容易踩反的规则,文档专门用一个 Note 写了:给 MCP 放行要用 allowedTools,别指望 permission mode。 permissionMode: "acceptEdits" 不会自动批准 MCP 工具,它只覆盖文件编辑和文件系统类的 Bash 命令;permissionMode: "bypassPermissions" 确实会自动批准 MCP 工具,但同时关掉了大部分其它安全提示,范围比你要的宽得多。一个通配符只放开你指定的那台服务器,不多给。
位置二:.mcp.json 配合 settingSources
项目根目录放一个 .mcp.json:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
}
}
}
文档写明:这个文件在 project 这个 setting source 启用时被读取,而默认的 query() 选项就是启用的;但如果你显式设置了 settingSources,就必须把 "project" 包含进去,否则这个文件不会加载。这是个很典型的「我明明什么都没改,只是加了一行 settingSources」型故障。
两个位置在行为上并不等价,下一节就是差在哪。
四、生命周期:谁会拖住第一轮
这是本篇的重点,也是 SDK 侧和 CLI 侧体感差最多的地方。
文档的说法是:Claude Code 在启动时注册你通过 options.mcpServers 传入的服务器,并在首轮等待(如果有的话)结束后发出 init 消息。而从设置文件(例如 .mcp.json)加载的服务器拿不到这个完整等待,在 init 时经常显示 pending。 你在两个位置写同一台服务器,看到的初始状态可能就不一样。
按服务器类型,官方文档列了三行:
| 服务器类型 | 是否拖慢第一轮 | 首轮等待超时 |
|---|---|---|
| stdio 服务器,或没有缓存工具列表的 HTTP/SSE 服务器 | 会,直到它连上 | 由 MCP_TIMEOUT 控制,到点连接失败 |
| 有缓存工具列表的远程服务器(Claude Code 从上次连接保存下来的) | 不会;缓存的工具从第一轮就可用 | 无;在首次工具调用时连接,那次延迟连接有自己的超时 |
| 进程内的 SDK MCP server | 不会;从不拖慢第一轮 | 无 |
三行读下来,结论很实际:你在代码里定义的进程内 SDK MCP server 永远不会拖住第一轮;而一台需要 npx 冷启动的 stdio 服务器会。(进程内自定义工具怎么写,官方另有 custom tools 一页在讲,这篇不展开。)
还有两个开关是控制更早那一段的——init 消息发出之前的启动阶段:
MCP_CONNECTION_NONBLOCKING设为0,会在整批连接上阻塞。这个等待有一个上限,上限值可以用MCP_CONNECT_TIMEOUT_MS环境变量调整,单位毫秒。到点还没连上的服务器会继续在后台连。- 在某台服务器的配置上设
alwaysLoad: true,它的工具会以完整 schema 在第一轮就可用,并豁免 tool search 的延迟加载。Claude Code 会在启动时等这台服务器的工具,等待同样受上面那个上限约束,其它服务器则继续在后台连。
顺带一句 tool search:文档说它默认开启,作用是把工具定义先扣着不放进上下文,每一轮只加载 Claude 需要的那些。所以「模型看不见工具」有时并不是没连上,而是它还没搜到——这跟 alwaysLoad 是同一件事的两面。
五、边界:哪些事 SDK 侧做不了
这一节别跳过,几个坑都在这。
OAuth 是半自动的。 文档明写:SDK 不会打开浏览器,也不会跑交互式 OAuth 流程。当一台配置好的服务器返回授权挑战、且没有已存 token 时,这次运行会在没有该服务器工具的情况下继续,服务器状态报 needs-auth——但 init 消息里的 mcp_servers 数组在发出时可能仍然显示 pending。要确认到底是不是缺凭据,得用 TypeScript 的 mcpServerStatus() 或 Python 的 get_mcp_status() 去轮询。想供凭据,就在你自己的应用里跑完 OAuth 流程,把拿到的 access token 塞进该服务器的 headers。
对比一下 CLI 侧:code.claude.com/docs/en/mcp 那边有 /mcp 面板的浏览器登录流程,也有 claude mcp login <name> 这种从 shell 直接跑 OAuth 的命令。这些在 SDK 这页里都没有对应物。同一件事,两边给的是两套手段。
SSE 已被标记为 deprecated。 SDK 这页的传输一节仍然给了 "type": "sse" 的示例,没有加提示;而 CLI 那页在「Add a remote SSE server」下面有一个明确的 Warning,写的是 SSE 传输已弃用,能用 HTTP 就用 HTTP。两页口径不同,这里照实标出来:新接的服务器优先用 HTTP。
"streamable-http" 这个别名,只在 JSON 里认。 文档写得很清楚:.mcp.json 和其它 JSON 配置文件里,"streamable-http" 被接受为 "http" 的别名;而程序化传入的 mcpServers 选项只接受 "http"。你从某个服务器的 README 里复制一段 JSON,粘到 .mcp.json 里能用,改写成 options.mcpServers 的对象字面量就不行——这条差异值得单独记一下。
WebSocket:CLI 那页有一节讲 type: "ws",Agent SDK 这页的传输类型一节里没有列出 WebSocket,官方文档没有说明 SDK 侧的对应写法。这一点我们不替它补。
连接失败不会抛异常。 文档在错误处理的代码注释里写明:连不上的 MCP 服务器不会 throw / raise,你必须自己去查状态;而且在 init 时仍是 pending 的服务器,需要之后再查一次状态。
输出上限。 一次工具结果过大时,SDK 应用的是与 Claude Code 相同的 MCP 输出限制:完整输出会被落盘成文件,工具结果被替换为一条点名了文件路径的错误信息,让 agent 分段读回来。上限由 MAX_MCP_OUTPUT_TOKENS 环境变量抬高。具体阈值随版本变动,这里不写数值,以官方文档为准。
六、怎么验证配对了
不用猜,SDK 每次查询开头都会发一条 system 消息、subtype 为 init,里面带每台 MCP 服务器的连接状态。文档列出的 status 取值一共五个:"pending"、"connected"、"failed"、"needs-auth"、"disabled"。
关键提醒也在文档里:别把 "pending" 当失败。 它覆盖两种情况——设置文件里的服务器没拿到完整等待,或者工具列表是从缓存里给的、连接推迟到首次调用。要检测真正不可用的,查 "failed" 和 "needs-auth":
if (message.type === "system" && message.subtype === "init") {
const unavailableServers = message.mcp_servers.filter(
(s) => s.status === "failed" || s.status === "needs-auth"
);
if (unavailableServers.length > 0) {
console.warn("Unavailable MCP servers:", unavailableServers);
}
}
想看这台服务器到底给了哪些工具,从同一条 init 消息里过滤 mcp__ 前缀:
for await (const message of query({ prompt: "...", options })) {
if (message.type === "system" && message.subtype === "init") {
const mcpTools = message.tools.filter((name) => name.startsWith("mcp__"));
console.log("Available MCP tools:", mcpTools);
}
}
Python 侧对应的是从 SystemMessage 的 message.data.get("tools", []) 里过滤同样的前缀。
读这个 tools 数组时要带上第四节的结论:它列的是到 init 发出那一刻已经连上的服务器的工具,加上那些有缓存工具列表、留到首次调用才连的服务器的工具。还没连上的服务器,其工具在这里是缺席的——所以数组里少了几个工具,第一反应应该是查状态,而不是回去改 allowedTools。
会话中途要拿最新状态,就调 mcpServerStatus()(TypeScript)或 ClaudeSDKClient.get_mcp_status()(Python)。
排查顺序建议这么走:先看 init 里的 status,failed 就按文档列的几类常见原因逐个排(环境变量没设、服务器没装、连接串不对、网络不通);status 是 connected 但工具没被调,那就回去看 allowedTools 有没有写对那个 mcp__servername__*;两边都对但第一轮就是没有,那多半是第四节的连接时机问题,考虑 alwaysLoad 或者把工具改成进程内的 SDK MCP server。
以上为按官方文档中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。
本文依据 Claude Code 官方文档(code.claude.com/docs)于 2026-08-17 的公开内容整理。
该产品闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观、操作手感与运行速度的任何描述。
该产品迭代频繁,文中涉及的命令、配置项与默认值随版本变动,请以官方文档最新内容为准。
本文不涉及价格、额度与限流的具体数值,相关信息请以官方定价与用量说明页为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。