@termwright/mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +12 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-2J5WHI6X.js +2000 -0
- package/dist/chunk-2J5WHI6X.js.map +1 -0
- package/dist/chunk-36C7A7DW.js +2685 -0
- package/dist/chunk-36C7A7DW.js.map +1 -0
- package/dist/chunk-3PLOAM2C.js +2427 -0
- package/dist/chunk-3PLOAM2C.js.map +1 -0
- package/dist/chunk-57GYK2EF.js +2991 -0
- package/dist/chunk-57GYK2EF.js.map +1 -0
- package/dist/chunk-ABLJBL5P.js +2687 -0
- package/dist/chunk-ABLJBL5P.js.map +1 -0
- package/dist/chunk-BOOUADRN.js +1938 -0
- package/dist/chunk-BOOUADRN.js.map +1 -0
- package/dist/chunk-BPWIETN5.js +2983 -0
- package/dist/chunk-BPWIETN5.js.map +1 -0
- package/dist/chunk-CMQB5G7R.js +2968 -0
- package/dist/chunk-CMQB5G7R.js.map +1 -0
- package/dist/chunk-I4B53KZ7.js +2955 -0
- package/dist/chunk-I4B53KZ7.js.map +1 -0
- package/dist/chunk-IPNUAUAN.js +2991 -0
- package/dist/chunk-IPNUAUAN.js.map +1 -0
- package/dist/chunk-KZWL2S6E.js +2869 -0
- package/dist/chunk-KZWL2S6E.js.map +1 -0
- package/dist/chunk-LB2QBYW4.js +2686 -0
- package/dist/chunk-LB2QBYW4.js.map +1 -0
- package/dist/chunk-MR3AXSXL.js +1977 -0
- package/dist/chunk-MR3AXSXL.js.map +1 -0
- package/dist/chunk-NVSZXEZU.js +2688 -0
- package/dist/chunk-NVSZXEZU.js.map +1 -0
- package/dist/chunk-PD2WKAFE.js +2531 -0
- package/dist/chunk-PD2WKAFE.js.map +1 -0
- package/dist/chunk-PGY4ZDLD.js +1843 -0
- package/dist/chunk-PGY4ZDLD.js.map +1 -0
- package/dist/chunk-QDIAASH7.js +2982 -0
- package/dist/chunk-QDIAASH7.js.map +1 -0
- package/dist/chunk-UZWFLJGG.js +2873 -0
- package/dist/chunk-UZWFLJGG.js.map +1 -0
- package/dist/chunk-VFYTROYG.js +2825 -0
- package/dist/chunk-VFYTROYG.js.map +1 -0
- package/dist/chunk-ZTHKAJKT.js +2981 -0
- package/dist/chunk-ZTHKAJKT.js.map +1 -0
- package/dist/chunk-ZZULGRRE.js +2991 -0
- package/dist/chunk-ZZULGRRE.js.map +1 -0
- package/dist/index.d.ts +826 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gorce-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# @termwright/mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that lets an agent drive terminal programs the way a person
|
|
4
|
+
would: launch a real pseudo-terminal, read a compact accessibility-style
|
|
5
|
+
snapshot, click a button by its ref, wait on a condition, and ask what changed.
|
|
6
|
+
|
|
7
|
+
It is deliberately thin. Every tool validates its arguments with zod, calls the
|
|
8
|
+
public `@termwright/driver` API, and renders the result. There is no locator
|
|
9
|
+
engine, no wait loop and no matching heuristic here — a behaviour that differed
|
|
10
|
+
between this server and the test preset would be a bug in this package.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add -D @termwright/mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Node >= 22, ESM only.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Register the binary with your MCP host (stdio is what hosts spawn):
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"termwright": { "command": "termwright-mcp" }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A typical agent loop:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { serveStdio } from '@termwright/mcp';
|
|
36
|
+
|
|
37
|
+
const running = await serveStdio(); // stdio, one implicit session
|
|
38
|
+
process.on('SIGINT', () => void running.close());
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```jsonc
|
|
42
|
+
// terminal.launch -> { terminal: "t1", semanticTree: "available", compact: … }
|
|
43
|
+
// envMode defaults to "replace": the child gets a minimal environment, not the
|
|
44
|
+
// operator's secrets. Pass "inherit" (or name variables in env) when it needs more.
|
|
45
|
+
{ "command": ["node", "app.js"], "columns": 100, "rows": 30 }
|
|
46
|
+
|
|
47
|
+
// terminal.snapshot -> the compact format, plus refs / cursor / modes / scroll
|
|
48
|
+
Terminal t1 100x30 revision 42
|
|
49
|
+
semanticTree: available
|
|
50
|
+
dialog "Permission" ref=n7@42 bounds=(8,20,40,9) modal
|
|
51
|
+
button "Approve" ref=n8@42 bounds=(14,23,11,1) focused
|
|
52
|
+
visible text:
|
|
53
|
+
…
|
|
54
|
+
|
|
55
|
+
// terminal.click { "terminal": "t1", "ref": "n8@42" }
|
|
56
|
+
// terminal.wait_for { "terminal": "t1", "wait": "text", "text": "Approved" }
|
|
57
|
+
// terminal.capture_since{ "terminal": "t1", "cursor": 42 } -> changed rows + subtrees
|
|
58
|
+
// terminal.close { "terminal": "t1" }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Streamable HTTP, for hosts that connect over a socket:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
termwright-mcp --http --port 7333 # endpoint: http://127.0.0.1:7333/mcp
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Sessions are keyed by `Mcp-Session-Id` in this package's own `SessionRegistry`,
|
|
68
|
+
not inside transport objects: each session owns its terminals, `DELETE` disposes
|
|
69
|
+
them, and the ceiling (16 sessions, 16 terminals each) is enforced before a
|
|
70
|
+
transport exists.
|
|
71
|
+
|
|
72
|
+
Streamable HTTP gives no disconnect signal, so a session also expires after
|
|
73
|
+
`idleTtlMs` without a request (10 minutes by default, `0` to disable). Every
|
|
74
|
+
request naming a session refreshes it; expiry runs the full teardown — terminals
|
|
75
|
+
closed, children gone, traces released, slot returned — and writes a line to
|
|
76
|
+
stderr. stdio has no TTL: there, EOF on the pipe is the signal.
|
|
77
|
+
|
|
78
|
+
## Tools
|
|
79
|
+
|
|
80
|
+
`terminal.launch`, `capabilities`, `snapshot`, `capture_since`, `query`,
|
|
81
|
+
`click`, `double_click`, `press`, `type`, `paste`, `write_raw`, `drag`, `wheel`,
|
|
82
|
+
`resize`, `signal`, `scrollback`, `select_cells`, `copy_selection`, `wait_for`,
|
|
83
|
+
`close` — the surface CONTRACTS.md §MCP defines — plus `trace.open`,
|
|
84
|
+
`trace.overview`, `trace.frame_at` and `trace.diff` for recorded sessions. Every
|
|
85
|
+
one carries an `inputSchema` and an `outputSchema` and returns
|
|
86
|
+
`structuredContent`.
|
|
87
|
+
|
|
88
|
+
Targeting, in precedence order: `ref`, `selector` (the CSS dialect
|
|
89
|
+
`dialog button#approve:focused`), `testId`, `role` (+ `name`), `label`, `text`.
|
|
90
|
+
Any name or text may be written as `/pattern/flags` to match as a regular
|
|
91
|
+
expression. Locators are strict: more than one match fails with
|
|
92
|
+
`ambiguous-locator` unless you pass `nth`.
|
|
93
|
+
|
|
94
|
+
## Replaying a recorded failure
|
|
95
|
+
|
|
96
|
+
A failing run leaves a `.twtrace` archive; the `trace.*` tools read it with the
|
|
97
|
+
same vocabulary as a live session.
|
|
98
|
+
|
|
99
|
+
```jsonc
|
|
100
|
+
// trace.open { "path": "out/login.twtrace" } -> handle tr1 + what was recorded
|
|
101
|
+
// trace.overview { "traceId": "tr1" } -> steps, markers, exit, which step failed
|
|
102
|
+
// trace.frame_at { "traceId": "tr1", "stepIndex": 1 }
|
|
103
|
+
Terminal tr1 40x6 revision 2
|
|
104
|
+
semanticTree: available
|
|
105
|
+
dialog "Permission" ref=n1@2 modal
|
|
106
|
+
button "Approve" ref=n2@2 disabled
|
|
107
|
+
visible text:
|
|
108
|
+
…
|
|
109
|
+
// trace.diff { "traceId": "tr1", "fromMs": 0, "toMs": 3000 } -> changed rows + subtrees
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Reconstruction is `@termwright/trace`'s: `stateAt()` returns the cast prefix and
|
|
113
|
+
the nearest semantic snapshot, and the prefix is replayed through the same
|
|
114
|
+
headless emulator the HTML report uses. A moment is named by `timeMs`,
|
|
115
|
+
`stepIndex` or `marker` — exactly one of them.
|
|
116
|
+
|
|
117
|
+
Archives are per session, capped at 8 open and 128 MB each; at the ceiling the
|
|
118
|
+
coldest handle is closed and named in the result, and re-opening a path always
|
|
119
|
+
works.
|
|
120
|
+
|
|
121
|
+
## Screenshots
|
|
122
|
+
|
|
123
|
+
`terminal.snapshot` and `trace.frame_at` take `screenshot: true` and attach a PNG
|
|
124
|
+
as `ImageContent`, rendered by `@termwright/screenshot` — a cell grid becomes an
|
|
125
|
+
SVG with the glyph outlines embedded and resvg rasterises it, so there is no
|
|
126
|
+
browser in the loop and no dependency on the agent's machine having the right
|
|
127
|
+
font.
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
{ "terminal": "t1", "screenshot": true, "screenshotScale": 2, "screenshotTheme": "light" }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The image is always *additional*: the compact tree and the screen text are in the
|
|
134
|
+
same result, so an agent that cannot see pictures loses nothing.
|
|
135
|
+
`structuredContent.screenshot` carries the size and `selfContained` — false when
|
|
136
|
+
a character had no embedded outline and fell back to a font the viewer may not
|
|
137
|
+
have. PNGs above 3 MB are refused with `capacity` rather than blowing a context
|
|
138
|
+
window; lower `screenshotScale` or resize the terminal.
|
|
139
|
+
|
|
140
|
+
## Refs and revisions
|
|
141
|
+
|
|
142
|
+
A ref is `n8@42`: node id at semantic revision 42 (grid matches get
|
|
143
|
+
`grid:1,2,9,1@7`). Refs go straight to `harness.locatorForRef()`, so they resolve
|
|
144
|
+
by node *identity* — two buttons with the same name stay distinct. A producer
|
|
145
|
+
which promises stable identity can resolve that node again in later revisions.
|
|
146
|
+
Frame-local identities and grid refs remain revision-bound; take a fresh
|
|
147
|
+
snapshot when either becomes stale.
|
|
148
|
+
|
|
149
|
+
`terminal.snapshot` also returns a screen `revision`; pass it back as the
|
|
150
|
+
`cursor` of `terminal.capture_since` to get only the rows and semantic subtrees
|
|
151
|
+
that changed. Cursors the server never handed out fail with `history-truncated`
|
|
152
|
+
(the last 16 captures per terminal are retained).
|
|
153
|
+
|
|
154
|
+
Programs without a framework probe or custom semantic producer report
|
|
155
|
+
`semanticTree: unavailable`. There are no invented roles: target them by text.
|
|
156
|
+
|
|
157
|
+
## Application logs
|
|
158
|
+
|
|
159
|
+
A terminal shows what a program drew; its log says what it decided. Follow one
|
|
160
|
+
at launch and read it alongside the screen:
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
// terminal.launch
|
|
164
|
+
{ "command": ["node", "app.js"], "logs": [{ "path": "out/app.log", "label": "app" }] }
|
|
165
|
+
|
|
166
|
+
// terminal.capture_since -> changed rows, changed subtrees, and:
|
|
167
|
+
logs: 2
|
|
168
|
+
1840ms [app] ERROR upstream refused the token
|
|
169
|
+
1841ms [app] WARN falling back to cached profile
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
An existing file is followed from its end, so a session never replays the
|
|
173
|
+
previous run. Entries are buffered per terminal (1000 deep) and returned since
|
|
174
|
+
your cursor, with `logsOmitted` counting anything that fell out in between —
|
|
175
|
+
computed when you read, so a program that went quiet still reports its last
|
|
176
|
+
drops. Files are polled, so a line written moments ago may arrive on the next
|
|
177
|
+
call; re-asking with the same cursor is lossless.
|
|
178
|
+
|
|
179
|
+
Structured records from an instrumented adapter keep their level, logger and
|
|
180
|
+
attributes; a followed file yields the raw line.
|
|
181
|
+
|
|
182
|
+
The same view exists for a recording: `trace.frame_at` returns the entries
|
|
183
|
+
leading up to that moment (`maxLogs`, default 20) and `trace.diff` the ones
|
|
184
|
+
between the two, so "what was it saying when the screen looked like this" reads
|
|
185
|
+
the same live and in replay.
|
|
186
|
+
|
|
187
|
+
## Crashes
|
|
188
|
+
|
|
189
|
+
When a child dies on its own, the driver records what the session knew and this
|
|
190
|
+
server surfaces it three ways: attached to whatever call failed next,
|
|
191
|
+
in `terminal.capabilities` and `terminal.snapshot` instead of a bare closed
|
|
192
|
+
session, and in `trace.overview` for a recording whose `meta.json` carries one.
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
crash: the program exited on its own — code=7 signal=null at 812ms
|
|
196
|
+
last input: key "\r"
|
|
197
|
+
screen tail:
|
|
198
|
+
Error: boom
|
|
199
|
+
at thing (app.js:3:9)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
That matters because a locator which never resolved because the program is gone
|
|
203
|
+
otherwise reports a plain `timeout`, and an agent reading a timeout waits longer.
|
|
204
|
+
|
|
205
|
+
In a recording, `crash.timeMs` is the cast offset, so
|
|
206
|
+
`trace.frame_at { traceId, timeMs }` jumps to the moment of death with the screen
|
|
207
|
+
and the semantic tree of that revision.
|
|
208
|
+
|
|
209
|
+
The screen tail is **unredacted** — it is what the terminal displayed, secrets
|
|
210
|
+
included. It is bounded (40 lines, 500 characters each) and never logged, but
|
|
211
|
+
treat it like a screenshot when storing or forwarding a result. Paste contents
|
|
212
|
+
are the one thing never recorded: the driver keeps their size only.
|
|
213
|
+
|
|
214
|
+
## Errors
|
|
215
|
+
|
|
216
|
+
Failures come back as tool results with `isError` set. The text content reads
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
error stale-snapshot: ref n8@42 no longer exists at semantic revision 43
|
|
220
|
+
suggestion: re-resolve the locator; the node identity is no longer present
|
|
221
|
+
semanticTree: true
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
and the same payload — `kind`, `message`, `suggestion`, bounded `candidates`,
|
|
225
|
+
`screenExcerpt` — travels structured in `_meta["io.termwright/error"]`. Stack
|
|
226
|
+
traces never leave the server, and neither the child's environment nor the
|
|
227
|
+
session token appears in any result or log.
|
|
228
|
+
|
|
229
|
+
## CLI
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
termwright-mcp # serve over stdio
|
|
233
|
+
termwright-mcp --http --port N # serve Streamable HTTP on /mcp
|
|
234
|
+
termwright-mcp agent-context # versioned JSON: every tool, param, enum, exit code
|
|
235
|
+
termwright-mcp usage # one-screen cheat sheet
|
|
236
|
+
termwright-mcp skill --out DIR # emit an agent-skill package (SKILL.md + reference)
|
|
237
|
+
termwright-mcp --json … # machine-readable errors carrying `kind`
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Exit codes: 0 ok / 1 assertion / 2 usage / 3 no-session / 4 ipc / 5 internal.
|
|
241
|
+
|
|
242
|
+
`agent-context` and the `skill` package are generated from the live zod schemas,
|
|
243
|
+
so neither can drift from the tools. `skill` writes `SKILL.md` (what an agent
|
|
244
|
+
reads), `reference.md` (every tool and parameter) and `agent-context.json`; with
|
|
245
|
+
no `--out` it prints them instead. The umbrella `termwright` CLI imports
|
|
246
|
+
`buildAgentContext()`, `buildUsage()` and `buildAgentSkill()` rather than
|
|
247
|
+
spawning this binary.
|
|
248
|
+
|
|
249
|
+
## Testing this package
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
pnpm build && pnpm typecheck && pnpm test
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The end-to-end suite runs a real MCP client over `InMemoryTransport` against the
|
|
256
|
+
real driver and the fixtures in `packages/driver/test-fixtures`. It skips itself
|
|
257
|
+
where no pseudo-terminal can be opened, or with `TERMWRIGHT_SKIP_PTY=1`.
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.js
ADDED
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/** The `termwright-mcp` executable. All logic lives in `cli.ts`. */\nimport { main } from './cli.js';\n\n// `termwright-mcp agent-context | head` closes the pipe early; that is a normal\n// end of output for a CLI, not a crash.\nprocess.stdout.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EPIPE') process.exit(0);\n throw error;\n});\n\nawait main();\n"],"mappings":";;;;;;AAMA,QAAQ,OAAO,GAAG,SAAS,CAAC,UAAiC;AAC3D,MAAI,MAAM,SAAS,QAAS,SAAQ,KAAK,CAAC;AAC1C,QAAM;AACR,CAAC;AAED,MAAM,KAAK;","names":[]}
|