内容
# terminal-mcp
**English** | [简体中文](README.zh-CN.md)
[](https://pkg.go.dev/github.com/fzxbl/terminal-mcp)
[](https://go.dev/)
[](https://modelcontextprotocol.io)
[](LICENSE)
[](https://github.com/fzxbl/terminal-mcp/stargazers)
**一款适用于 AI 代理的真实 PTY 终端,具有完全的可观察性和无缝的人类接管能力。**
`terminal-mcp` 是一个 [Model Context Protocol](https://modelcontextprotocol.io) 服务器,为 LLM 代理提供了一个**真实的 PTY 终端**(而非一次性的 `exec` 管道)。人类可以**实时观察代理的每一步操作**,**在代理走错路时立即接管同一个会话**,然后代理**在人类完成操作后继续执行,具有完整的上下文信息**。本地 shell、`ssh`、容器、`gdb`、`python`、`top`、`vim` — 代理像人类一样驱动它们,而你始终不会被锁定在外。
> 一个终端,由人类和代理共享。代理负责操作,人类实时观察,需要时接管,然后将控制权交回 — 中间没有任何间断。
使用 Go 构建,单个二进制文件,官方 MCP SDK,支持 Streamable HTTP。可与 Cursor、Claude、Comate 或任何 MCP 客户端集成。
---
## 为什么选择 terminal-mcp
大多数“运行命令”工具为模型提供一个无状态的管道:一个命令输入,一个输出块。它在交互式操作(登录、REPL、长时间运行的会话)上会失败,并且更糟糕的是,它**不允许人类观察正在发生的事情或在代理出错时进行干预**。
terminal-mcp 围绕以下五个方面构建:
- **1. 真实的 PTY,而非管道。** 一个具有持久会话的真正伪终端。交互式和全屏程序(`gdb`、`python`、`mysql`、`top`、`vim`)以及行编辑、颜色、提示和控制键 — 所有这些都像人类操作一样。不是一次性的 `exec` 操作。
- **2. 完全的可观察性 — 实时观察代理的操作。** 每个会话都有一个实时的 Web 终端 URL。打开它,您可以实时看到代理产生的每个按键和字节 — 代理看到的屏幕一样。无需猜测代理做了什么。
- **3. 即时人类接管 — 在错误发生时立即纠正。** 看到代理走错路了吗?点击 *接管* 并直接输入正在运行的 PTY。代理的写入操作会立即暂停;您可以通过手动操作解决问题 — 输入密码、运行正确的命令或退出错误的操作。
- **4. 无缝恢复 — 代理在您完成操作后继续执行,具有完整的上下文信息。** 当您释放控制权时,您输入的所有内容都会以 `[rc=n] $ 命令`的形式反馈给代理,包含输出,以便它继续执行**,并完全了解您刚刚做了什么** — 无需重新解释,也不会丢失状态。
- **5. 代理周围有一道防护栏 — 透明的资源限制,无法逃脱 shell 切换。** 配置一个模型不可见的 `ulimit`,在会话开始时注入,并在代理进入新 shell 时**重新注入**(`ssh` 远程 / `su` / 容器)。硬限制被子进程继承,因此非特权命令无法提高它 — 代理无法通过切换 shell 或运行其他命令来逃避限制。
此外,还有一些使上述功能可靠的机制:
- **精确的命令边界。** 一个哨兵提示告诉服务器一个命令何时完成及其退出代码 — 代理永远不会将“仍在运行中”误认为是“已完成”。
- **在 shell 切换时存活。** `ssh`、`su`、`docker exec`、进入容器 — 哨兵会自动重新启用,以便在切换时继续跟踪。
- **LLM 友好的输出。** 删除 ANSI 转义序列,延迟部分转义序列 — 模型永远不会接收损坏的字节。会话日志是磁盘上的一个只追加文件(真实来源),具有有界的内存尾部缓存,因此失控的 `cat`/`yes` 不会占用内存;超大结果作为日志范围引用返回,并按需分页。
- **本地和远程。** `mode=local` 在主机上运行 shell/命令;`mode=ssh` 连接到远程主机。
- **审计和重放。** 每个工具调用都记录为 JSON(调用者 IP、用户、工具、参数、结果);每个会话的原始字节流都保存以供重放,并具有可配置的保留期。
---
## 快速开始
需要 Go 1.25+。
```bash
go install github.com/fzxbl/terminal-mcp/cmd/terminal-mcp@latest
terminal-mcp --listen 127.0.0.1:8900
```
MCP 端点位于 `/mcp`;会话的 Web 终端位于 `/debug/terminal/<session_id>`。
## 在您的 AI 编码工具中使用它
terminal-mcp 通过 **Streamable HTTP** 与 MCP 进行通信。将您的客户端指向 `http://127.0.0.1:8900/mcp`。
通用 MCP 客户端配置 (`mcp.json`):
```jsonc
{
"mcpServers": {
"terminal": { "url": "http://127.0.0.1:8900/mcp" }
}
}
```
然后只需询问您的代理:
> “打开一个终端,ssh 到 staging,tail 服务日志,并告诉我为什么它会报错 500。”
代理打开一个会话,运行命令,并流式传输结果。如果它需要密码或您想介入,请打开返回的 `terminal_url` 并接管 — 代理等待、观察并继续执行。
## 人类接管,20 秒内
1. 代理调用 `terminal_open` → 获取一个 `terminal_url`。
2. 您在浏览器中打开它,点击 **接管**。
3. 会话报告 `held=true`;代理的写入操作暂停。
4. 您输入(输入密码、运行风险命令、浏览)。代理看到您输入的每个命令,并将其重建为 `[rc=n] $ 命令`。
5. 您点击 **释放**。代理在您离开的地方继续执行。
## 工具
| 工具 | 功能 |
| --- | --- |
| `terminal_open(mode, command?, host?)` | 启动一个持久的 PTY 会话。`mode=local` 或 `mode=ssh`。返回 `session_id` + `terminal_url`。 |
| `terminal_send(session_id, input, wait_ms?)` | 输入一个命令,等待它完成。返回输出、状态、退出代码。 |
| `terminal_read(session_id, wait_ms?, mode?, from?, to?)` | 观察输出。`tail`(peek)、`since_last`(完整增量;也是人类接管命令的记录)或 `range`(分页绝对日志字节窗口 `[from,to)`);使用返回的 `range`(在截断结果中)。 |
| `terminal_control(session_id, key)` | 发送控制键(`ctrl-c`、`ctrl-d`、`ctrl-z`,...)或恢复操作(`flush`、`hard`、`rearm`)。 |
| `terminal_status(session_id)` | 查询轻量级状态 / 提示 / 退出代码 / 保持状态。 |
| `terminal_close(session_id)` | 关闭会话,回收进程组。 |
| `terminal_list()` | 列出活跃的会话。 |
## 配置
复制 `config.example.toml`。重点:`listen_addr`、`data_dir`、`default_shell`、`ssh_user`、`ssh_opts`、`shell_switch_commands`(触发自动重新加载的命令 — 添加您自己的,例如容器进入命令)、`auto_rearm`、`max_buffer_bytes`(内存尾部缓存上限;完整日志存储在磁盘上)、`exec_output_max_bytes`(每个调用返回上限;较大结果返回为分页范围)、`transcript_retention_days`、`log_dir` / `log_rotate` / `log_max_age_days`。
**透明的资源防护栏(`resource_limit_cmd`):** 在会话开始时与哨兵一起注入的模型不可见的 `ulimit`,并在每次代理进入新 shell 时**重新注入**(`ssh`/`su`/`docker exec`/`matrix_jail` ...)和硬重置时。没有 `-S/-H` 的 `ulimit` 设置软限制和硬限制;硬限制被子进程继承,不能被非特权命令提高,因此切换 shell 或运行其他命令无法逃避限制:
```toml
resource_limit_cmd = "ulimit -v 4194304; ulimit -t 600; ulimit -u 4096"
```
> 界限:`ulimit` 无法限制提升到 root 并提高其自身硬限制的进程,或由守护进程启动且不继承此会话的进程树。对于本地进程树的权限无关的上限,请使用 cgroup v2。
**自定义工具描述:** 使用 `[tool_descriptions]` 覆盖每个工具的模型可见描述(在将工具嵌入外部 MCP 并希望使用自己的措辞时很有用;缺少/空条目保留内置默认值):
```toml
[tool_descriptions]
terminal_open = "打开一个持久的终端会话;返回 session_id 和只读 Web 终端 URL。"
terminal_send = "向会话中输入一个命令并等待其完成;返回新的输出和状态。"
```
## 将其嵌入为库(共享一个 MCP 服务器)
已经运行了一个 MCP 服务器并希望在同一个 `/mcp` 上添加终端工具?`go get github.com/fzxbl/terminal-mcp` 并注册到您自己的官方 SDK 服务器:
```go
mcpserver.Init("config.toml")
mcpserver.SetAdvertiseAddr("10.0.0.5:8080") // 用于构建 terminal_url 的主机:端口
mcpserver.SetToolDescriptions(map[string]string{ // 可选:重新定义工具描述
"terminal_open": "打开一个持久的终端会话;返回 session_id 和 Web 终端 URL。",
})
mcpserver.StartIdleGC(ctx)
mcpserver.RegisterTools(server, auditWriter) // terminal_* 工具
mux.Handle("/debug/terminal/", mcpserver.TerminalHandler()) // Web 终端(人类接管)
```
`/mcp` 路径您可以自行选择;Web 终端前缀 `/debug/terminal/` 是固定的。工具描述也可以通过 `[tool_descriptions]` 配置表覆盖(优先级:`SetToolDescriptions` > 配置 > 内置默认值)。请参阅 **[docs/EMBEDDING.md](docs/EMBEDDING.md)** 获取完整的公共 API 参考、共享 `/mcp` 集成模式、路径约定、授权说明和配置参考。
## 安全
**HTTP 服务(MCP 端点 + Web 终端)默认情况下是未经身份验证的。** 调用这些工具等同于主机上的 shell 访问(或通过 ssh)。将其绑定到环回地址,或在将其暴露于受信任网络之外之前,请使用身份验证反向代理。
## 许可证
MIT。欢迎贡献和星级。