Skip to main content
Glama

mcp-terminal:给 Agent 用的本地终端 MCP Server(macOS)

mcp-terminal 让任何 MCP 客户端(Manus、Claude Desktop、Cursor、自研 Agent……)通过 stdio 在本机创建并控制终端会话:启动命令、按游标增量读取输出、向交互式程序写入、判断“在跑 / 等输入 / 已结束”,并在需要时中断或终止整棵进程树。所有会话默认运行在 macOS Seatbelt 沙箱里,沙箱可写目录可以由你指定。

它不是把单次 exec 包成一个 Tool,而是一个有状态的终端服务:会话里的 cd、export、虚拟环境会保留;长任务不会阻塞调用方;输出不会因为没及时读取而丢失;客户端断开或服务崩溃后,残留进程会被找回并清理。

特性一览

方面

做法

会话模型

PTY(node-pty)为默认后端,另有 pipe 后端用于分离 stdout/stderr;每个会话有 session_id,也可起名(name="devserver")后用名字引用

命令边界与退出码

向 zsh / bash 注入 OSC 133 shell 集成标记(带每会话随机 nonce,程序伪造的标记会被忽略),terminal_exec 返回这条命令自己的输出与退出码,并跟踪 cwd

增量输出

单调递增的字节游标;内存环形缓冲 + 磁盘分段溢写;省略游标即“从上次读到的地方继续”;超长输出头尾保留、中间给出可分页的游标

不阻塞的等待

`until=["command_done"

交互输入

terminal_send 支持文本、submit、按键名(C-c、Up、Escape、F5……,自动适配应用光标模式);检测“等待输入”(前台进程阻塞在 tty 读 + 提示符启发式)

全屏程序

基于 @xterm/headless 维护屏幕模型;vim、top、less 等进入备用屏幕时自动返回渲染后的屏幕快照

输出整理

默认去掉 ANSI 颜色与控制序列,\r 进度条折叠为最终状态;view="raw" 可取原始内容

沙箱

每个会话用 sandbox-exec 套一份动态生成的 Seatbelt 配置:可读大部分文件,只能写会话 cwd 和你允许的目录;凭据目录不可读;网络可设 allow / localhost / deny

命令策略

tree-sitter-bash 解析整条命令(管道、&&、$( )、sh -c "…"、env/xargs/timeout 包装都会展开),内置危险命令规则 + 用户规则,整体取最严

审批

需审批的命令依次尝试:MCP elicitation(客户端支持时)→ macOS 原生对话框 → termctl approve <id> 审批票据;审批与参数摘要绑定、一次性、会过期

环境变量与脱敏

默认白名单继承环境变量,*_TOKEN、*_KEY、AWS_* 等被剥离(rc 文件里再导出的也会在会话内 unset),其值和常见令牌格式在输出中被替换为 [REDACTED:…]

审计

JSONL 审计日志,按实例做哈希链,termctl audit verify 可检测篡改;只记录输出哈希,不记录输出内容

生命周期

关闭会话时按 SIGHUP → SIGTERM → SIGKILL 升级,覆盖进程组、tty 上的作业和已脱离的 nohup 进程;SQLite 注册表记录会话与观察到的进程组,服务崩溃重启后自动对账并清理孤儿进程

Related MCP server: shell-session-mcp

环境要求

macOS(已在 macOS 26.5 / Apple Silicon 上测试;所用 Seatbelt 规则在较早版本上同样可用,但未逐一验证),Node.js 22.13 及以上(需要内置的 node:sqlite)。不需要 Xcode:node-pty 自带 macOS 预编译二进制,bash 解析器使用 WASM。

安装与自检

git clone https://github.com/dogeheng/MCP-server-controller.git
cd MCP-server-controller
npm install          # postinstall 会修复 node-pty spawn-helper 的执行权限
npm run build
node dist/bin/termctl.js doctor

doctor 会检查 Node 版本、sandbox-exec、PTY 能否启动、解析器能否加载、当前生效的沙箱与审批配置。可以 npm link 把 mcp-terminal 和 termctl 两个命令放进 PATH,下文为简洁起见直接写命令名。

接入 MCP 客户端(stdio)

最简单的方式是让 termctl 生成配置片段,它会写入当前 Node 的绝对路径(GUI 应用启动时 PATH 往往不完整):

termctl client-config --workspace ~/Projects/demo

输出形如:

{
  "mcpServers": {
    "terminal": {
      "command": "/Users/you/.n/bin/node",
      "args": ["/path/to/mcp-terminal/dist/bin/mcp-terminal.js", "--workspace", "/Users/you/Projects/demo"]
    }
  }
}

Manus:在 Manus 的自定义 MCP(Custom MCP)里选择以 JSON / 命令方式添加,粘贴上面的 mcpServers 片段即可(stdio 形式为 command + args + 可选 env)。添加后先让 Agent 调一次 terminal_list 或 terminal_run {"command":"pwd"} 验证。已在 Manus Studio 桌面端实测:服务由本机的 manus-computer-operator 以 stdio 方式拉起,运行在这台 Mac 上;服务进程退出后,下一次工具调用会自动重新拉起。Manus 目前不声明 elicitation 能力,所以需要审批的命令会弹出 macOS 原生对话框,超时后退回到 termctl approve <id>。修改代码并 npm run build 后,在活动监视器里结束名为 node 且命令行含 dist/bin/mcp-terminal.js 的进程,Manus 下次调用时就会加载新版本。完整测试步骤见 TESTING.md。

Claude Desktop:把片段合并进 ~/Library/Application Support/Claude/claude_desktop_config.json。其他客户端:只要支持 stdio 传输,配置方式相同。

服务端同时支持 2025-era 的 initialize 握手和 2026-07-28 版协议(无初始化握手、MRTR 形式的 elicitation),由 SDK 在首个请求时自动选择。

设定沙箱目录

沙箱是“读宽写窄”:会话能读系统里的大部分文件(凭据目录除外),但只能写下面这些位置。

方式

作用

--workspace <dir>

最常用。会话只能在 <dir> 内启动(allowed_roots = [dir]),默认 cwd = dir,沙箱可写 = 会话 cwd + 临时目录

--root <dir>(可重复)

只设置允许的工作目录根,替换默认的 ~、/tmp

--cwd <dir>

默认工作目录

--allow-write <dir>(可重复)

额外可写目录,例如 ~/.npm、~/Library/Caches、~/.cargo

--deny-read <path>(可重复)

额外不可读路径

`--network allow

