用编码智能体的人都遇到过同一摊乱账:每台机器、每个 Agent 各自配一份 MCP 配置,密钥散落在十几个文件里,谁装过哪个服务器没人记得住,某个同事离职之后他配的那批连接还留在共享环境里。Docker Sandboxes 给出的解法是把 MCP 变成宿主机上的一项注册:你在外面注册一次,沙箱里的 Agent 只看到一个统一的网关端点,凭证由宿主机管、生命周期由宿主机管。
sbx,并安装了受支持的 Agent 集成(Claude Code、Codex、Devin、Gemini、Kiro、OpenCode 之一)。网关模式和「每个 Agent 自己配」差在哪
这两种做法看起来只是配置文件放在哪儿,实际上管的东西完全不同:
| 维度 | 直接给 Agent 配 MCP | 宿主机 MCP 网关 |
|---|---|---|
| 生效范围 | 只对这一家 Agent 的客户端生效 | 注册在宿主机,可被多个沙箱复用 |
| 凭证位置 | 散落在各家客户端的配置与凭证库里 | 集中托管,沙箱内不接触原始密钥 |
| 加载方式 | 每家各配一遍 | 创建沙箱时显式指定要暴露哪个 |
| 更新 | 逐个改配置 | 改一处注册,运行中的沙箱可用 load 加载 |
一个常见误会:这套东西和 Docker Desktop 的 MCP Toolkit 不是同一套。用 sbx mcp 不需要装 MCP Toolkit,两者的服务器设置也不共享。照着 Toolkit 的文档来配 Sandboxes,会一直配不通。
Step 1:先确认前置条件
# 登录(网关注册与密钥都挂在你的账号下)
sbx login
# 确认可用的 Agent 集成(会随版本扩展,以官方文档为准)
# Claude Code / Codex / Devin / Gemini / Kiro / OpenCode
# 如果注册的服务器是走 OCI 包的本地 stdio 服务器,
# 宿主机还需要装好并运行 Docker
Step 2:在宿主机注册一个 MCP 服务器
# 注册一个远端 MCP 服务器
sbx mcp add notion --url https://mcp.notion.com/mcp
# 需要 OAuth 的服务器,注册时会先弹一次授权流程
# 授权结果存在宿主机,注册本身不会把服务器挂到任何沙箱上
# 再注册一个
sbx mcp add linear --url https://mcp.linear.app/mcp
名字只能用字母、数字、点、连字符和下划线。要特别注意 --url 后面接的东西决定了它在哪里跑:
| --url 指向 | 服务器在哪运行 |
|---|---|
| 远端端点地址 | 远端运行,沙箱网关去连它 |
| 元数据地址(配合 --local) | 宿主机用 Docker 拉起 OCI 镜像 |
| 显式命令(配合 --command) | 宿主机上作为 stdio 服务器运行 |
Step 3:确认注册结果
sbx mcp ls
# 输出形如:
# NAME TYPE URL/COMMAND
# notion remote https://mcp.notion.com/mcp
# linear remote https://mcp.linear.app/mcp
注册成功不等于连得通。sbx mcp ls 只说明宿主机记下了这条定义;远端服务器是否可达、OAuth 是否还有效,要等沙箱真的去连的时候才知道。遇到工具数为零的情况,先怀疑凭证过期,而不是先怀疑 Agent 不会用工具。
Step 4:起沙箱并暴露服务器
# 创建沙箱时静态指定要暴露的服务器
sbx run claude --name mcp-demo --static-mcp notion
# 沙箱起来时会带一个 MCP 网关,并预加载 notion
# 同一个注册可以给多个沙箱复用
# 已经跑着的沙箱,按需再挂一个
sbx mcp load linear
load。全都堆在静态列表里,等于把网关变成了原先那种「什么都挂着」的配置。Step 5:自定义请求头,密钥不进注册文件
# 注册时用 ${...} 占位符,不要把真密钥写进去
sbx mcp add acme --url https://mcp.acme.com/mcp --header 'Authorization: Bearer ${api-key}' --header 'Accept: application/json, text/event-stream'
# 真值存进宿主机的凭证存储(会交互式提示你输入)
sbx secret set mcp:acme:api-key
# 沙箱连接时,网关读取密钥并在请求头里替换占位符
--header。这个设计的好处是注册记录可以安全地进版本库或团队共享:里面只有占位符,没有真密钥。密钥交接变成了「谁需要谁来 set 一次」。Step 6:把本地 stdio 服务器也纳进来
# 写法一:元数据地址 + --local
# 指向 registry 条目 / server.json / server.yaml,
# sbx 会解析出镜像并在宿主机上用 Docker 跑起来
sbx mcp add my-tool --local --url https://example.com/server.json
# 写法二:显式命令,直接作为 stdio 服务器在宿主机运行
sbx mcp add my-tool --command docker run -i --rm my/mcp-server
--local --url 的场景下宿主机必须有可用的 Docker;--command docker ... 同理。如果只是把 CI 里跑 Agent 的容器搬过来,却没在宿主侧准备 Docker,这一步会卡住,而报错往往不会直白地说「你缺 Docker」。
Step 7:SSRF 检查与它的逃生门
如果 --url 的主机名解析到内网、回环、链路本地或云元数据地址,注册仍会成功,但会给你一条警告。OAuth 元数据发现同样会屏蔽这些地址,包括重定向目标。要连可信的内部服务器,才用逃生门:
# 仅在你完全信任该 MCP 主机、OAuth 提供方
# 以及所有元数据重定向目标时才加这个开关
sbx mcp add internal-tool --url https://internal.corp/mcp --skip-ssrf-check
从不可信 URL 拉取清单,可能暴露内部服务或云元数据端点;DNS 重绑定还可能在检查之后把主机名指向别处。这条开关跳过的是两类检查(MCP URL 与 OAuth 元数据发现),不是一类。不要在「先让它跑起来」的心态下随手加。
两个容易搞混的点,再强调一次
一是位置:本地 stdio 的 MCP 服务器运行在宿主机,沙箱内的 Agent 只是网关的客户端。这解释了为什么在沙箱里 ps 找不到那个进程,也解释了为什么宿主机重启会影响所有沙箱。二是边界:sbx mcp 与 Docker Desktop 的 MCP Toolkit 互不共享设置,别把两边的排错经验互相套用。
顺带提醒一句,远端 MCP 端点如果用了私有地址,跳过检查就意味着宿主机可能被引导去访问不该访问的地方。团队共用一台开发机时,这条尤其要注意——注册是一次性的,影响是持续的。
常见问题速查
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 沙箱里看不到服务器 | 只注册了,没在启动时暴露 | 加 --static-mcp,或对运行中的沙箱用 mcp load |
| 调用报鉴权失败 | 占位符对应的密钥没设或已过期 | 重新 sbx secret set,并确认 OAuth 仍然有效 |
| 注册时提示解析到私有地址 | 命中了内网 / 回环 / 云元数据地址 | 确认可信后再考虑 --skip-ssrf-check |
| 本地服务器起不来 | 宿主机缺少或未启动 Docker | 装好并运行 Docker 后重试 |
| 改了服务器设置但沙箱没变 | 沙箱用的是启动时的静态列表 | 重建沙箱,或用 load 重新加载 |
| 照着 MCP Toolkit 文档配不通 | 两套产品不共享设置 | 改用 sbx mcp 系列命令 |
结语
把 MCP 收进网关,本质上是在重复企业 IT 走过很多遍的那条路:先让每个人自己配,等连接多到没人管得过来,再统一收到一层代理后面。这一层带来的收益很实在——密钥不再散落、服务器有账可查、同一个注册能给多个沙箱复用;代价是多了一个需要在宿主机上活着的组件,以及一个必须自己守住的权限边界。
务实的落地顺序是:先把只读类的服务器收进来,确认网关与密钥替换都跑通;再逐步接入会在外部系统写数据的服务器,并对这类服务器保持每次调用的显式暴露。不要在还没验证密钥替换是否生效的情况下,就把所有服务器一股脑注册进来——那只会把原来散落在配置文件里的风险,搬到一个你还没看懂的组件里。