@termwright/mcp 0.2.0 → 0.3.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 (55) hide show
  1. package/README.md +109 -28
  2. package/dist/bin.js +2 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-R2N52YYH.js +629 -0
  5. package/dist/chunk-R2N52YYH.js.map +1 -0
  6. package/dist/{chunk-IPNUAUAN.js → chunk-ROMJP5D3.js} +595 -482
  7. package/dist/chunk-ROMJP5D3.js.map +1 -0
  8. package/dist/docs-85fGFORb.d.ts +13 -0
  9. package/dist/docs.d.ts +1 -0
  10. package/dist/docs.js +7 -0
  11. package/dist/docs.js.map +1 -0
  12. package/dist/index.d.ts +50 -28
  13. package/dist/index.js +9 -7
  14. package/package.json +11 -8
  15. package/dist/chunk-2J5WHI6X.js +0 -2000
  16. package/dist/chunk-2J5WHI6X.js.map +0 -1
  17. package/dist/chunk-36C7A7DW.js +0 -2685
  18. package/dist/chunk-36C7A7DW.js.map +0 -1
  19. package/dist/chunk-3PLOAM2C.js +0 -2427
  20. package/dist/chunk-3PLOAM2C.js.map +0 -1
  21. package/dist/chunk-57GYK2EF.js +0 -2991
  22. package/dist/chunk-57GYK2EF.js.map +0 -1
  23. package/dist/chunk-ABLJBL5P.js +0 -2687
  24. package/dist/chunk-ABLJBL5P.js.map +0 -1
  25. package/dist/chunk-BOOUADRN.js +0 -1938
  26. package/dist/chunk-BOOUADRN.js.map +0 -1
  27. package/dist/chunk-BPWIETN5.js +0 -2983
  28. package/dist/chunk-BPWIETN5.js.map +0 -1
  29. package/dist/chunk-CMQB5G7R.js +0 -2968
  30. package/dist/chunk-CMQB5G7R.js.map +0 -1
  31. package/dist/chunk-I4B53KZ7.js +0 -2955
  32. package/dist/chunk-I4B53KZ7.js.map +0 -1
  33. package/dist/chunk-IPNUAUAN.js.map +0 -1
  34. package/dist/chunk-KZWL2S6E.js +0 -2869
  35. package/dist/chunk-KZWL2S6E.js.map +0 -1
  36. package/dist/chunk-LB2QBYW4.js +0 -2686
  37. package/dist/chunk-LB2QBYW4.js.map +0 -1
  38. package/dist/chunk-MR3AXSXL.js +0 -1977
  39. package/dist/chunk-MR3AXSXL.js.map +0 -1
  40. package/dist/chunk-NVSZXEZU.js +0 -2688
  41. package/dist/chunk-NVSZXEZU.js.map +0 -1
  42. package/dist/chunk-PD2WKAFE.js +0 -2531
  43. package/dist/chunk-PD2WKAFE.js.map +0 -1
  44. package/dist/chunk-PGY4ZDLD.js +0 -1843
  45. package/dist/chunk-PGY4ZDLD.js.map +0 -1
  46. package/dist/chunk-QDIAASH7.js +0 -2982
  47. package/dist/chunk-QDIAASH7.js.map +0 -1
  48. package/dist/chunk-UZWFLJGG.js +0 -2873
  49. package/dist/chunk-UZWFLJGG.js.map +0 -1
  50. package/dist/chunk-VFYTROYG.js +0 -2825
  51. package/dist/chunk-VFYTROYG.js.map +0 -1
  52. package/dist/chunk-ZTHKAJKT.js +0 -2981
  53. package/dist/chunk-ZTHKAJKT.js.map +0 -1
  54. package/dist/chunk-ZZULGRRE.js +0 -2991
  55. package/dist/chunk-ZZULGRRE.js.map +0 -1
