Skip to main content
Glama
MCP Terminal Controller

MCP Terminal Controller

by Quinnverse

MCP Terminal Controller

MCP Terminal Controller 是一个通过 MCP stdio 提供受限、异步 POSIX 进程会话的本地服务器。它使用官方 MCP SDK 2.3.0,要求 Python >= 3.11。客户端只能调用配置所有者(Owner)预先批准的 command_id;不能提交 argv、shell 命令、重定向或命令替换。

这是本地进程控制器,不是沙箱,不提供远程访问、Web/云端 UI,也不能在服务重启后恢复会话。

当前状态

  • 项目版本:0.1.0。

  • 默认 profile 和 demo_process.py 启动的都是真实 OS 子进程,不是模拟器或内存假实现;它们仅用于可控的本地演示与测试。

  • 2026-10-22 实测:本机全量 83 passed,新目录/新虚拟环境复现 83 passed;标准 MCP 客户端独立完成 16 个场景,安装后的 console entry 也完成真实闭环。完整范围与证据见 ACCEPTANCE_REPORT.md。

  • Manus 初始连接已实测创建、交互输入、游标续读和终止;加载最终修复后的重连返回 transport closed,原因 UNKNOWN。最终版本以官方标准客户端验收,不声称 Manus 最终重载成功。

Related MCP server: ptywright

工作方式与边界

  1. 服务进程只使用标准输入/输出(stdio)承载 MCP 协议。不得向服务的 stdout 写普通日志或业务文本;入口将日志写入 stderr。

  2. terminal_start 接收固定 profile 的 command_id,由 Owner 本地 JSON 配置映射为固定、真实的 argv(其中可执行项 argv[0] 必须是绝对路径)。服务使用 asyncio.create_subprocess_exec(),不启动 shell。

  3. 每个会话分别连接子进程的 stdin、stdout、stderr。它们是 pipe,不是 PTY(伪终端):没有终端模拟、TTY 行编辑、窗口尺寸、控制序列解释或 job control 语义。交互程序可能因为 isatty() 为假而表现不同。

  4. 输出以带 stream: "stdout" | "stderr" 标签的记录返回。两个 pipe 的跨流到达顺序由调度决定;请勿假定 stdout 与 stderr 有全局行序。

  5. 输出 buffer、会话记录和子进程状态都只在内存中保存。服务器重启、崩溃或被强制杀死后没有会话重新连接、历史回放或孤儿进程恢复机制。

安装

在项目根目录选择一种方式安装测试额外依赖:

uv sync --locked --extra test
source .venv/bin/activate

或:

python -m pip install '.[test]'

上述依赖声明包括 mcp==2.3.0;项目要求 Python >=3.11。仅支持具有 POSIX 进程组的系统;非 POSIX 平台会拒绝配置。

生成 Owner 本地配置

仓库提供 scripts/make_config.py,它调用 Config.defaults(),把 dataclass 字段导出为 JSON。生成内容中的工作目录、允许根目录、Python 可执行文件和演示脚本路径均为本机绝对路径,不含 ~ 或相对路径。

python scripts/make_config.py \
  --root /绝对/允许/工作目录 \
  --output /绝对/路径/mcp-terminal-controller.json

选项:

选项

含义

--output 路径

必填;要写入的本地 Owner JSON 文件。父目录不存在时会创建。

--root 根目录

可选;默认是运行脚本时的当前工作目录(cwd)。该目录必须存在,且将成为默认工作目录和唯一允许根。

该脚本只生成本机的 Owner 配置;它不能、也不会开放客户端任意命令执行。生成的默认 stream、interactive 等 profile 仍然是指向本地 demo_process.py 的固定真实子进程。生产 Owner 应审核并按需要替换为最小权限的固定 profile,而不是把用户输入拼进 argv。

配置格式与校验

启动时传入的 JSON 只能含 Config 的 dataclass 字段;未知字段会被拒绝。所有 allowed_roots 和 default_cwd 必须是存在的绝对目录,实际使用时会解析符号链接,并要求 cwd 位于至少一个允许根内。