localhost

--profile host / --no-sandbox

不使用 OS 沙箱(命令策略与审批仍生效)

同样的设置都可以写进 ~/.config/mcp-terminal/config.toml,带注释的完整示例见 config.example.toml。termctl sandbox-profile --cwd <dir> 可以打印实际生成的 Seatbelt 配置。服务自身的状态目录(~/.local/state/mcp-terminal)和配置目录在沙箱内不可写、关键部分不可读,Agent 无法篡改审批记录、审计日志或给自己加规则。

在沙箱里,写 home 目录下的文件会得到 Operation not permitted,这是预期行为。zsh 的补全缓存和 oh-my-zsh 缓存已被重定向到可写的用户缓存目录;vim 的 .viminfo 等写不了的文件会提示错误但不影响编辑。

工具一览

所有工具都同时返回一段便于模型阅读的文本(首行是 [session · 状态 · 退出码] 摘要,末行是下一步建议)和对应的 structuredContent。出错时返回 isError: true,structuredContent.error 含 code、message 和 hint。

工具

用途

关键参数

terminal_run

一次性命令,等待结束并返回 stdout / stderr / 退出码;超过 wait_ms 仍未结束则返回 session_id 以便后续读取

command, cwd, stdin, timeout_s, wait_ms, max_bytes

terminal_create

创建持久会话:交互 shell(默认),或直接运行程序(program + args,如 python3、node、npm run dev)

program, args, shell, mode(pty/pipe), name, cwd, env, cols, rows, idle_ttl_s

terminal_exec

在 shell 会话中执行一条命令,返回这条命令的输出、退出码和新的 cwd;超时返回 running

session_id, command, wait_ms, max_bytes, view

terminal_send

向会话写入文本和按键,返回随后产生的输出

