ZUSHENG Studio ← 返回 Activity

Field Note · 001

让 ChatGPT 普通 Chat 直接操作我的本地设备。

需求很直接:不进 Work 模式,让当前网页 Chat 直接读文件、改代码、跑命令,操作我本地设备上的真实工作区。省步骤是一半原因,另一半是 token——大量小任务不必再把上下文重新建立一遍。

2026 年 9 月 15 日 MCP · OAuth · DevSpace · SSH Reverse Tunnel 已验证可用

需求

我要的是: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 里添加连接器,授权页确认一次, 回到对话里显式选中它,它就开始读那台机器的真实环境了。连接器各挂各的,互不影响。

ChatGPT 中添加 DevSpace Home PC 连接器的授权页,提示始终遵守权限、一切由你掌控、连接器可能引入风险
添加连接器。授权页确认一次,连接器名按机器取——这里是 DevSpace Home PC。

Mac mini。下面就是实际在用的普通 Chat,这个对话挂了 DevSpace Mac Mini,可以直接进我的 Mac mini,不需要切到 Work。

ChatGPT 网页端普通 Chat 连接 DevSpace Mac Mini,读取本机仓库并通过 EasyEDA bridge 检查实时 PCB、DRC、网表和发布状态的实际会话截图
实际会话:普通 Chat 直接连上 DevSpace Mac Mini,读本机仓库,再通过本机 EasyEDA bridge 取实时 PCB、DRC 和网表状态。执行和分析都没有切到 Work 模式。

这次是让它核对一块板子:它先读仓库里的提交状态和审计结论, 再用 EasyEDA bridge 拿实时 PCB 的 DRC 结果和网表,最后逐条比对两者是否一致。

本机环境没有搬走。

仓库、EasyEDA、Git、Node 都还在 Mac mini 上跑,ChatGPT 只是通过 MCP 调它们。

win-pc。同一套流程换一台机器,连接器叫 DevSpace Home PC。

ChatGPT 普通 Chat 读到的这台机器的进程列表:ZCode、嘉立创 EDA、blender-mcp、node、python、ToDesk,以及逐项判断
实际读到的:ZCode、嘉立创 EDA、blender-mcp、node、python、ToDesk, 每一项后面跟着它自己的判断。

同一套架构,两台机器

这套链路先在 Mac mini 上跑通,后来被原样复制到第二台机器上。 两台机器共用同一个境外入口,新增站点是加性修改,原来那套一行没动—— 真正不同的只是下面这两组取值。

ChatGPT Web · 普通 Chat
        ↓
https://mcp.<域名>
        ↓
境外节点 · Caddy
        ↓
127.0.0.1:<隧道端口>
        ↓
SSH 反向隧道
        ↓
本机 · 127.0.0.1:7676
        ↓
DevSpace
        ↓
真实工作区
为什么用 SSH 反向隧道

手边还有 FRP 和 Tailscale 两条路:两者都要往服务器上装新东西, FRP 还要额外开安全组入站端口。SSH 反向隧道对服务器只读即用, 也不用放开任何端口,所以选了它。

那台机器同时跑着另一个业务,所以改动纪律是「只加不碰」: Caddy 站点配置独立成一个文件,主配置只追加一行 import, 改完用 reload 而不是 restart。事后原业务的健康检查照旧。

链路

域名 · 隧道端口
mcp.example.com · 17676
本机监听
DevSpace 只监听 127.0.0.1:7676
隧道
ssh -N -R 17676:127.0.0.1:7676,由这台机器主动建立并常驻
公网入口
境外节点上的 Caddy 终止 HTTPS,再反代到服务器本机的 17676
入口站点
/etc/caddy/devspace.caddy
常驻方式
两个 LaunchAgent,服务与隧道各一个,都是 KeepAlive
工作区白名单
/Volumes/PSSD/Dev(42 个 git 仓库)+ 桌面上的一个项目目录
ChatGPT 连接
通过 MCP + OAuth 连接 https://mcp.example.com/mcp

关键点是 Mac 主动连出去。外部没有任何服务直接打进 Mac, DevSpace 也始终只绑定回环地址。

排查过程

最初的现象很简单:在 ChatGPT 里创建连接器时报 「获取 OAuth 配置时出错 / Request timeout」,授权页根本不出现。 但这一句报错后面叠了好几个互不相干的问题。

01 · 最初的怀疑
是不是入口上的 WAF 用 444 把请求吞了。

444 的表现和现场很像:连接被直接关闭、客户端超时、access log 里什么都看不到。 但「像」不等于「是」,这个假设得单独验证。

02 · 单变量实验
只关掉 WAF,其他变量全部不动。

本机能完整跑通 OAuth + MCP 的 E2E,说明 OpenResty、反向隧道和 DevSpace 协议层都没问题。 但 ChatGPT 云端依旧一个请求都没到服务器。

03 · 结论
WAF 被证伪,问题在 OpenAI 云端 → 中国大陆 IP 的链路。

取证的关键是两处日志都对得上:ChatGPT 抓取的第一个请求 /.well-known/oauth-protected-resource/mcp 返回 200, 第二个请求此后彻底消失——从未到达服务器。 换到新加坡入口后授权页立刻出现,所以那次迁移不是优化,它本身就是修复。

04 · 另一个独立 bug
/.well-known/openid-configuration 返回 404。

MCP SDK 的路由没有提供这个路径,但 ChatGPT 会去探测它。 两侧入口都补上 rewrite 之后,discovery 才算真正完整。

05 · 最后一处配置
ChatGPT 面板必须选动态客户端注册(DCR)。

如果选「用户自定义的 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 · 17677
本机监听
DevSpace 只监听 127.0.0.1:7676
隧道
ssh -N -R 17677:127.0.0.1:7676,由这台机器主动建立并常驻
公网入口
同一台境外节点上的另一个 Caddy 站点,反代到服务器本机的 17677
入口站点
/etc/caddy/win.caddy
常驻方式
计划任务:登录触发、交互式会话、失败自动重试
工作区白名单
C:\Users\Zusheng + D:\luma-render
ChatGPT 连接
同样走 MCP + OAuth,注册方式选 DCR

关键点一样:这台机器主动连出去,没有任何入站端口被打开。

平台特有的坑

SSH 非交互会话不继承用户级 PATH。

装了 nvm-windows 也照旧:node -v 报「不是内部或外部命令」, npm i -g 会因为 node-gyp 找不到 node 而失败。 脚本里得显式写 set PATH=C:\env\nvm\v24.21.0;%PATH%。

计划任务默认 72 小时就把长驻进程杀掉。

执行时限要显式改成不限时;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。