{
  "allowed_roots": ["/absolute/allowed/root"],
  "default_cwd": "/absolute/allowed/root",
  "commands": {
    "approved-job": {
      "argv": ["/absolute/path/to/executable", "--fixed-option"],
      "allowed_env": ["SAFE_LABEL"]
    }
  },
  "max_sessions": 8,
  "max_records": 32,
  "buffer_bytes": 1048576,
  "buffer_records": 65536,
  "default_timeout_ms": 30000,
  "max_timeout_ms": 300000,
  "retention_seconds": 300,
  "max_input_bytes": 65536
}

commands 中的每个 profile 必须有非空、固定的 argv,且 argv[0] 必须是绝对路径。command_id 最长 128 个字符;最多 128 个 profile。参数含义与合法范围如下。

字段

默认值

合法范围/规则

allowed_roots

无

至少一个存在的目录;客户端 cwd 只能在其内。

default_cwd

生成时的 root

存在且位于允许根内。

commands

默认演示 profiles

字典;每项为固定 argv 与可选 allowed_env。

max_sessions

8

1–64 个同时未结束会话。

max_records

32

1–1024 个保留会话记录;不得小于 max_sessions。

buffer_bytes

1,048,576

4–16,777,216,单会话保留输出的 UTF-8 字节上限。

buffer_records

65,536

1–1,048,576,单会话保留输出记录上限。

default_timeout_ms

30,000

100–3,600,000;不得大于 max_timeout_ms。

max_timeout_ms

300,000

100–3,600,000。

retention_seconds

300

1–86,400;最终状态会话记录的 TTL。

max_input_bytes

65,536

1–1,048,576;一次 terminal_write 的 UTF-8 字节上限。

子进程环境从固定的 PATH=/usr/bin:/bin、LANG=C.UTF-8、LC_ALL=C.UTF-8 开始,不会继承服务进程的 os.environ。客户端最多传 32 个环境变量,只有 profile allowed_env 中的名称才可覆写;名称还会拒绝 PYTHON*、LD_*、DYLD_*、PATH、HOME、SHELL、ENV、BASH_ENV、IFS、CDPATH 以及包含 TOKEN、SECRET、PASSWORD、CREDENTIAL、API_KEY、PRIVATE_KEY 的名称。另外拒绝 NODE_OPTIONS、PERL5OPT、RUBYOPT、GCONV_PATH、Java 启动选项等已知运行时注入变量。其他允许名称仍需 Owner 审核业务语义;黑名单不是通用安全保证。

启动与客户端配置

先激活已安装本项目的环境,生成或审核 Owner JSON,然后启动:

python -m mcp_terminal stdio --config /绝对/路径/mcp-terminal-controller.json

省略 --config 会使用当前目录作为允许根的安全默认演示 profiles;生产使用建议显式提供 Owner 审核过的配置。stdio 是唯一 transport 参数。

以下是标准 mcpServers 配置形状示例。请用本机的绝对 Python 路径和配置路径替换占位符;示例不会、也不应修改用户现有客户端配置文件。

{
  "mcpServers": {
    "terminal-controller": {
      "command": "/absolute/path/to/python",
      "args": [
        "-m",
        "mcp_terminal",
        "stdio",
        "--config",
        "/absolute/path/to/mcp-terminal-controller.json"
      ]
    }
  }
}

客户端通过 MCP 的 tools/list 发现工具,并通过协议的 JSON 请求调用它们;不要把交互式 shell 文本写到服务 stdin。若客户端启动器需要环境变量,应由客户端自行管理,不要用它替代 Owner JSON 的命令批准策略。

五个 MCP 工具

所有应用错误均使用 {"ok": false, "error": {"code": "…", "message": "…"}} 的 MCP tool error 返回;原始 OS 异常文本不会返回给客户端。工具 调用入口拒绝未知参数,且整数为严格整数(布尔值和数字字符串不被当作整数接受)。

工具

参数(默认值)

行为

terminal_start

command_id(必填字符串,1–128);cwd(可选字符串);env(可选字符串字典);timeout_ms(可选严格整数,100–3,600,000,且受配置上限约束)