session_id, text, keys, submit, wait_ms, view

terminal_read

按游标增量读取输出,可在服务端等待条件

session_id, cursor, until, wait_ms, max_bytes, view(text/raw/screen), stream, tail_lines

terminal_status

会话状态与活动判断(idle / busy / awaiting_input / exited)、前台进程、未读字节数

session_id

terminal_list

列出会话

state(running/all)

terminal_signal

向前台作业或整个会话发信号

session_id, signal, target

terminal_resize

调整 PTY 尺寸

session_id, cols, rows

terminal_close

结束会话及其整棵进程树,可保留输出供之后读取

session_id, grace_ms, keep_output

所有会执行命令或写入输入的工具都接受 approval_id,用于审批后的重试。

典型调用编排

一次性命令:

terminal_run {"command": "git status --short && npm test"}
→ [term_x · exited · exit 0 · 8.2s] …输出…

多步工作(状态会保留):

terminal_create {"name": "main"}
terminal_exec {"session_id": "main", "command": "cd api && source .venv/bin/activate"}
terminal_exec {"session_id": "main", "command": "pytest -q"}

长任务:先短等,再跟随输出直到结束,期间不会丢输出:

terminal_exec {"session_id": "main", "command": "npm run build", "wait_ms": 3000}
→ status: running
terminal_read {"session_id": "main", "until": ["command_done"], "wait_ms": 30000}

开发服务器:放在独立会话里,等到就绪行出现:

terminal_create {"name": "dev", "program": "npm", "args": ["run", "dev"]}
terminal_read {"session_id": "dev", "until": ["pattern:Local:|listening on|ready in"], "wait_ms": 30000}

交互程序:

terminal_exec {"session_id": "main", "command": "npm init"}
→ status: awaiting_input, "package name: (demo)"
terminal_send {"session_id": "main", "text": "my-pkg", "submit": true}
terminal_send {"session_id": "main", "keys": ["C-c"]}      # 中断

全屏程序:

terminal_exec {"session_id": "main", "command": "vim notes.txt"}      # 自动返回屏幕快照
terminal_send {"session_id": "main", "text": "ihello", "keys": ["Escape"]}
terminal_send {"session_id": "main", "text": ":wq", "submit": true}

安全模型

三道防线,彼此独立:OS 沙箱限制“能碰到什么”,命令策略限制“能直接说什么”,审批把高风险操作交给人决定。沙箱是硬边界;策略解析是尽力而为(无法覆盖脚本内部的行为),所以默认两者同时开启。

内置规则(termctl check "<命令>" 可查看任意命令的判定与原因):sudo/su/doas、格式化磁盘、dd of=/dev/…、关机、csrutil/nvram、rm -rf 关键路径(/、~、/System……)以及 fork bomb 为 deny;rm -rf <目录>、find -delete、git push --force、git reset --hard、git clean -f、curl … | sh、chmod -R、killall、npm publish、docker system prune、kubectl delete、launchctl load、读取钥匙串密码、osascript 以及无法静态判定的动态命令为 ask。用户规则可以把 ask 放宽为 allow,也可以新增 ask / deny;内置 deny 是底线,规则无法放宽。项目目录里的 .mcp-terminal.toml 只能让策略更严格。

审批流程(security.approval.mode,默认 auto):

  1. 客户端声明支持 elicitation 时,直接在客户端界面里弹出审批表单(允许一次 / 总是允许 / 拒绝)。

  2. 否则弹出 macOS 原生对话框,默认按钮是“拒绝”,超时(默认 60 秒)不算同意。

  3. 都不可用或超时时,返回 APPROVAL_REQUIRED 和一个审批 ID,Agent 会请你在自己的终端里运行 termctl approve apr_xxx,然后带 approval_id 重试。审批只对完全相同的参数有效、只能用一次、默认 5 分钟过期。

“总是允许”会把这条精确命令(限定在该目录)写入 ~/.config/mcp-terminal/rules.toml,可随时编辑或删除。

