MCP
MCP(Model Context Protocol)是一套标准协议,用来把外部系统包装成模型能调的工具。 接一次,工单系统 / 发布平台 / 内部知识库就都成了 sid-code 能直接操作的东西。
这页讲怎么接、配置放哪、以及一个几乎人人会撞的问题:接上了但模型说"没有 mcp 工具"。
快速上手
不用手写 JSON,sid-code mcp add 一条命令:
sid-code mcp add fs npx -y @modelcontextprotocol/server-filesystem /tmp实测输出:
MCP 服务器 "fs" 已添加到 project 配置(stdio)。重启会话后生效。默认写进项目级 .mcp.json。项目级 server 来自仓库(等于别人的配置),未批准前不加载, 所以这时它不在 mcp list 的已配置列表里,而是在末尾被逐个列为待审批:
sid-code mcp list未配置任何 MCP 服务器。用 `sid-code mcp add <name> <command|url>` 添加。
另有 1 个项目级 MCP 服务器待审批(未加载),见 `sid-code mcp pending`:
fs ⏸ 待审批(启动 sid-code 时审批)sid-code mcp pending待审批的项目级 MCP 服务器(共 1 个,当前**未加载**):
fs
项目: /path/to/your/repo
批准: sid-code mcp approve <name> (或 --all 批准全部)
拒绝: sid-code mcp reject <name>
批准后需重启会话才会连接。批准它:
sid-code mcp approve fs已批准 1 个 MCP 服务器: fs。重启会话后连接。这之后它才进入 mcp list / mcp get 的已配置列表(来自 .mcp.json 的会多一行「来自 <文件路径>」):
sid-code mcp get fsfs [stdio] npx -y @modelcontextprotocol/server-filesystem /tmp
{
"transport": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/tmp"
],
"scope": "project"
}项目级 server 未批准前不加载
项目级 MCP server(.mcp.json)来自仓库,从没批准或拒绝过的一律不加载(fail-closed), mcp list 只把它列为「⏸ 待审批」,mcp get 查不到。批准有三条路:
- 启动时:TUI 启动发现待审批 server 会弹审批框,一次一个,可选批准 / 全部批准 / 拒绝, 光标默认停在「拒绝」;Esc 暂不决定(本会话不加载,下次启动再问)。批准后当场连接,不用重启。 有信任框或外部
@import审批框时,审批框排在它们之后弹。 - 会话里:
/mcp approve <name>、/mcp reject <name>。注意不带参数的/mcp面板没有审批入口—— 它只管理已加载的 server(查看工具、重连、启停、OAuth 授权),待审批项还没加载,不在面板里 - CLI:
sid-code mcp approve <name>(或--all批准全部待审批项),sid-code mcp reject <name> | --all拒绝。 CLI 不启动会话,批准后下次启动才连接
拒绝后不再询问;指名 approve / reject 可以随时改判已经批准或拒绝过的 server。 批准记录存在 ~/.sid-code/state/mcp-approvals.json,按 项目身份:server 名 加配置内容指纹 (SHA-256)记账。指纹覆盖该 server 在 settings.json / .mcp.json 里除 enabled 外的全部字段—— command / args / url / headers / env,也包括 transport / timeout / retries / includeTools / excludeTools / auth / oauth。任何一处被改过, 下次启动会回到待审批、重新询问,不会沿用旧批准执行新命令。只改 enabled 开关不需要重新审批; ${VAR} 按原文计算指纹,换环境变量的值也不需要。拒绝按名字记,内容变了也仍是拒绝。 「全部批准」是对整个项目的显式授权,不受指纹约束。早于此机制的批准记录没有指纹,升级后需要重新批准一次。 项目身份是 git 主仓根:同一仓库的任意子目录、任意 linked worktree 共用一份审批, 只批准一次;不在 git 仓库里时就是启动目录。旧版按启动目录记的审批仍然认,下次改判时迁移。 同一个 server 在不同仓库里要分别批准。用户级配置不需要批准。
sid-code mcp 这套子命令不启动会话,只读写配置文件,所以脚本和 CI 里能直接用。
五种传输方式
| transport | 什么时候用 | 配置要点 |
|---|---|---|
stdio | server 是本地可执行程序(绝大多数官方 server) | 给 command + args |
http | server 是远程 HTTP 服务(内部平台常见)。走 Streamable HTTP,对齐 2025-03-26 规范 | 给 url |
http-json | 仅当远程 server 不支持 Streamable HTTP 时才显式指定(旧的单 JSON HTTP 传输,兼容保留) | 给 url |
sse | 远程 Server-Sent Events | 给 url |
ws | 远程 WebSocket | 给 url |
在 .mcp.json / mcp.local.json 里不写 transport 时会推断:先看 Claude Code 风格的 type(streamable-http 是 http 的别名),再按有没有 command / url 判—— 有 command → stdio,只有 url → http。settings.json 里没有这层推断: 那里的 transport 是必填字段,省了会在启动横幅里报「无效值」。
手写配置
~/.sid-code/settings.json(用户级,所有项目都能用):
{
"mcpServers": {
"internal-api": {
"transport": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer ${INTERNAL_MCP_TOKEN}"
}
},
"fs": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}.mcp.json(项目级,跟仓库走,团队共享):
{
"mcpServers": {
"fs": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}别把 token 明文写进 .mcp.json
.mcp.json 跟着仓库进 git。凭据用 ${环境变量} 引用,别直接写值—— 配置加载时会做环境变量展开。用户级 settings.json 不进仓库,但也建议同样处理。
完整模板与三个容易踩的点
一份带远程 server 示例的完整模板:
{
"mcpServers": {
"tavily": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "tavily-mcp@0.2.22"],
"env": {
"TAVILY_API_KEY": "${TAVILY_API_KEY}"
}
},
"example-remote": {
"transport": "http",
"url": "https://mcp.example.com/v1",
"headers": {
"Authorization": "Bearer ${EXAMPLE_MCP_TOKEN}"
},
"enabled": false
}
}
}.mcp.json 从哪里读
从启动目录开始逐级向上一直读到文件系统根,每层的 .mcp.json 都读,同名 server 离启动目录越近越优先。上界不是 git 仓库根,也不是家目录——所以 monorepo 外层工作区、 甚至 ~/.mcp.json 里声明的 server,在仓库深层子目录启动时同样可见(对齐 Claude Code)。 sid-code mcp list/pending/approve/reject 与会话用的是同一套查找,列表一致。
"enabled": false 让这个 server 默认不连接(面板里显示「已禁用」,可随时 /mcp enable)。
模板里 tavily-mcp 锁到了具体版本(写本文时的最新版,用前换成你核过的版本)。 不要写 @latest:npx 每次启动都可能拉到新代码,而审批指纹只看配置文本, @latest 这串字不变,上游换了实现也不会触发重新审批。
三个只有读过源码才知道的点:
${VAR}在五个字段里都会展开:command/args/url/headers/env(见packages/core/src/mcp/env-expansion.ts的expandConfigEnvVars)。 支持${VAR}与${VAR:-默认值}两种写法,所以上面env里写的"${TAVILY_API_KEY}"会取到你 shell 里 export 的那个值,不用把 key 落进配置文件。env里展开的另一个用途是「改名」:stdio 子进程本就继承父进程的整个环境 (packages/core/src/mcp/transport.ts的{ ...process.env, ...env }), 所以这里的${VAR}价值在于把MY_API_KEY喂成 server 要的API_KEY。 也可以索性删掉env块、直接靠继承——两种都行。- 远程服务器的凭据用
headers里的${VAR},不必把 token 明文写进.mcp.json或落到磁盘上。
需要登录的远程 server(OAuth)
远程 server 要求 OAuth 登录时,在配置里加一个空的 oauth 对象即可,其余全部自动发现:
{
"mcpServers": {
"internal-api": {
"transport": "http",
"url": "https://mcp.example.com/mcp",
"oauth": {}
}
}
}首次连接时的流程(2026-10-02 用本地 OAuth mock 端到端实测):
按 RFC 9728 / RFC 8414 发现授权服务器,没配
clientId就动态注册一个在
127.0.0.1起一个临时回调端口,把授权 URL 打到 stderr:text[MCP] internal-api 需要 OAuth 授权,请在浏览器打开以下 URL(5 分钟内有效): https://auth.example.com/authorize?response_type=code&client_id=...&code_challenge_method=S256&...在浏览器里同意后,回调拿到授权码,用 PKCE(S256)换 token,然后连接 server
token 存在 ~/.sid-code/mcp-oauth.json,权限 0600。access token 过期后, 下次连接会用 refresh token 静默刷新,不会再弹授权;授权服务器轮换了 refresh token 也会一并保存。
目前的验证范围
上面这套流程只在本地 mock 授权服务器上跑通过,还没有对接过真实的第三方授权服务器。 接你们自己的 server 时如果卡在发现阶段,可以在 oauth 里显式给 authServerMetadataUrl(必须是 https://)。
公司后端提供的远程 server(auth: "sid-backend")
公司后端自己提供的远程 MCP(例如飞书文档 feishu-docs,后端怎么开启见飞书文档授权)不用配 token。写上 "auth": "sid-backend",sid-code 就会用 sid-code auth login 拿到的设备凭据去连:
{
"mcpServers": {
"feishu-docs": {
"transport": "http",
"url": "https://agent.example.com/traj/api/v1/ctl/feishu/mcp",
"auth": "sid-backend"
}
}
}- 凭据每次请求现取,不写进配置文件。重新登录续期后不用重启。
- 只发往
backend.url的同一个 origin(协议 + 主机 + 端口完全一致)。url指向别的地址时直接拒绝连接并在/mcp里显示原因,不会「不带凭据照连」。 这是给插件用的:插件写一句auth: "sid-backend"加一个外部地址,也拿不到你的凭据。 - 没配
backend.url,或者本机还没登录,连接同样会被拒绝,按提示执行sid-code auth login(TUI 里/login,见企业登录)。 - 只支持
http/http-json/sse。配置里自己写的Authorization头会被忽略。
四层作用域与优先级
| scope | 位置 | 谁能看到 |
|---|---|---|
dynamic | 运行时注入(IDE 集成等) | 当前会话 |
user | ~/.sid-code/settings.json | 你的所有项目 |
local | ~/.sid-code/projects/<项目身份>/mcp.local.json(只能手写,mcp add --scope 只接受 user / project;项目身份 = git 主仓根,全部 worktree 共用) | 当前项目,不进 git |
project | 启动目录及其每一级父目录的 .mcp.json | 团队共享 |
同名或同签名(相同 command+args,或相同 url)时,优先级:
dynamic > user > local > projectdynamic 排最高是有意的:IDE 注册的是运行时活连接,不该被配置文件里的同名项顶掉。 其余三层的顺序对应「个人全局 > 本地实验 > 团队共享」。
签名去重的意义:同一个 server 在用户级和项目级各配了一份(名字还不一样), 不会连两次——按签名认出是同一个,只保留高优先级那份。
上面四层合并完之后,还有两个来源会叠上去:
- 插件带的 MCP server:名字带
plugin:<插件名>:前缀,不会和你自己的配置撞名; 但它不参与上面的签名去重,和你自己配的是同一个 server 时会连两次 --mcp-config(见下一节):同名时优先级最高,盖过上面所有来源; 加--strict-mcp-config时只剩它
企业 mcpPolicy 对这些来源全部生效,见下方企业管控。
会话级注入:--mcp-config
不改任何配置文件,临时接一个 server:
sid-code --mcp-config /tmp/extra-mcp.json配合 --strict-mcp-config 可以只用这一份,忽略其他所有来源——CI 里跑干净环境很有用:
sid-code --mcp-config /tmp/extra-mcp.json --strict-mcp-config -p "..."实测日志确认了隔离生效:
● [MCP] 严格 MCP 配置模式(--strict-mcp-config):仅加载 --mcp-config 指定的 1 个服务器。
● [MCP] 开始连接 1 个 MCP 服务器
● [MCP] 连接服务器: fetch-demo--mcp-config 可以重复给多次,也接受内联 JSON 而不只是文件路径。
企业管控
MCP server 能以你的身份跑进程、访问内网,所以企业侧有三道管控:
① mcpPolicy 黑白名单。 在 settings.json(或企业下发的 managed 配置)里写:
{
"mcpPolicy": {
"deniedServers": [{ "url": "https://*.untrusted.example.com/*" }],
"allowedServers": [
{ "name": "internal-api" },
{ "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"] },
{ "url": "https://*.corp.example.com/*" }
]
}
}每一条可以按 name、完整 command(含参数的数组)或 url(支持 * 通配,按整串匹配,所以要连协议一起写)匹配。 判定顺序是先 deny(命中即否决)再 allow(配了 allow 名单就必须命中其中一条),都没配则放行。 匹配用的是 ${VAR} 展开之后的值,写成模板绕不过去。
② 企业策略整体禁用 MCP。 企业策略的 policyLimits 里把 mcp 设为不允许, 启动时所有已配置的 server 都会被清空,日志:
MCP 已被企业策略禁用,忽略 N 个已配置的服务器这行是 info 级,默认不输出——用 -d(或 --debug)启动才看得见。
它只关 MCP 服务器,不会连带关掉 IDE 集成。
③ 连接前最后一道闸。 mcpPolicy 不只在配置合并时过滤,每次真正建立连接之前还会再过一遍 (packages/core/src/mcp/manager.ts)。所以插件带的 server、--mcp-config 注入的 server、 IDE 运行时注册的 server 这些不经过合并层的来源,同样逃不过它。被拦时日志:
策略过滤: xxx 被 mcpPolicy 拒绝,不建立连接MCP 工具的名字
MCP 工具统一命名成 mcp__<server 名>__<工具名>:
mcp__fetch-demo__echo权限规则里可以按这个格式写,支持整个 server 通配:
{
"permissions": {
"allow": ["mcp__fetch-demo__*"]
}
}MCP 的三原语:Tools / Prompts / Resources
MCP 协议有三类原语,sid-code 全部支持。上面讲的是 Tools——最常用的一条腿,但不是唯一一条:
| 原语 | 是什么 | 在 sid-code 里长什么样 |
|---|---|---|
| Tools | 模型能调的函数 | 延迟加载的 mcp__<server>__<tool> 工具,走 tool_search 取出 |
| Prompts | server 预置的提示词模板 | 动态转成 mcp__<server>__<prompt> 斜杠命令,进 / 菜单可补全 |
| Resources | server 暴露的数据(文件、DB 行、配置…) | ListMcpResources / ReadMcpResource 两个工具,模型自主列举和读取 |
三者的区别在于谁发起:Tools 和 Resources 由模型在对话中按需调用,Prompts 由你主动 / 触发。
Prompts:server 预置的提示词变成斜杠命令
一个 MCP server 可以声明一组 prompt(带参数的提示词模板)。sid-code 会把每个 prompt 动态注册成一个斜杠命令,命名规则和工具一致:
mcp__<server 名>__<prompt 名>接了一个叫 git 的 server,它有个 commit-message prompt,输入 / 就能看到:
/mcp__git__commit-message直接跑,prompt 模板渲染后的内容会作为你的输入提交给模型——相当于 server 帮你写好了一段提问。带参数的 prompt 两种传法都行:
/mcp__git__commit-message staged=true # key=value 形式
/mcp__git__commit-message true # 位置参数,按 prompt 声明的参数顺序填几个要知道的行为:
- 命令列表是动态的——server 连上才有对应命令,断了就消失,不用重启会话
- 参数提示:
/菜单里会显示必填<param>和可选[param] - 不是所有 server 都有 prompt——没有 prompt 的 server 不会产生任何斜杠命令,这是正常的
Resources:让模型读 server 暴露的数据
Resources 是 server 暴露的数据源——可以是文件、数据库行、配置片段,由 URI 标识(如 file:///tmp/a.txt、db://table/rows)。模型通过两个工具自主访问:
ListMcpResources 列出已连接 server 暴露的资源(可按 server 过滤)
ReadMcpResource 读取指定资源,参数 server + uri这两个工具不走延迟加载——它们首轮就常驻在工具池里,模型可以直接调,用不着 tool_search(tool_search 只搜延迟池,反而搜不到它们)。典型用法是你直接说要什么, 模型会自己去列、去读:
看一下 git server 里有哪些资源模型会调 ListMcpResources 列清单,再按需 ReadMcpResource 取内容。
两个设计取舍值得知道:
- 文本进上下文,二进制落盘。文本资源直接读进对话(受下面的 token 上限约束);二进制资源(图片、PDF 等)不把 base64 灌进上下文,而是落盘到临时文件、返回路径——模型再走 Read 工具的 vision 管道看它
- 资源内容当数据,不当指令。Resources 来自外部系统,sid-code 把它当数据处理而不是指令执行——这和权限与人工确认的安全语义一致,防止恶意 server 用 resource 内容注入指令
工具返回值的上限
所有 MCP 工具的返回内容(不只 Resources,Tools 也一样)都按 25000 token 预算截断, 超了会附一段「输出截断」的引导语,而不是把整段内容塞进上下文。要调整用 SID_CODE_MAX_MCP_OUTPUT_TOKENS(也认无前缀的 MAX_MCP_OUTPUT_TOKENS,对齐 Claude Code), 非法值回退默认。
文本超过 10 万字符时还有一层字符级保护:完整结果先落盘到临时文件,工具只回 「结果过大 (N 字符),完整结果已保存到: <路径>」加一段预览——需要全文时再走 Read 去读那个文件。 两层可以同时发生:落盘存完整存档,截断控喂给模型的量。
server 自带的「使用说明」(instructions)
MCP server 可以在 initialize 时返回一段 instructions,用它告诉模型「我该怎么被使用」。 sid-code 会在 server 首次连上时把它(截断到 2048 字)注入一次:包在 <system-reminder> 围栏里追加到你的消息旁边,并标明这是 harness 注入的说明、不是你的输入。 已公告过的 server 不会重复注入。
走动态注入而不是 system prompt 是刻意的:server 会中途连断,塞进静态前缀会击穿 prompt cache。
常见问题
接上了,但模型说"没有 mcp 工具"
这是最常见的困惑,而且不是故障。实测问模型能用哪些 mcp__ 工具:
当前环境中没有 `mcp__` 前缀的工具。可用的 MCP 相关工具只有两个:
- ListMcpResources
- ReadMcpResource原因:MCP 工具默认延迟加载。MCP 是上下文膨胀的头号来源——一个 server 十几个工具, 每个带完整 JSON Schema,几个 server 就能吃掉几万 token,而其中大部分这次任务根本用不到。 所以它们不进首轮工具池,模型要用先搜。
让模型走 tool_search 就能拿到,实测:
搜索 `echo` 找到 1 个工具:
- `mcp__fetch-demo__echo` — 回显输入的字符串(Echoes back the input string)所以正确的提问方式不是"你有 xx 工具吗",而是直接说要干的事——模型会自己去搜。
无头模式(-p)下搜到了也要放行才能调
tool_search 本身在任何模式下都不需要确认。但它调出来的 MCP 工具会像普通工具一样 走权限判定,而无头模式没人能点确认,所以会被自动拒绝。要在 -p 里用 MCP, 就把对应工具加进白名单:
sid-code -p "用 context7 查 Bun.serve 的 idleTimeout 默认值" \
--allowed-tools "mcp__context7__resolve-library-id,mcp__context7__query-docs"或者在 settings.json 的 allowedTools 里长期配置。
真的需要某个工具首轮就可见,用 toolSearchKeepLoaded 豁免(支持 mcp__server__* 通配):
{
"toolSearchKeepLoaded": ["mcp__internal-api__*", "mcp__fetch-demo__echo"]
}代价是这些工具的 schema 每轮都占上下文,只对真正高频的工具开。
加了但没生效
三件事按顺序查:
- 重启了吗 ——
mcp add的输出明确写了「重启会话后生效」,不热加载 - 项目级批准了吗 ——
sid-code mcp pending里还列着它,就是还没批准(未批准的不加载) - server 起来了吗 —— 开
-d看[MCP] 连接服务器: xxx后面有没有报错; 如果看到被 mcpPolicy 拒绝,是企业策略拦了,见企业管控
临时停用一个 server,不删配置
会话里 /mcp disable <name>(或在 /mcp 面板里选它 → 禁用):当场断开连接、注销它的工具, 面板显示「已禁用」,模型下一轮就调不到了。/mcp enable <name> 当场重连。
- 状态存在你自己的
~/.sid-code/projects/<项目身份>/mcp-state.json(项目身份 = git 主仓根, 同一仓库的子目录和 worktree 共用),不改写.mcp.json——那是团队共享的文件。 - 持久生效:重启后仍是禁用。对插件和
--mcp-config来源的 server 同样有效。 - 配置文件里写死
"enabled": false的 server,/mcp enable之后重启也保持启用 (mcp-state.json里的「已启用」记录盖过配置里的false)。 sid-code mcp list里禁用的 server 照常列出,后面标「⊘ 已禁用」。
删一个 server
sid-code mcp remove fsMCP 服务器 "fs" 已移除。重启会话后生效。删不存在的会明确告诉你,退出码是 1,脚本里可以直接靠退出码判断:
错误: MCP 服务器 "nope" 不存在于配置中。mcp add 写到哪个文件了
默认 project(.mcp.json)。要写用户级加 --scope user:
sid-code mcp add internal-api https://mcp.example.com/mcp --scope userMCP 服务器 "internal-api" 已添加到 user 配置(http)。重启会话后生效。写 user 作用域时是外科式补丁 settings.json 的 mcpServers 字段,不整体覆写文件—— 这样你手写的其他配置和注释不会被冲掉,密钥也不会被重新序列化成明文。
把 sid-code 自己当 MCP server 用
sid-code mcp serve它会把自身工具通过 stdio 暴露成 MCP server,给别的 MCP 客户端调。默认只读, 要放开写操作加 --allow-write。
相关
- 扩展方式总览 — MCP 和 Skill / Hook / 子代理怎么选
- 权限与人工确认 —
mcp__*规则怎么写 - 无头模式与脚本化 — CI 里配合
--strict-mcp-config - CLI 参数与子命令 —
mcp子命令与--mcp-config的完整签名 - settings.json 字段 —
mcpServers字段结构