启动 Owner 批准的固定 profile 并立即返回会话快照。没有 argv 参数,也不接受 shell 语法。cwd 必须在允许根内;环境覆写须获该 profile 批准。

terminal_read

session_id(必填);cursor=0(0–2^63−1);max_bytes=65536(4–65536);wait_ms=0(0–2000)

从绝对 cursor 非消费式读取输出。无新记录且会话仍运行时,可最多等待 wait_ms。

terminal_write

session_id(必填);text(必填字符串);close_stdin=false(严格布尔值)

将精确 UTF-8 文本写入运行中的子进程。需要换行时由客户端自行在 text 中加入。可在写入后关闭 stdin。

terminal_status

session_id(必填)

查询会话状态、真实退出码/信号、终止原因、输出可用性和 cursors。

terminal_terminate

session_id(必填);grace_ms=500(0–2000)

幂等地终止该会话进程组:发送 SIGTERM、等待宽限期、必要时 SIGKILL,并等待最终状态确认。

启动、状态和终止快照包含 session_id、command_id、process_id、status、exit_code、exit_signal、termination_reason、stdin_closed、input_uncertain、base_cursor、next_cursor、created_at、last_activity_at、output_complete、output_incomplete。terminal_read 返回 records、base_cursor、next_cursor、truncated、has_more,以及当前 status 和输出完成标志。

推荐读取流程

  1. 调用 terminal_start,保存返回的 session_id,初始读取 cursor 使用 0,避免跳过启动期间已生成的输出。

  2. 调用 terminal_read(session_id, cursor=上次 next_cursor);处理每一条 {cursor, stream, text} 记录。

  3. 始终将下一次 cursor 更新为响应的 next_cursor。当 has_more=true 时立即继续读取;运行中无输出时可使用有限的 wait_ms。

  4. 用 terminal_status 或 read 响应的 output_complete 判断尾部输出是否已完全排空。进程退出并不等于 pipe 尾部已经读取完毕。

  5. 需要中止时调用 terminal_terminate;最终记录仍会在 TTL 内可读。

Cursor、Unicode 与截断恢复

Cursor 是绝对输出记录序号,不是字节偏移,也不是行号。buffer 将文本拆成 Python Unicode 码点,每个记录都保留 stdout 或 stderr 标签。max_bytes 约束所返回文本的 UTF-8 字节总量,不是 JSON 线长。每页还固定最多返回 1,024 条记录,达到上限时 has_more=true;它不改变 cursor 的计数单位。

buffer 达到 buffer_bytes 或 buffer_records 上限时,会淘汰最旧记录,并将 truncated 设为 true。如果客户端请求的 cursor 小于当前 base_cursor,服务返回 CURSOR_EXPIRED 和可恢复的 base_cursor,而不是静默丢失数据。此时表示已经出现不可见输出缺口;客户端应记录该缺口,将 cursor 移至错误中给出的 base_cursor,再继续读取。cursor 大于 next_cursor 则为 INVALID_CURSOR。

从子进程 pipe 读取时采用增量 UTF-8 解码;无效字节以替换字符表示。输出 reader 被取消、失败或在尾部排空期限内未完成时,output_incomplete=true,此时不要把已收到输出当作完整审计记录。

写入确认与背压

terminal_write 成功的 accepted_bytes 仅代表数据已被控制器接受并完成 pipe drain();它不是目标应用已经读取、解析或处理输入的确认。如需应用级确认,应由固定 profile 的协议通过 stdout/stderr 显式返回。

单次写入受 max_input_bytes 限制。若 drain 在 1 秒内无法完成,服务会关闭该会话 stdin 并返回 INPUT_BACKPRESSURE;此时字节可能已部分送达,不得盲目重试同一输入。进程关闭输入时会返回 STDIN_CLOSED;结束会话写入则为 SESSION_NOT_RUNNING。

写入排队后若请求被取消,已发送字节不能撤回;控制器关闭 stdin,并在状态中设置 input_uncertain=true。同一会话再次写入会被拒绝,客户端不得把取消视为“从未写入”,也不能跨新会话盲目重放。

资源配额、超时与生命周期