审计:每次工具调用、策略判定、审批、会话开始与结束都写入 ~/.local/state/mcp-terminal/audit/*.jsonl。termctl audit tail 查看最近记录,termctl audit verify 校验哈希链。

生命周期与恢复

会话在空闲超过 idle_ttl_s(默认 1 小时)后自动关闭;结束的会话输出保留 retain_after_exit_s(默认 30 分钟)供读取。全局内存超出预算时,最久没有活动的会话先缩减内存缓冲,被淘汰的输出仍可从磁盘溢写日志中读取。客户端关闭 stdin、发送 SIGTERM/SIGINT/SIGHUP,或父进程消失(被 launchd 接管)时,服务会终止全部会话后退出。

如果服务被强制杀死,PTY 会话中的普通进程会随 PTY 关闭收到 SIGHUP 自行退出;pipe 会话和 nohup 后台进程会被下一次启动的服务根据注册表中记录的进程组(校验进程启动时间,避免误杀 PID 复用的进程)找回并清理,会话状态标记为 lost。也可以随时手动运行 termctl cleanup。

termctl

termctl approvals                 列出待审批请求
termctl approve <id> [--always]   批准(--always:记住这条命令)
termctl deny <id>                 拒绝
termctl sessions [--all]          注册表中的会话
termctl cleanup                   清理已失去服务端的会话进程
termctl check "<cmd>" [--cwd d]   查看一条命令的策略判定
termctl sandbox-profile [--cwd d] 打印生成的 Seatbelt 配置
termctl audit verify | tail [-n N]
termctl config                    生效配置及其来源
termctl doctor                    环境自检
termctl client-config [--workspace d]  生成 MCP 客户端配置片段

测试

npm test                 # 构建后运行单元测试与集成测试
npx vitest run test/unit # 只跑单元测试

集成测试会通过 stdio 启动真实服务,覆盖:退出码与 stderr 分离、shell 状态保持、等待输入检测与应答、长任务游标续读、正则就绪等待、C-c 中断、沙箱写入限制、工作目录根限制、策略拒绝与审批票据、密钥脱敏、关闭时清理整棵进程树(含 nohup)、以及 2026-07-28 协议下的 elicitation 审批。

已知限制

仅支持 macOS。sandbox-exec 已被苹果标记为弃用但在当前系统上仍可用(Codex CLI、Claude Code 的 macOS 沙箱同样基于它);若未来不可用,termctl doctor 会报告,可暂时用 --profile host 运行。沙箱内的网络只有开关和仅本机两档,没有按域名过滤。命令策略是静态分析,无法判断脚本文件内部会做什么,这部分由沙箱兜底。等待输入检测对没有提示符、也不阻塞在 tty 读的程序可能判断为 busy,此时可用 until=["idle:2000"]。登录 shell 环境在启动时解析一次(oh-my-zsh 等较慢的配置约 2 秒),第一次创建会话会等待它完成。

目录结构

src/
  bin/            mcp-terminal(stdio 服务入口)、termctl(管理 CLI)
  server.ts       运行时装配与 MCP Server 构建
  mcp/            工具注册与渲染、审批守卫(elicitation / 对话框 / 票据)
  core/           会话、管理器、等待器、输出视图、按键映射、环境变量
  backends/       PTY 与 pipe 进程后端
  detect/         shell 集成脚本生成、OSC 133/7 标记扫描
  output/         环形缓冲、磁盘溢写、文本规范化、屏幕模型、脱敏
  policy/         命令解析、策略引擎、审批存储、macOS 对话框
  sandbox/        Seatbelt 配置生成
  store/          SQLite 会话注册表、哈希链审计日志
  config/         配置结构与分层加载
test/             单元测试与集成测试
examples/         测试用的交互脚本(greet.py)
docs/             技术选型报告与初步设计

设计背景与各关键节点的选型对比见 docs/tech-selection-report.md,Manus 中的分步测试见 TESTING.md。

许可证

MIT © dogeheng

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides persistent interactive shell sessions via pseudo-terminals for MCP agents, enabling bidirectional communication, incremental reads, and stateful command execution across steps.
    7 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to interact with real terminal sessions across local and remote hosts, driving multi-turn interactive programs like TUIs, REPLs, and SSH prompts through MCP.
    57
    MIT