pty-session-mcp
v0.0.5
Published
本机 PTY 会话:浏览器直连 SDK + Node MCP/HTTP 服务。
Readme
pty-session-mcp
本机 PTY 会话服务:同一套 create / write / read / resize / kill / list,给浏览器走 HTTP + WebSocket SDK,给 Agent 走 MCP。
默认 npm 入口只打包浏览器客户端(fetch + WebSocket,约 5.6KB),不会把 node-pty、MCP SDK 打进前端。服务端从 pty-session-mcp/server 引入。
浏览器 (xterm.js + LocalTerminalClient)
│ HTTP / WebSocket
▼
pty-session-mcp serve ── PTY (bash / $SHELL)
│
└── /mcp 或 stdio ── Cursor / Claude要求 Node.js ≥ 20。启动 MCP 或 daemon 必须设置 LOCAL_TERMINAL_TOKEN,为空会直接退出。
安装
npm install pty-session-mcp本仓库(Rush):
rush update
rush build --to pty-session-mcp| 入口 | 给谁 | 内容 |
|------|------|------|
| pty-session-mcp / pty-session-mcp/client | 前端 | LocalTerminalClient |
| pty-session-mcp/server | Node | SessionManager、MCP、HTTP daemon |
| pty-session-mcp CLI | 进程 | 无参数 = stdio MCP;serve = HTTP/WS |
所有操作都走 options 对象。未识别字段会透传(zod .passthrough()),旧服务忽略、新字段以后再接。
快速开始
在跑终端的那台机器(或容器)上起 daemon:
LOCAL_TERMINAL_TOKEN=secret npx pty-session-mcp serve --host 0.0.0.0 --port 8080- 默认绑
0.0.0.0:7788。前端经过网关 / 预览域名访问时必须绑0.0.0.0,只绑127.0.0.1会得到connect error 111(ECONNREFUSED)。 - 前端
url填页面能访问到的地址(预览域名或http://127.0.0.1:8080),不要填容器内部 hostname 除非浏览器也能解析。 - HTTP 带
Authorization: Bearer <token>。浏览器 WebSocket 不能自定义 header,订阅流用?token=。
前端:xterm.js
这是主要用法。按键必须走原始输入,不要用 HTTP write 的默认 submit: true(那会给每个字符补 \n)。
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { LocalTerminalClient } from 'pty-session-mcp';
import '@xterm/xterm/css/xterm.css';
const term = new Terminal({ convertEol: true });
const fit = new FitAddon();
term.loadAddon(fit);
term.open(document.getElementById('terminal')!);
fit.fit();
const client = new LocalTerminalClient({
url: 'https://your-preview-host',
token: 'secret',
});
const session = await client.create({
cols: term.cols,
rows: term.rows,
// cwd 不传则默认 /home
});
const stream = client.subscribe(session.id);
term.onData((data) => stream.write(data));
term.onResize(({ cols, rows }) => stream.resize(cols, rows));
window.addEventListener('resize', () => fit.fit());
for await (const chunk of stream) {
term.write(chunk);
}
// 离开页面时
stream.close();
await client.kill({ id: session.id });要点:
create时传入当前term.cols/term.rows,否则 PTY 按 80×24 排版,和画面会对不齐。subscribe()连上会先回放环形缓冲里的已有输出(prompt 不会丢),再推增量。stream.write/stream.resize走同一条 WebSocket,不加换行。- 窗口变化:
FitAddon触发onResize→stream.resize,PTY 才会跟着变(vim / less / 换行才正确)。 for await在进程退出或stream.close()后结束。
不用 xterm、只发命令
import { LocalTerminalClient } from 'pty-session-mcp';
const client = new LocalTerminalClient({
url: 'http://127.0.0.1:8080',
token: 'secret',
});
const { id } = await client.create();
await client.write({ id, data: 'ls -la' }); // submit 默认 true,会补 \n
const out = await client.read({ id, waitMs: 2000 });
console.log(out.data);
await client.kill({ id });LocalTerminalClient 方法:create / write / read / resize / kill / list / subscribe。
subscribe(id) 返回:
| 成员 | 说明 |
|------|------|
| for await (const chunk of stream) | 服务端输出(含连上时的回放) |
| stream.write(data) | 原始输入,submit: false |
| stream.resize(cols, rows) | 通知 PTY 改尺寸 |
| stream.close() | 关掉 WebSocket,不杀进程 |
后端:MCP
Cursor / Claude(stdio)
{
"mcpServers": {
"pty-session": {
"command": "npx",
"args": ["-y", "pty-session-mcp"],
"env": {
"LOCAL_TERMINAL_TOKEN": "replace-with-a-long-random-secret"
}
}
}
}本仓库开发时可以把 command 换成 node,args 换成 ["libraries/local-terminal-mcp/dist/cli.js"]。
工具名与 HTTP 相同:create、write、read、resize、kill、list。Agent 发命令用 write(默认补换行);交互程序把 submit 设为 false。
进程内挂 MCP 或开 HTTP
import {
SessionManager,
createMcpServer,
startDirectServer,
startMcpStdio,
requireToken,
} from 'pty-session-mcp/server';
const token = requireToken();
const manager = new SessionManager({
cwdAllowList: ['/home'],
bufferSize: 1024 * 1024,
defaultCols: 80,
defaultRows: 24,
});
await startDirectServer(manager, { token, host: '0.0.0.0', port: 7788 });stdio MCP:
await startMcpStdio(manager, '0.0.3');serve 同时提供 REST、WebSocket 和 MCP Streamable HTTP(/mcp)。一律要 token。
SessionManager 选项:
| 字段 | 默认 | 说明 |
|------|------|------|
| bufferSize | 1 MiB | 每个会话环形输出缓冲 |
| defaultCols / defaultRows | 80 / 24 | create 未传尺寸时使用 |
| cwdAllowList | 不限制 | 只允许这些目录及其子目录作为 cwd |
Docker 与默认用户
面向容器:daemon 以 root 跑(USER root 或 docker run -u 0),PTY 默认 uid/gid 为 0。
- 未指定
cwd时依次试:/home→process.cwd()→ 家目录 → 临时目录。 - 以 root 启动时,子进程
HOME/USER/LOGNAME设为/root/root。 - 进程本身不是 root 时不会 setuid,子进程仍是当前用户。这时若前端硬传
cwd: '/root/workspace',会因/root为drwx------而失败。
本地非 root 开发不受影响:跳过进不去的 /root,落到可用目录。
操作
MCP 工具、SDK 方法、REST 路径共用同一套 options。
create
开一个持久 PTY。默认 $SHELL 或 /bin/bash。TERM 为 xterm-256color。新建会话会强制 LANG / LC_ALL / LC_CTYPE 为 C.UTF-8,避免中文乱码。
| 字段 | 类型 | 说明 |
|------|------|------|
| cwd | string | 工作目录。未传时依次试 /home、process.cwd()、家目录、临时目录 |
| cols / rows | number | 默认 80 × 24 |
| env | Record<string, string> | 覆盖子进程环境。LANG / LC_ALL / LC_CTYPE 仍固定为 C.UTF-8 |
| name | string | 展示名,默认等于 id |
| command | string | 可执行文件 |
| args | string[] | 参数 |
| uid / gid | number | 默认 0。仅当 daemon 自身是 root 时才会真正 setuid |
返回:{ id, name, pid, cwd, cols, rows, createdAt, running }。id 形如 t_<uuid>。
write
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 必填 |
| data | string | 写入内容 |
| submit | boolean | HTTP/MCP 默认 true:末尾没有 \n / \r 时自动补 \n。xterm.js 按键必须 false,或走 subscribe().write() |
返回:{ id, bytes }。
read
增量读输出。把上次返回的 cursor 传回 since。适合轮询;实时画面用 subscribe。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 必填 |
| since | number | 字节游标,默认 0(缓冲起点) |
| waitMs | number | 没有新输出时最多阻塞多久,默认 0(立即返回) |
| maxBytes | number | 本次最多返回多少字节 |
返回:{ id, data, cursor, running, exitCode?, dropped? }。dropped 表示环形缓冲溢出、在这次读取之前丢掉的字节。
resize
同步 PTY 尺寸。xterm.js onResize / FitAddon 之后调用。
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 必填 |
| cols / rows | number | 正整数 |
返回当前 SessionInfo(含更新后的 cols / rows)。会话已退出时失败。
kill
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 必填 |
| signal | string \| number | 默认 SIGTERM |
返回:{ id, ok: true, running, exitCode? }。
list
列出当前进程里还活着的会话。options 预留给以后的过滤字段。
每项即 SessionInfo:id、name、pid、cwd、cols、rows、createdAt、running、exitCode?。
REST
鉴权:HTTP 用 Authorization: Bearer <token>;WebSocket 也可用 ?token=。未带或错误返回 401。OPTIONS 预检不校验 token(CORS)。
成功:{ ok: true, data }。失败:{ ok: false, error: { code, message } }。
| 方法 | 路径 | 操作 |
|------|------|------|
| POST | /sessions | create,JSON body |
| GET | /sessions | list |
| GET | /sessions/:id?since=&waitMs=&maxBytes= | read |
| POST | /sessions/:id/write | write,JSON body |
| POST | /sessions/:id/resize | resize,JSON body |
| DELETE | /sessions/:id?signal= | kill |
| GET | /sessions/:id/stream | WebSocket |
| GET | /health | 健康检查(同样要 token) |
| * | /mcp | MCP Streamable HTTP |
WebSocket /sessions/:id/stream
连上后服务端先发缓冲回放,再推增量。会话退出时发 exit 并关闭连接。未知 session 以 code 4004 关闭。
服务端 → 客户端:
{ "type": "data", "data": "..." }
{ "type": "exit", "exitCode": 0 }客户端 → 服务端(subscribe().write / resize 已封装):
{ "type": "write", "data": "...", "submit": false }
{ "type": "resize", "cols": 120, "rows": 40 }非 JSON 文本按原始输入处理(submit: false)。WS 上 write 的 submit 默认 false(和 HTTP 相反),方便 xterm 按键。
CLI
# stdio MCP(给 Cursor)
LOCAL_TERMINAL_TOKEN=secret pty-session-mcp
# HTTP / WS / MCP HTTP
LOCAL_TERMINAL_TOKEN=secret pty-session-mcp serve --host 0.0.0.0 --port 8080| 变量 / 参数 | 说明 |
|-------------|------|
| LOCAL_TERMINAL_TOKEN | 必填 |
| LOCAL_TERMINAL_HOST / --host / --host= | 默认 0.0.0.0。只给本机连时再设 127.0.0.1 |
| LOCAL_TERMINAL_PORT / --port / --port= | 默认 7788 |
监听地址打到 stderr:pty-session-mcp listening on http://0.0.0.0:8080。SIGINT / SIGTERM 会 dispose 全部会话后退出。
本仓库未发布的改动要用本地产物,不要只靠已发布的 npx:
LOCAL_TERMINAL_TOKEN=secret node libraries/local-terminal-mcp/dist/cli.js serve --host 0.0.0.0 --port 8080错误码
| code | HTTP | 含义 |
|------|------|------|
| UNAUTHORIZED | 401 | token 缺失或错误 |
| BAD_REQUEST | 400 | cwd 不可用、会话已退出、body 不合法等 |
| NOT_FOUND | 404 | 会话或路由不存在 |
| FORBIDDEN | 403 | cwd 不在 cwdAllowList 内 |
| INTERNAL | 500 | 未预期错误 |
进程启动时没有 token:抛 TokenConfigError(LOCAL_TERMINAL_TOKEN is required and must be a non-empty string),CLI 退出码 1。
SDK 失败时抛 Error,message 为 CODE: ...。
常见问题
网关 connect error 111
daemon 只绑了 127.0.0.1,或前端 url 指向了浏览器到不了的地址。改 --host 0.0.0.0,url 用预览 / 网关域名。
cannot use cwd "/root/workspace": permission denied
daemon 不是 root,进不了 drwx------ 的 /root。容器里用 root 跑;或不传 cwd,默认 /home。
xterm 每个按键都像回车 / 错位
按键走了 HTTP write 且 submit 为 true。改用 subscribe().write。创建时传入 cols/rows,窗口变化要 resize。
npx pty-session-mcp 行为对不上源码
npx 拉的是 npm 上的版本。改完后先 rushx build,用 node dist/cli.js,或重新 publish。
输出缺了一截
环形缓冲默认 1 MiB,更早的数据在 read 的 dropped 里体现。加大 SessionManager({ bufferSize })。
以后加参数
在 src/core/types.ts 的对应 options 接口上加字段,并同步 src/core/schemas.ts。zod 使用 .passthrough(),新旧版本可以错开发布。
License
MIT