整个实例所有保留会话的 max_records × buffer_records 不得超过 2,097,152 条,max_records × buffer_bytes 不得超过 67,108,864 字节;超额配置被拒绝。这限制对象数与文本容量,不是操作系统 RSS/CPU 的硬隔离。默认 32 个保留会话。MCP 输入帧解析前限制为 1 MiB(含换行),实际 text/env 仍受更低的配置或字节限制。

  • 同时运行会话达到 max_sessions 时,新的启动返回 SESSION_LIMIT。

  • 已保留会话(包括最终状态但尚未到 TTL 的会话)达到 max_records 时,新的启动返回 RECORD_LIMIT;服务不会为腾出空间而静默驱逐仍承诺保留的最终输出。

  • 会话到达 timeout_ms 后,控制器按超时原因终止它;默认超时和最大可选超时由 Owner 配置决定。

  • 完成后,会话输出和状态保存 retention_seconds。TTL 到期后记录被移除,随后查询会得到 SESSION_NOT_FOUND。TTL 是最终记录的保留时间,不是进程运行时限。

  • 最终状态为 EXITED、TERMINATED 或 FAILED;运行中可见 RUNNING,终止过程中可见 TERMINATING。自然退出的原因是 natural;用户终止、超时、正常服务关闭分别会标为 user、timeout、shutdown。

进程组清理与其边界

每个 profile 通过 start_new_session=True 启动,POSIX 会为它建立新 session/进程组(setsid 语义)。控制器以子进程 leader PID 为进程组 ID,使用 killpg() 向该组发信号:用户终止/超时/关闭时先 SIGTERM,等待所选宽限期后才 SIGKILL;若 leader 自然结束,仍会对其原组发送 SIGKILL,以避免已知同组后代继续持有 pipe 或存活。

这是有限的进程组清理,不是容器隔离:

  • 只有仍在这个进程组内的后代会被处理。主动 setsid()、另建进程组、委托给守护进程/服务管理器或跨越其他权限边界的工作可能逃离,控制器不保证回收它们。

  • SIGKILL 不可捕获,目标应用没有机会执行优雅清理、刷新文件或发送自己的完成确认;因此可能产生不完整输出或外部副作用。

  • 无法向进程组发信号、退出无法在确认期限内完成或服务遭到非正常杀死时,不能承诺清理成功。服务正常关闭会尝试终止已知会话,但不存在重启后的恢复/补救。

如果业务需要强隔离、网络限制、文件系统隔离、资源控制或可恢复编排,请在本控制器之外使用操作系统账户、容器/VM、服务管理器和相应审计机制。

默认演示 profiles

默认配置包含以下固定 profile:stream、interactive、long、unicode、flood、exit7、empty、children、closed-stdin、split-utf8、invalid-utf8、eof、ignore-term。它们均由真实 Python 子进程运行 demo_process.py:例如 interactive 读取并回显输入,flood 产生足以触发 buffer 截断的输出,children 建立同进程组后代,ignore-term 用于验证 TERM→KILL 路径。它们不是生产命令批准清单。

测试与独立 MCP 验证

测试与验证脚本是两条独立命令,请分开运行。随包 evidence 保存已执行结果;这不替代你在目标环境的复现。

pytest
# 仅真实 wire-level 测试:pytest -m e2e

pytest 会发现 tests/。端到端测试会通过官方 MCP stdio client 启动新服务器子进程;其他测试还覆盖会话和输出 buffer 行为。

独立证据脚本不在进程内调用 SessionManager,而是使用官方 StdioServerParameters 和 ClientSession 启动实际 python -m mcp_terminal stdio,从 wire protocol 记录响应。示例:

python scripts/verify_mcp.py --output /tmp/mcp-evidence.json

可用 --scenario 名称 重复选择单个或多个场景;省略时运行全部。--output 对验证脚本是必填项,并且无论成功或失败都会写出 JSON 证据;失败时脚本以非零状态退出。该脚本使用其自身路径定位项目,可从无关当前目录运行。运行前请确认依赖已安装,并自行检查 JSON 中的 success、failure、断言和调用记录。

安全说明

