@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +12 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-2J5WHI6X.js +2000 -0
  7. package/dist/chunk-2J5WHI6X.js.map +1 -0
  8. package/dist/chunk-36C7A7DW.js +2685 -0
  9. package/dist/chunk-36C7A7DW.js.map +1 -0
  10. package/dist/chunk-3PLOAM2C.js +2427 -0
  11. package/dist/chunk-3PLOAM2C.js.map +1 -0
  12. package/dist/chunk-57GYK2EF.js +2991 -0
  13. package/dist/chunk-57GYK2EF.js.map +1 -0
  14. package/dist/chunk-ABLJBL5P.js +2687 -0
  15. package/dist/chunk-ABLJBL5P.js.map +1 -0
  16. package/dist/chunk-BOOUADRN.js +1938 -0
  17. package/dist/chunk-BOOUADRN.js.map +1 -0
  18. package/dist/chunk-BPWIETN5.js +2983 -0
  19. package/dist/chunk-BPWIETN5.js.map +1 -0
  20. package/dist/chunk-CMQB5G7R.js +2968 -0
  21. package/dist/chunk-CMQB5G7R.js.map +1 -0
  22. package/dist/chunk-I4B53KZ7.js +2955 -0
  23. package/dist/chunk-I4B53KZ7.js.map +1 -0
  24. package/dist/chunk-IPNUAUAN.js +2991 -0
  25. package/dist/chunk-IPNUAUAN.js.map +1 -0
  26. package/dist/chunk-KZWL2S6E.js +2869 -0
  27. package/dist/chunk-KZWL2S6E.js.map +1 -0
  28. package/dist/chunk-LB2QBYW4.js +2686 -0
  29. package/dist/chunk-LB2QBYW4.js.map +1 -0
  30. package/dist/chunk-MR3AXSXL.js +1977 -0
  31. package/dist/chunk-MR3AXSXL.js.map +1 -0
  32. package/dist/chunk-NVSZXEZU.js +2688 -0
  33. package/dist/chunk-NVSZXEZU.js.map +1 -0
  34. package/dist/chunk-PD2WKAFE.js +2531 -0
  35. package/dist/chunk-PD2WKAFE.js.map +1 -0
  36. package/dist/chunk-PGY4ZDLD.js +1843 -0
  37. package/dist/chunk-PGY4ZDLD.js.map +1 -0
  38. package/dist/chunk-QDIAASH7.js +2982 -0
  39. package/dist/chunk-QDIAASH7.js.map +1 -0
  40. package/dist/chunk-UZWFLJGG.js +2873 -0
  41. package/dist/chunk-UZWFLJGG.js.map +1 -0
  42. package/dist/chunk-VFYTROYG.js +2825 -0
  43. package/dist/chunk-VFYTROYG.js.map +1 -0
  44. package/dist/chunk-ZTHKAJKT.js +2981 -0
  45. package/dist/chunk-ZTHKAJKT.js.map +1 -0
  46. package/dist/chunk-ZZULGRRE.js +2991 -0
  47. package/dist/chunk-ZZULGRRE.js.map +1 -0
  48. package/dist/index.d.ts +826 -0
  49. package/dist/index.js +105 -0
  50. package/dist/index.js.map +1 -0
  51. 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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ main
4
+ } from "./chunk-IPNUAUAN.js";
5
+
6
+ // src/bin.ts
7
+ process.stdout.on("error", (error) => {
8
+ if (error.code === "EPIPE") process.exit(0);
9
+ throw error;
10
+ });
11
+ await main();
12
+ //# sourceMappingURL=bin.js.map
@@ -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":[]}