需求
我要的是:ChatGPT 网页端的普通 Chat,直接访问并控制我这台 Mac mini。
不是远程桌面,不是再开一套云端开发环境,也不是把任务切到 Work。 当前对话只要需要验证真实环境,它就能自己去 Mac mini 上读、改、执行。
- 能打开真实 Git 仓库和工作区
- 能读文件、搜索代码、修改文件
- 能运行 shell、Git、构建、测试等命令
- 保留当前对话上下文,不重新开一套执行环境
我高频使用 ChatGPT,很多任务只是看文件、改代码、跑命令、验证结果。 每次切到 Work 都要把上下文重新带一遍,既麻烦,也多花一份 token。 省 token 是这次动手的动机之一。
约束
这不是「能连上就算完」,动手前先给链路划了几条边界:
- DevSpace 继续只监听
127.0.0.1 - Mac 不开放任何公网入站端口
- 不加 Docker / VM / sandbox / worktree
- 不把项目复制到另一套环境
- 仍以当前 macOS 用户身份操作现有开发环境
- 公网入口不能影响服务器上原有的业务
普通 Chat 最终拿到了什么能力
端到端跑通后,tools/list 实际暴露 6 个工具。
工具模式要设成 codex 才会凑齐这 6 个,否则会静默少掉两个(见踩坑 1)。
open_workspace
打开允许的本地工作区。
read
读取文件内容。
apply_patch
修改真实工作区里的文件。
show_changes
查看改了什么。
exec_command
运行 shell / Git / build / test 等命令。
write_stdin
继续跟长时间运行的进程交互。
实践演示
两台机器都是这样接进来的:在 ChatGPT 里添加连接器,授权页确认一次, 回到对话里显式选中它,它就开始读那台机器的真实环境了。连接器各挂各的,互不影响。
Mac mini。下面就是实际在用的普通 Chat,这个对话挂了 DevSpace Mac Mini,可以直接进我的 Mac mini,不需要切到 Work。
这次是让它核对一块板子:它先读仓库里的提交状态和审计结论, 再用 EasyEDA bridge 拿实时 PCB 的 DRC 结果和网表,最后逐条比对两者是否一致。
仓库、EasyEDA、Git、Node 都还在 Mac mini 上跑,ChatGPT 只是通过 MCP 调它们。
win-pc。同一套流程换一台机器,连接器叫 DevSpace Home PC。
同一套架构,两台机器
这套链路先在 Mac mini 上跑通,后来被原样复制到第二台机器上。 两台机器共用同一个境外入口,新增站点是加性修改,原来那套一行没动—— 真正不同的只是下面这两组取值。
ChatGPT Web · 普通 Chat
↓
https://mcp.<域名>
↓
境外节点 · Caddy
↓
127.0.0.1:<隧道端口>
↓
SSH 反向隧道
↓
本机 · 127.0.0.1:7676
↓
DevSpace
↓
真实工作区
手边还有 FRP 和 Tailscale 两条路:两者都要往服务器上装新东西, FRP 还要额外开安全组入站端口。SSH 反向隧道对服务器只读即用, 也不用放开任何端口,所以选了它。
那台机器同时跑着另一个业务,所以改动纪律是「只加不碰」:
Caddy 站点配置独立成一个文件,主配置只追加一行 import,
改完用 reload 而不是 restart。事后原业务的健康检查照旧。
链路
mcp.example.com · 17676127.0.0.1:7676ssh -N -R 17676:127.0.0.1:7676,由这台机器主动建立并常驻/etc/caddy/devspace.caddyKeepAlive/Volumes/PSSD/Dev(42 个 git 仓库)+ 桌面上的一个项目目录https://mcp.example.com/mcp关键点是 Mac 主动连出去。外部没有任何服务直接打进 Mac, DevSpace 也始终只绑定回环地址。
排查过程
最初的现象很简单:在 ChatGPT 里创建连接器时报 「获取 OAuth 配置时出错 / Request timeout」,授权页根本不出现。 但这一句报错后面叠了好几个互不相干的问题。
444 的表现和现场很像:连接被直接关闭、客户端超时、access log 里什么都看不到。 但「像」不等于「是」,这个假设得单独验证。
本机能完整跑通 OAuth + MCP 的 E2E,说明 OpenResty、反向隧道和 DevSpace 协议层都没问题。 但 ChatGPT 云端依旧一个请求都没到服务器。
取证的关键是两处日志都对得上:ChatGPT 抓取的第一个请求
/.well-known/oauth-protected-resource/mcp 返回 200,
第二个请求此后彻底消失——从未到达服务器。
换到新加坡入口后授权页立刻出现,所以那次迁移不是优化,它本身就是修复。
/.well-known/openid-configuration 返回 404。
MCP SDK 的路由没有提供这个路径,但 ChatGPT 会去探测它。 两侧入口都补上 rewrite 之后,discovery 才算真正完整。
如果选「用户自定义的 OAuth 客户端」且 client ID 留空,就不会触发
POST /register,整条授权流程必然失败。
DNS 为什么不是问题。排查中一度怀疑 DNS,后来把它排除了:Google、Cloudflare 和权威 NS 三方解析一致;
从境外节点打这套 discovery 端点,40 个节点 39 个通,
oauth-authorization-server 端点 25/25 通。解析从头到尾都是对的。
真正的区别在于请求的形态:单个探测请求能过, 而 OAuth 握手需要连续多个请求,这一串过不去。
最终验证
成功时整条链路的关键状态如下:
GET /.well-known/oauth-protected-resource/mcp → 200
GET /.well-known/oauth-authorization-server → 200
GET /.well-known/openid-configuration → 200
POST /register → 201
GET /authorize → 200
POST /authorize → 302
POST /token → 200
POST /mcp → 200
tools/list → 6 tools
| 验证项 | 结果 |
|---|---|
| ChatGPT OAuth discovery | 通过 |
| DCR 注册 | POST /register → 201 |
| Token exchange | POST /token → 200 |
| MCP session | POST /mcp → 200 |
| 工具列表 | 6 个工具 |
| 真实本地访问 | 普通 Chat 已实际读取 Mac mini 上的工作区 |
最后一项才是重点。health 和 tools/list 都只能证明服务活着,
证明不了普通 Chat 真的能碰到的我的文件——必须实际读一次。
链路
mcp.win.example.com · 17677127.0.0.1:7676ssh -N -R 17677:127.0.0.1:7676,由这台机器主动建立并常驻/etc/caddy/win.caddyC:\Users\Zusheng + D:\luma-render关键点一样:这台机器主动连出去,没有任何入站端口被打开。
平台特有的坑
装了 nvm-windows 也照旧:node -v 报「不是内部或外部命令」,
npm i -g 会因为 node-gyp 找不到 node 而失败。
脚本里得显式写 set PATH=C:\env\nvm\v24.21.0;%PATH%。
执行时限要显式改成不限时;LogonType 还得是交互式,
窗口才会出现在桌面上。
open_workspace 的响应里会夹 SSE 的 : keepalive 行。
打开大目录时尤其明显。写验证脚本别假设只有一个 data 事件—— 我第一版就是因此把成功误判成了失败。
验证
公网 E2E 走通 register 201 → authorize 302 → token 200 → initialize 200 → tools/list 200,
同样是 6 个工具。入口侧五分钟内记录到 46 条 openai-mcp/1.0.0 请求,全部 200 / 202;
模型侧也确实读到了这台机器的进程列表——不是「连上了」,是在被真正调用。
踩坑记录
1. DEVSPACE_TOOL_MODE 不是 config.json 配置
DEVSPACE_TOOL_MODE 和 DEVSPACE_WIDGETS 只能从环境变量读取,
config.json 里写这两项没有任何作用。
直接跑 devspace serve 会静默退回 minimal 模式:服务照常启动、不报任何错,
但 apply_patch 和 show_changes 会消失。
DEVSPACE_TOOL_MODE=codex
DEVSPACE_WIDGETS=changes
devspace serve
所以 Mac 侧必须经过一个固定的启动脚本拉起服务,再由 LaunchAgent 托管。
2. OAuth discovery 不止一个 well-known 路径
最后两侧入口都确保这 4 个路径可用:
/.well-known/oauth-protected-resource
/.well-known/oauth-protected-resource/mcp
/.well-known/oauth-authorization-server
/.well-known/openid-configuration
注意 ChatGPT 抓的是不带 /mcp 后缀的根路径,
而 MCP SDK 只提供带后缀的变体,两边都要补映射。
3. mihomo TUN 会让本机网络诊断产生假象
本机开着 mihomo TUN 时,curl --resolve、nc、
甚至 dig @1.1.1.1 的结果都不可信:流量会被劫回原路由,
返回的 198.18.x.x 是 fake-IP。我因此出现过一次「以为在测境外节点、
实际打到了大陆」。
要从对端或真正独立的境外节点反向测,否则很容易把本机代理行为误判成服务器故障。
4. DNS 缓存会制造 TLS 假故障
切到新入口后,浏览器一度报 ERR_SSL_PROTOCOL_ERROR。
原因不是证书有问题,而是浏览器还缓存着旧 IP;旧入口已经没有对应 SNI 的站点,
于是回了 TLS alert 80。清掉缓存就恢复了。
5. POST /mcp → 400 No valid MCP session 不一定是故障
实测这是 MCP 会话协商过程中可能出现的请求,随后带上 session 的重试直接返回 200。 只看单条 400 很容易误判成配置坏了。
权限边界:这里比我预想的更宽
allowedRoots 只约束 open_workspace。
但只要打开任意一个允许的工作区,exec_command 拿到的就是当前 macOS 用户身份的完整 shell。
实测它可以访问白名单之外的目录,也能读 ~/.devspace。所以:
allowedRoots 不是安全边界。
它只能防止「打开错工作目录」,防不住 shell 越界。 真正的边界是 OS 用户权限、网络入口、OAuth 授权和动作审批。
下次再搬一台机器,我会先看这几条
这份清单是两台机器各走一遍之后修正过的版本。 标了「Windows 侧」的那几条,是第一版清单里没有、搬第二台时才补上的:
- DevSpace 保持只监听
127.0.0.1 - Node 版本看 DevSpace 的
package.json:要求>=22.19 <27,别信devspace doctor打印的>=20.12 DEVSPACE_TOOL_MODE/DEVSPACE_WIDGETS写进启动脚本,而不是 config.json- Windows 侧:脚本里显式设置
PATH,非交互会话不继承用户级变量 - 服务和 SSH 反向隧道都做系统级常驻,异常能自动拉起
- Windows 侧:计划任务的执行时限改成不限时(默认 72 小时会杀掉长驻进程),
LogonType用交互式 - 公网入口放在境外节点
- 每台机器用独立域名:ChatGPT 按 URL 缓存的是 OAuth 元数据和失败结果,同域名会让两套服务互相干扰
- 入口端口每台机器分一个(Mac 17676、win-pc 17677),都绑在入口机器的
127.0.0.1 - 补齐 4 个 well-known discovery 路径
- ChatGPT 连接器的注册方式选 DCR
- 先在本机或从公网跑通 E2E,再接 ChatGPT
- Windows 侧:解析
open_workspace的响应要容忍 SSE 的: keepalive - ChatGPT 侧新增连接器后,要新开会话并显式选中它,刷新页面不会重连
- 最后一定用普通 Chat 实际读一次本地工作区,而不是只看 health / tools/list
当前状态
已验证:ChatGPT 网页端的普通 Chat 可以通过 MCP 直接访问本地设备上的真实工作区,
并调用 open_workspace、read、apply_patch、
show_changes、exec_command、write_stdin。
同一套架构后来复制到了第二台机器,两台各自独立、共用同一个境外入口,
都已经在普通 Chat 里被实际调用过。
直接收益是:大量小任务不必再切到 Work,也省掉了重复建立上下文的那份 token。