部署、配置所有权、威胁模型和漏洞报告方式见 SECURITY.md。最重要的原则是:仅让可信 Owner 写配置,仅批准确有必要的固定可执行文件与根目录,并将本服务视为在其运行用户权限下执行真实本地代码的接口。

SDK 兼容说明与来源

官方 SDK 2.3.0 的默认 stdio 文件读取在 stdin 保持打开时可能阻塞 SIGTERM 停机。本项目 transport.py 用 asyncio fd readiness 提供可取消的文件适配器,仍由官方 stdio_server 解析、序列化及处理协议。该模块集中使用 SDK _lowlevel_server 入口;另在不完整输出清理时使用 asyncio Process 的私有 transport 关闭句柄。它们是已记录的兼容风险,升级 SDK/Python 必须重跑真实协议与停机测试。支持 pipe,未实现 PTY、网络服务或重启恢复。

依赖包含 uv.lock 及带哈希的 requirements.lock / requirements-test.lock。需要 pip 哈希锁定时,在新虚拟环境中安装 python -m pip install --require-hashes -r requirements-test.lock,再执行 python -m pip install --no-deps .。不要把示例绝对路径直接复制到另一台机器;用 make_config.py 重新生成。

Available Tools

5 tools
terminal_readA
Read-onlyIdempotent

Read non-consuming output records starting at cursor, with a bounded optional wait.

Cursor is an absolute record sequence, not a byte index. Continue with next_cursor. Old cursors return CURSOR_EXPIRED with base_cursor, never silent loss. stdout/stderr retain stream labels. output_complete only becomes true after tail output is drained.

Inputs: session_id (required), cursor, wait_ms, max_bytes. Returns: object

terminal_startB

Start a real approved process and immediately return its persistent session ID.

Default profiles: stream, interactive, long, unicode, flood, exit7, empty, children, closed-stdin, split-utf8, invalid-utf8, eof, ignore-term. Owner config replaces profiles. No client argv or shell substitution is accepted. cwd must remain under approved roots.

Inputs: command_id (required), cwd, env, timeout_ms. Returns: object

terminal_statusB
Read-onlyIdempotent

Query lifecycle, real exit code/signal, termination reason and output availability.

Inputs: session_id (required). Returns: object

terminal_terminateA
DestructiveIdempotent

Idempotently terminate the session process group: TERM, grace, KILL, confirm exit.

Final output and status remain readable for the configured retention interval.

Inputs: session_id (required), grace_ms. Returns: object

terminal_writeA

Write exact UTF-8 text to the running process; append newline yourself if needed.

Optionally close stdin after writing. accepted_bytes means transport acceptance, not application acknowledgement. Never retry blindly after INPUT_BACKPRESSURE.

Inputs: text (required), session_id (required), close_stdin. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4/5.0

    Scored across 5 tools

    Disambiguation5/5

    Each tool maps to a distinct lifecycle stage: start, write, read, status, terminate. The only near-overlap is status reporting 'output availability' versus read returning output, but the descriptions make clear status is metadata-only while read consumes records from a cursor.

    Naming Consistency5/5

    All five tools share a uniform terminal_ prefix with a single-word verb suffix (start, write, read, status, terminate). Fully predictable and consistent.

    Tool Count5/5

    Five tools is exactly the minimal complete set for process session control, with no redundant operations. Each tool earns its place in the lifecycle.

    Completeness4/5

    Start/write/read/status/terminate covers the full session lifecycle including graceful and forced termination. Minor gaps exist for interactive/pty use: no terminal_resize and no dedicated signal-send tool (Ctrl-C must be emulated via write), and no session listing, but agents can work around these.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables MCP clients to spawn and control Codex CLI and Claude Code sessions on the host machine, with session management and filesystem access.
      4
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables management of long-running development processes (such as dev servers, compilers, and watchers) from MCP hosts. Provides tools to start, stop, restart, check status, view logs, and send input to managed processes.
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Enables launching and supervising local Codex and Claude Code agent sessions and one-shot tasks, exposing run, list, stop, and output operations over MCP with session-scoped permissions and sandbox controls.
      2
      264 npm
      1
      MIT