@@ -0,0 +1,629 @@
1
+ import {
2
+ CrashContextError,
3
+ EXIT_CODES,
4
+ SERVER_NAME,
5
+ SERVER_VERSION,
6
+ SessionRegistry,
7
+ TOOLS,
8
+ buildAgentContext,
9
+ buildAgentSkill,
10
+ buildUsage,
11
+ closeSessionStores,
12
+ createSessionStores,
13
+ describeCrash,
14
+ exitCodeFor,
15
+ renderErrorPayload,
16
+ toErrorPayload,
17
+ usageError,
18
+ writeAgentSkill
19
+ } from "./chunk-ROMJP5D3.js";
20
+
21
+ // src/server.ts
22
+ import { createServer } from "http";
23
+ import { randomBytes, randomUUID } from "crypto";
24
+
25
+ // src/http-security.ts
26
+ import { timingSafeEqual } from "crypto";
27
+ import { isIP } from "net";
28
+ var DEFAULT_HTTP_RATE_LIMIT = Object.freeze({
29
+ windowMs: 6e4,
30
+ maxRequests: 120,
31
+ maxClients: 1024
32
+ });
33
+ var BoundedRateLimiter = class {
34
+ #windowMs;
35
+ #maxRequests;
36
+ #maxClients;
37
+ #buckets = /* @__PURE__ */ new Map();
38
+ constructor(options = {}) {
39
+ this.#windowMs = positiveInteger(
40
+ options.windowMs ?? DEFAULT_HTTP_RATE_LIMIT.windowMs,
41
+ "rateLimit.windowMs"
42
+ );
43
+ this.#maxRequests = positiveInteger(
44
+ options.maxRequests ?? DEFAULT_HTTP_RATE_LIMIT.maxRequests,
45
+ "rateLimit.maxRequests"
46
+ );
47
+ this.#maxClients = positiveInteger(
48
+ options.maxClients ?? DEFAULT_HTTP_RATE_LIMIT.maxClients,
49
+ "rateLimit.maxClients"
50
+ );
51
+ }
52
+ /** Visible for deterministic tests and operational diagnostics. */
53
+ get size() {
54
+ return this.#buckets.size;
55
+ }
56
+ admit(identity, now) {
57
+ if (!Number.isFinite(now)) throw new TypeError("rate-limit clock must return a finite number");
58
+ let bucket = this.#buckets.get(identity);
59
+ if (bucket !== void 0 && now - bucket.startedAt >= this.#windowMs) {
60
+ this.#buckets.delete(identity);
61
+ bucket = void 0;
62
+ }
63
+ if (bucket === void 0) {
64
+ if (this.#buckets.size >= this.#maxClients) this.#purgeExpired(now);
65
+ if (this.#buckets.size >= this.#maxClients) {
66
+ return { allowed: false, retryAfterSeconds: this.#retryForOldest(now) };
67
+ }
68
+ this.#buckets.set(identity, { startedAt: now, requests: 1 });
69
+ return { allowed: true, retryAfterSeconds: 0 };
70
+ }
71
+ if (bucket.requests >= this.#maxRequests) {
72
+ return {
73
+ allowed: false,
74
+ retryAfterSeconds: secondsUntil(bucket.startedAt + this.#windowMs, now)
75
+ };
76
+ }
77
+ bucket.requests += 1;
78
+ return { allowed: true, retryAfterSeconds: 0 };
79
+ }
80
+ #purgeExpired(now) {
81
+ for (const [identity, bucket] of this.#buckets) {
82
+ if (now - bucket.startedAt >= this.#windowMs) this.#buckets.delete(identity);
83
+ }
84
+ }
85
+ #retryForOldest(now) {
86
+ let oldestExpiry = Number.POSITIVE_INFINITY;
87
+ for (const bucket of this.#buckets.values()) {
88
+ oldestExpiry = Math.min(oldestExpiry, bucket.startedAt + this.#windowMs);
89
+ }
90
+ return Number.isFinite(oldestExpiry) ? secondsUntil(oldestExpiry, now) : 1;
91
+ }
92
+ };
93
+ function normalizeAllowedOrigins(origins = []) {
94
+ const normalized = /* @__PURE__ */ new Set();
95
+ for (const candidate of origins) {
96
+ let url;
97
+ try {
98
+ url = new URL(candidate);
99
+ } catch {
100
+ throw new TypeError(`allowed origin is not a URL: ${JSON.stringify(candidate)}`);
101
+ }
102
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
103
+ throw new TypeError(
104
+ `allowed origin must be an HTTP(S) origin without path, query or credentials: ${JSON.stringify(candidate)}`
105
+ );
106
+ }
107
+ normalized.add(url.origin);
108
+ }
109
+ return normalized;
110
+ }
111
+ function isLoopbackHost(host) {
112
+ const lower = host.toLowerCase().replace(/^\[|\]$/gu, "");
113
+ if (lower === "localhost" || lower === "::1") return true;
114
+ if (isIP(lower) === 4) return lower.split(".")[0] === "127";
115
+ return /^::ffff:127(?:\.\d{1,3}){3}$/u.test(lower);
116
+ }
117
+ function admitHttpRequest(request, response, options) {
118
+ const identity = request.socket.remoteAddress ?? "<unknown-peer>";
119
+ const origin = request.headers.origin;
120
+ if (origin !== void 0 && (Array.isArray(origin) || !options.allowedOrigins.has(origin))) {
121
+ sendSecurityError(response, 403, "origin is not allowed");
122
+ return false;
123
+ }
124
+ if (typeof origin === "string") {
125
+ response.setHeader("access-control-allow-origin", origin);
126
+ response.setHeader("access-control-expose-headers", "Mcp-Session-Id");
127
+ response.setHeader("vary", "Origin");
128
+ if (request.method === "OPTIONS") {
129
+ if (!admitRate(identity, response, options.preflightRateLimiter, options.now)) return false;
130
+ return admitPreflight(request, response);
131
+ }
132
+ }
133
+ if (!bearerMatches(request.headers.authorization, options.token)) {
134
+ sendSecurityError(response, 401, "missing or invalid bearer token", {
135
+ "www-authenticate": 'Bearer realm="termwright-mcp"'
136
+ });
137
+ return false;
138
+ }
139
+ if (!admitRate(identity, response, options.authenticatedRateLimiter, options.now)) return false;
140
+ return true;
141
+ }
142
+ function admitRate(identity, response, limiter, now) {
143
+ const rate = limiter.admit(identity, now());
144
+ if (rate.allowed) return true;
145
+ sendSecurityError(response, 429, "rate limit exceeded", {
146
+ "retry-after": String(rate.retryAfterSeconds)
147
+ });
148
+ return false;
149
+ }
150
+ var CORS_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "DELETE"]);
151
+ var CORS_HEADERS = /* @__PURE__ */ new Set([
152
+ "accept",
153
+ "authorization",
154
+ "content-type",
155
+ "last-event-id",
156
+ "mcp-protocol-version",
157
+ "mcp-session-id"
158
+ ]);
159
+ function admitPreflight(request, response) {
160
+ const method = request.headers["access-control-request-method"];
161
+ const requestedHeaders = (request.headers["access-control-request-headers"] ?? "").split(",").map((header) => header.trim().toLowerCase()).filter((header) => header !== "");
162
+ if (typeof method !== "string" || !CORS_METHODS.has(method.toUpperCase()) || requestedHeaders.some((header) => !CORS_HEADERS.has(header))) {
163
+ sendSecurityError(response, 403, "CORS preflight is not allowed");
164
+ return false;
165
+ }
166
+ response.writeHead(204, {
167
+ "access-control-allow-methods": [...CORS_METHODS].join(", "),
168
+ "access-control-allow-headers": [...CORS_HEADERS].join(", "),
169
+ "access-control-max-age": "600"
170
+ });
171
+ response.end();
172
+ return false;
173
+ }
174
+ function bearerMatches(header, expected) {
175
+ if (header === void 0 || !header.startsWith("Bearer ")) return false;
176
+ const provided = header.slice("Bearer ".length);
177
+ const left = Buffer.from(provided, "utf8");
178
+ const right = Buffer.from(expected, "utf8");
179
+ return left.length === right.length && timingSafeEqual(left, right);
180
+ }
181
+ function sendSecurityError(response, status, error, headers = {}) {
182
+ const text = JSON.stringify({ error });
183
+ response.writeHead(status, {
184
+ "cache-control": "no-store",
185
+ connection: "close",
186
+ "content-type": "application/json",
187
+ ...headers
188
+ });
189
+ response.end(text);
190
+ }
191
+ function positiveInteger(value, name) {
192
+ if (!Number.isSafeInteger(value) || value <= 0)
193
+ throw new TypeError(`${name} must be a positive safe integer`);
194
+ return value;
195
+ }
196
+ function secondsUntil(deadline, now) {
197
+ return Math.max(1, Math.ceil((deadline - now) / 1e3));
198
+ }
199
+
200
+ // src/sdk-facade.ts
201
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
202
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
203
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
204
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
205
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
206
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
207
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
208
+ async function connectTransport(server, transport) {
209
+ await server.connect(transport);
210
+ }
211
+
212
+ // src/server.ts
213
+ var INSTRUCTIONS = "Drive terminal programs the way a person would. terminal.launch starts a program and returns a handle; terminal.snapshot gives compact refs plus visible text; act with terminal.click / press / type; wait with terminal.wait_for; poll cheaply with terminal.capture_since using the revision a snapshot returned. Refs like semantic:n8@42 are only valid at semantic revision 42 \u2014 re-snapshot after the screen changes. Programs without a termwright adapter report semanticTree: unavailable; target them by text instead of by role.";
214
+ function successResult(outcome) {
215
+ return {
216
+ content: [
217
+ { type: "text", text: outcome.text },
218
+ ...(outcome.images ?? []).map((image) => ({
219
+ type: "image",
220
+ data: image.data,
221
+ mimeType: image.mimeType
222
+ }))
223
+ ],
224
+ structuredContent: outcome.data
225
+ };
226
+ }
227
+ function withCrashContext(context, args, error) {
228
+ if (error instanceof CrashContextError) return error;
229
+ const id = args?.terminal;
230
+ if (typeof id !== "string") return error;
231
+ const report = context.terminals.find(id)?.harness.crashReport();
232
+ return report === void 0 || report === null ? error : new CrashContextError(error, describeCrash(report));
233
+ }
234
+ var ERROR_META_KEY = "io.termwright/error";
235
+ function errorResult(error) {
236
+ const payload = toErrorPayload(error);
237
+ return {
238
+ isError: true,
239
+ content: [{ type: "text", text: renderErrorPayload(payload) }],
240
+ _meta: { [ERROR_META_KEY]: payload }
241
+ };
242
+ }
243
+ function createTermwrightMcpServer(stores) {
244
+ const server = new McpServer(
245
+ { name: SERVER_NAME, version: SERVER_VERSION },
246
+ { capabilities: { tools: {} }, instructions: INSTRUCTIONS }
247
+ );
248
+ const context = { terminals: stores.terminals, traces: stores.traces };
249
+ for (const tool of TOOLS) {
250
+ server.registerTool(
251
+ tool.name,
252
+ {
253
+ title: tool.title,
254
+ description: tool.description,
255
+ inputSchema: tool.inputSchema,
256
+ outputSchema: tool.outputSchema,
257
+ annotations: tool.annotations
258
+ },
259
+ async (args) => {
260
+ try {
261
+ return successResult(await tool.handler(context, args));
262
+ } catch (error) {
263
+ return errorResult(withCrashContext(context, args, error));
264
+ }
265
+ }
266
+ );
267
+ }
268
+ return server;
269
+ }
270
+ async function connect(stores, transport) {
271
+ const server = createTermwrightMcpServer(stores);
272
+ try {
273
+ await connectTransport(server, transport);
274
+ } catch (error) {
275
+ const cleanup = await Promise.allSettled([closeSessionStores(stores), server.close()]);
276
+ const failures = cleanup.flatMap(
277
+ (result) => result.status === "rejected" ? [result.reason] : []
278
+ );
279
+ if (failures.length > 0)
280
+ throw new AggregateError([error, ...failures], "MCP transport startup and rollback failed");
281
+ throw error;
282
+ }
283
+ return {
284
+ server,
285
+ stores,
286
+ close: async () => {
287
+ const results = await Promise.allSettled([closeSessionStores(stores), server.close()]);
288
+ const failures = results.flatMap(
289
+ (result) => result.status === "rejected" ? [result.reason] : []
290
+ );
291
+ if (failures.length > 0)
292
+ throw new AggregateError(failures, "MCP transport failed to close cleanly");
293
+ }
294
+ };
295
+ }
296
+ async function serveStdio(options = {}) {
297
+ const stores = createSessionStores({ sessionKey: "stdio", storageDir: options.storageDir });
298
+ return connect(stores, new StdioServerTransport());
299
+ }
300
+ async function serveInMemory(options = {}) {
301
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
302
+ const stores = createSessionStores({
303
+ sessionKey: options.sessionKey ?? "in-memory",
304
+ storageDir: options.storageDir
305
+ });
306
+ const running = await connect(stores, serverTransport);
307
+ return { ...running, clientTransport };
308
+ }
309
+ var DEFAULT_IDLE_TTL_MS = 10 * 6e4;
310
+ function sendJson(response, status, body) {
311
+ const text = JSON.stringify(body);
312
+ response.writeHead(status, { "content-type": "application/json" });
313
+ response.end(text);
314
+ }
315
+ async function readBody(request) {
316
+ const chunks = [];
317
+ let size = 0;
318
+ for await (const chunk of request) {
319
+ const buffer = Buffer.from(chunk);
320
+ size += buffer.byteLength;
321
+ if (size > 4 * 1024 * 1024) throw new Error("request body too large");
322
+ chunks.push(buffer);
323
+ }
324
+ if (chunks.length === 0) return void 0;
325
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
326
+ }
327
+ async function serveHttp(options = {}) {
328
+ const path = options.path ?? "/mcp";
329
+ const host = options.host ?? "127.0.0.1";
330
+ if (!isLoopbackHost(host) && options.allowNonLoopback !== true) {
331
+ throw usageError(
332
+ `refusing non-loopback MCP HTTP bind ${JSON.stringify(host)} without allowNonLoopback`,
333
+ "keep the default loopback bind, or explicitly acknowledge the remote trust boundary"
334
+ );
335
+ }
336
+ const authToken = randomBytes(32).toString("base64url");
337
+ const allowedOrigins = normalizeAllowedOrigins(options.allowedOrigins);
338
+ const authenticatedRateLimiter = new BoundedRateLimiter(options.rateLimit);
339
+ const preflightRateLimiter = new BoundedRateLimiter(options.rateLimit);
340
+ const now = options.now ?? Date.now;
341
+ const log = options.log ?? ((message) => void process.stderr.write(`${message}
342
+ `));
343
+ const registry = new SessionRegistry({
344
+ ...options.maxSessions === void 0 ? {} : { maxSessions: options.maxSessions },
345
+ ...options.storageDir === void 0 ? {} : { storageDir: options.storageDir },
346
+ ...options.now === void 0 ? {} : { now: options.now },
347
+ idleTtlMs: options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS,
348
+ disposeAttachment: async (attachment) => {
349
+ await attachment.transport.close();
350
+ },
351
+ onExpired: (key) => {
352
+ log(`termwright: session ${key} expired after idling; terminals and traces released`);
353
+ },
354
+ onBackgroundError: (error) => {
355
+ log(
356
+ `termwright: idle session cleanup failed: ${error instanceof Error ? error.message : String(error)}`
357
+ );
358
+ }
359
+ });
360
+ const http = createServer((request, response) => {
361
+ void (async () => {
362
+ try {
363
+ if (!admitHttpRequest(request, response, {
364
+ token: authToken,
365
+ allowedOrigins,
366
+ authenticatedRateLimiter,
367
+ preflightRateLimiter,
368
+ now
369
+ }))
370
+ return;
371
+ const url = new URL(request.url ?? "/", "http://localhost");
372
+ if (url.pathname !== path) {
373
+ sendJson(response, 404, { error: "not found" });
374
+ return;
375
+ }
376
+ const sessionId = request.headers["mcp-session-id"];
377
+ const key = Array.isArray(sessionId) ? sessionId[0] : sessionId;
378
+ if (request.method === "DELETE") {
379
+ if (key !== void 0) await registry.delete(key);
380
+ response.writeHead(204).end();
381
+ return;
382
+ }
383
+ const body = request.method === "POST" ? await readBody(request) : void 0;
384
+ if (key !== void 0) {
385
+ const session2 = registry.get(key);
386
+ if (session2 === void 0) {
387
+ sendJson(response, 404, { error: "unknown session", kind: "no-session" });
388
+ return;
389
+ }
390
+ registry.touch(key);
391
+ await session2.attachment.transport.handleRequest(request, response, body);
392
+ return;
393
+ }
394
+ if (request.method !== "POST" || !isInitializeRequest(body)) {
395
+ sendJson(response, 400, { error: "missing Mcp-Session-Id", kind: "usage" });
396
+ return;
397
+ }
398
+ const newKey = randomUUID();
399
+ const session = registry.create(newKey, (stores) => {
400
+ const transport = new StreamableHTTPServerTransport({
401
+ sessionIdGenerator: () => newKey
402
+ });
403
+ const server = createTermwrightMcpServer(stores);
404
+ transport.onclose = () => {
405
+ void registry.delete(newKey).catch((error) => {
406
+ log(
407
+ `termwright: session ${newKey} transport cleanup failed: ${error instanceof Error ? error.message : String(error)}`
408
+ );
409
+ });
410
+ };
411
+ return { transport, server };
412
+ });
413
+ await connectTransport(session.attachment.server, session.attachment.transport);
414
+ await session.attachment.transport.handleRequest(request, response, body);
415
+ } catch (error) {
416
+ const payload = toErrorPayload(error);
417
+ if (!response.headersSent)
418
+ sendJson(response, 500, { error: payload.message, kind: payload.kind });
419
+ else response.end();
420
+ }
421
+ })();
422
+ });
423
+ try {
424
+ await new Promise((resolve, reject) => {
425
+ const onError = (error) => {
426
+ http.off("listening", onListening);
427
+ reject(error);
428
+ };
429
+ const onListening = () => {
430
+ http.off("error", onError);
431
+ resolve();
432
+ };
433
+ http.once("error", onError);
434
+ http.once("listening", onListening);
435
+ http.listen(options.port ?? 0, host);
436
+ });
437
+ } catch (error) {
438
+ await registry.closeAll().catch((cleanup) => {
439
+ throw new AggregateError([error, cleanup], "MCP HTTP bind and rollback both failed");
440
+ });
441
+ throw error;
442
+ }
443
+ registry.startIdleSweeper();
444
+ const address = http.address();
445
+ const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
446
+ return {
447
+ http,
448
+ registry,
449
+ port,
450
+ authToken,
451
+ close: async () => {
452
+ registry.stopIdleSweeper();
453
+ const results = await Promise.allSettled([
454
+ registry.closeAll(),
455
+ new Promise((resolve, reject) => {
456
+ http.close((error) => error === void 0 ? resolve() : reject(error));
457
+ })
458
+ ]);
459
+ const failures = results.flatMap(
460
+ (result) => result.status === "rejected" ? [result.reason] : []
461
+ );
462
+ if (failures.length > 0)
463
+ throw new AggregateError(failures, "MCP HTTP server failed to close cleanly");
464
+ }
465
+ };
466
+ }
467
+
468
+ // src/cli.ts
469
+ var defaultIo = {
470
+ out: (text) => process.stdout.write(`${text}
471
+ `),
472
+ err: (text) => process.stderr.write(`${text}
473
+ `)
474
+ };
475
+ function parseArgs(argv) {
476
+ let command = "serve";
477
+ let json = false;
478
+ let http = false;
479
+ let port;
480
+ let host;
481
+ let allowNonLoopback = false;
482
+ let showAuthToken = false;
483
+ let out;
484
+ for (let index = 0; index < argv.length; index += 1) {
485
+ const arg = argv[index] ?? "";
486
+ switch (arg) {
487
+ case "--json":
488
+ json = true;
489
+ break;
490
+ case "--http":
491
+ http = true;
492
+ break;
493
+ case "--port": {
494
+ const value = Number(argv[index + 1]);
495
+ if (!Number.isInteger(value) || value < 0 || value > 65535) {
496
+ throw usageError("--port needs an integer between 0 and 65535");
497
+ }
498
+ port = value;
499
+ index += 1;
500
+ break;
501
+ }
502
+ case "--host":
503
+ host = argv[index + 1];
504
+ if (host === void 0) throw usageError("--host needs a value");
505
+ index += 1;
506
+ break;
507
+ case "--allow-non-loopback":
508
+ allowNonLoopback = true;
509
+ break;
510
+ case "--show-auth-token":
511
+ showAuthToken = true;
512
+ break;
513
+ case "--out":
514
+ out = argv[index + 1];
515
+ if (out === void 0) throw usageError("--out needs a directory");
516
+ index += 1;
517
+ break;
518
+ case "--help":
519
+ case "-h":
520
+ command = "help";
521
+ break;
522
+ case "--version":
523
+ case "-v":
524
+ command = "version";
525
+ break;
526
+ case "serve":
527
+ case "stdio":
528
+ command = "serve";
529
+ break;
530
+ case "agent-context":
531
+ command = "agent-context";
532
+ break;
533
+ case "usage":
534
+ command = "usage";
535
+ break;
536
+ case "skill":
537
+ command = "skill";
538
+ break;
539
+ default:
540
+ throw usageError(
541
+ `unknown argument ${JSON.stringify(arg)}`,
542
+ "run `termwright-mcp usage` for the one-screen cheat sheet"
543
+ );
544
+ }
545
+ }
546
+ return { command, json, http, port, host, allowNonLoopback, showAuthToken, out };
547
+ }
548
+ function httpStartupMessages(args, handle) {
549
+ return [
550
+ `${SERVER_NAME} MCP listening on http://${args.host ?? "127.0.0.1"}:${handle.port}/mcp`,
551
+ args.showAuthToken ? `${SERVER_NAME} MCP bearer token: ${handle.authToken}` : `${SERVER_NAME} MCP bearer token hidden; restart with --show-auth-token to disclose it explicitly`
552
+ ];
553
+ }
554
+ async function runCli(argv, io = defaultIo) {
555
+ let json = argv.includes("--json");
556
+ try {
557
+ const args = parseArgs(argv);
558
+ json = args.json;
559
+ switch (args.command) {
560
+ case "version":
561
+ io.out(
562
+ json ? JSON.stringify({ name: SERVER_NAME, version: SERVER_VERSION }) : SERVER_VERSION
563
+ );
564
+ return EXIT_CODES.ok;
565
+ case "help":
566
+ case "usage":
567
+ io.out(json ? JSON.stringify(buildAgentContext()) : buildUsage());
568
+ return EXIT_CODES.ok;
569
+ case "agent-context":
570
+ io.out(JSON.stringify(buildAgentContext(), null, json ? 0 : 2));
571
+ return EXIT_CODES.ok;
572
+ case "skill": {
573
+ if (args.out === void 0) {
574
+ const files = buildAgentSkill();
575
+ io.out(
576
+ json ? JSON.stringify(Object.fromEntries(files.map((file) => [file.path, file.contents]))) : files.map((file) => `=== ${file.path}
577
+ ${file.contents}`).join("\n")
578
+ );
579
+ return EXIT_CODES.ok;
580
+ }
581
+ const written = await writeAgentSkill(args.out);
582
+ io.out(json ? JSON.stringify({ written }) : written.join("\n"));
583
+ return EXIT_CODES.ok;
584
+ }
585
+ case "serve": {
586
+ if (args.http) {
587
+ const handle = await serveHttp({
588
+ ...args.port === void 0 ? {} : { port: args.port },
589
+ ...args.host === void 0 ? {} : { host: args.host },
590
+ ...args.allowNonLoopback ? { allowNonLoopback: true } : {}
591
+ });
592
+ for (const line of httpStartupMessages(args, handle)) io.err(line);
593
+ await new Promise((resolve) => {
594
+ handle.http.on("close", resolve);
595
+ });
596
+ return EXIT_CODES.ok;
597
+ }
598
+ const running = await serveStdio();
599
+ await new Promise((resolve) => {
600
+ const shutdown = () => {
601
+ void running.close().then(resolve, resolve);
602
+ };
603
+ process.once("SIGINT", shutdown);
604
+ process.once("SIGTERM", shutdown);
605
+ running.server.server.onclose = shutdown;
606
+ });
607
+ return EXIT_CODES.ok;
608
+ }
609
+ }
610
+ } catch (error) {
611
+ const payload = toErrorPayload(error);
612
+ io.err(json ? JSON.stringify(payload) : `${payload.kind}: ${payload.message}`);
613
+ if (!json && payload.suggestion !== void 0) io.err(`suggestion: ${payload.suggestion}`);
614
+ return exitCodeFor(payload.kind);
615
+ }
616
+ }
617
+ async function main() {
618
+ process.exitCode = await runCli(process.argv.slice(2));
619
+ }
620
+
621
+ export {
622
+ createTermwrightMcpServer,
623
+ serveStdio,
624
+ serveInMemory,
625
+ serveHttp,
626
+ runCli,
627
+ main
628
+ };
629
+ //# sourceMappingURL=chunk-R2N52YYH.js.map