@orkestrel/mcp 0.0.34 → 0.0.36

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.
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_process = require("@orkestrel/process");
2
3
  let _src_core = require("../core/index.cjs");
3
4
  let _orkestrel_contract = require("@orkestrel/contract");
4
5
  let _orkestrel_server = require("@orkestrel/server");
@@ -7,10 +8,19 @@ let _orkestrel_websocket = require("@orkestrel/websocket");
7
8
  let node_crypto = require("node:crypto");
8
9
  let node_http = require("node:http");
9
10
  let node_https = require("node:https");
11
+ let node_readline = require("node:readline");
10
12
  let _orkestrel_process_server = require("@orkestrel/process/server");
11
- let _orkestrel_process = require("@orkestrel/process");
12
13
  let node_stream = require("node:stream");
13
14
  //#region src/server/constants.ts
15
+ /**
16
+ * Sets the bound in milliseconds for a stdio server to exit after the client ends its input.
17
+ *
18
+ * @remarks
19
+ * Gives EOF cleanup half the process supervisor's 5,000 ms signal grace before escalation gets
20
+ * its existing full window. This bounds the MCP lifecycle's reasonable-time wait without spending
21
+ * another full signal grace before termination. Pending input flushes share this bound.
22
+ */
23
+ var MCP_STDIO_GRACE = _orkestrel_process.PROCESS_GRACE / 2;
14
24
  /** Names the reverse-proxy response header controlling buffering of an SSE response. */
15
25
  var SSE_BUFFERING_HEADER = "x-accel-buffering";
16
26
  /** Names the `X-Accel-Buffering` value that disables reverse-proxy buffering. */
@@ -298,7 +308,7 @@ function writeLine(output, line) {
298
308
  * Decodes and delivers each complete newline-framed line onto a {@link
299
309
  * MCPMessageTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
300
310
  * transports run their framed lines through: the server transport frames with {@link
301
- * extractLines}, the client transport takes its lines from the process supervisor.
311
+ * extractLines}, the client transport frames the supervisor's stdout with Node's `readline`.
302
312
  *
303
313
  * @remarks
304
314
  * A blank line is skipped (a stray trailing newline). Every other line runs through the
@@ -720,6 +730,8 @@ var HTTPDisconnect = class {
720
730
  * handler through `MCPDispatchOptions.signal`. After every transport validation and immediately
721
731
  * before dispatch, the optional synchronous `caller` extractor reads front-middleware state; a
722
732
  * defined value is added to `MCPDispatchOptions`, while `undefined` is omitted.
733
+ * For legacy `initialize`, the dispatch response is recorded as `initialization` before
734
+ * JSON or SSE framing only when consumer state is an object with `'session' in context.state`.
723
735
  *
724
736
  * @typeParam TState - The consumer's opaque per-request route state type
725
737
  * @param mcp - The transport-agnostic MCP dispatcher to dispatch through
@@ -812,6 +824,9 @@ function createMCPPostHandler(mcp, options) {
812
824
  queueMicrotask(() => void sendEventStream(response, stream));
813
825
  return disconnect.bridge(stream);
814
826
  }
827
+ if (era === "legacy" && invocation.method === "initialize" && (0, _orkestrel_contract.isObject)(context?.state) && "session" in context.state) {
828
+ if (!Reflect.set(context.state, "initialization", response)) throw new Error("MCP initialization state is not writable");
829
+ }
815
830
  const status = inferStatus(response, era);
816
831
  if (response === void 0) return new Response(null, { status });
817
832
  if (status === 200 && streaming && acceptsEventStream(request)) {
@@ -1229,32 +1244,37 @@ var WebSocketClientTransport = class {
1229
1244
  *
1230
1245
  * @remarks
1231
1246
  * - **Composes `@orkestrel/process`.** `start()` builds one supervised
1232
- * {@link import('@orkestrel/process/server').Process} with `writable: true`, so the child's
1247
+ * {@link import('@orkestrel/process/server').Supervisor} with `writable: true`, so the child's
1233
1248
  * `stdin`/`stdout` are the JSON-RPC channel and its `stderr` is retained as bounded evidence
1234
- * rather than parsed as protocol. The supervisor owns spawn, framing, and termination.
1235
- * - **Inbound (`message`).** Standard output is drained eagerly through the supervisor's
1236
- * `readline`-framed `lines` iterable, so a multi-byte UTF-8 sequence split across two reads is
1249
+ * rather than parsed as protocol. The supervisor owns spawn and termination.
1250
+ * - **Inbound (`message`).** Standard output is drained eagerly through Node's `readline`,
1251
+ * so a multi-byte UTF-8 sequence split across two reads is
1237
1252
  * decoded whole and a final line written without a trailing newline still arrives. Each framed
1238
1253
  * line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
1239
1254
  * {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
1240
1255
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
1241
- * through the supervisor's `send` and awaits its answer, so this promise settles only after the
1256
+ * through the supervisor's `deliver` and awaits its answer, so this promise settles only after the
1242
1257
  * host reports the line handled rather than the moment the write is queued. The supervisor never
1243
1258
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, for a write
1244
1259
  * that failed, or for one that remained unconfirmed through `delivery`. A call made without a
1245
1260
  * live child rejects as not connected; a `false` answer from a live child rejects as unable to
1246
1261
  * deliver. The supervisor does not disclose which cause produced that answer.
1247
- * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1248
- * (idempotent). That teardown reaches the child's terminal moment, where the supervisor freezes
1249
- * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1250
- * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1251
- * A line the supervisor had already framed behind the one being delivered is dropped rather than
1262
+ * - **`close()`** ends the child's input and waits up to {@link MCP_STDIO_GRACE} for native exit,
1263
+ * including any pending input flush. Only after that wait does the supervisor terminate a child
1264
+ * that remains alive. Teardown freezes `evidence`, closes the reader, and settles `exit`, then
1265
+ * fires `close` once (idempotent).
1266
+ * A child that exits on input end within the grace receives no termination signal, so it must
1267
+ * end its own child processes on input end; only escalation reaches its process tree.
1268
+ * If an `MCPClient` request `timeout` is shorter than the grace and the close outlasts that
1269
+ * timeout, `disconnect()` rejects with `MCP transport close timed out after <timeout>ms`.
1270
+ * The close keeps running, and a later caller joins it while it remains pending.
1271
+ * A line already framed behind the one being delivered is dropped rather than
1252
1272
  * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1253
1273
  * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1254
1274
  * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1255
1275
  * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1256
- * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1257
- * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1276
+ * bound cuts that wait off independently of the input grace. Escalation belongs to the host:
1277
+ * a POSIX host signals the
1258
1278
  * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1259
1279
  * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1260
1280
  * tree with `taskkill /F /T`, which nothing in the child can intercept.
@@ -1316,7 +1336,7 @@ var StdioClientTransport = class {
1316
1336
  }
1317
1337
  if (this.#process !== void 0 && !this.#closed) return;
1318
1338
  this.#closed = false;
1319
- const child = new _orkestrel_process_server.Process({
1339
+ const child = new _orkestrel_process_server.Supervisor({
1320
1340
  command: {
1321
1341
  file: this.#command,
1322
1342
  arguments: [...this.#args],
@@ -1326,11 +1346,23 @@ var StdioClientTransport = class {
1326
1346
  grace: _orkestrel_process.PROCESS_GRACE,
1327
1347
  delivery: this.#delivery,
1328
1348
  writable: true
1349
+ }, {
1350
+ chunk: () => void 0,
1351
+ fault: (cause) => this.#emitter.emit("error", cause),
1352
+ close: () => reader.close(),
1353
+ terminal: () => void 0,
1354
+ teardown: () => reader.close()
1355
+ });
1356
+ const reader = (0, node_readline.createInterface)({
1357
+ input: child.stdout,
1358
+ crlfDelay: Infinity
1329
1359
  });
1330
1360
  this.#process = child;
1331
- child.emitter.on("error", (cause) => this.#emitter.emit("error", cause));
1361
+ reader.on("line", (line) => {
1362
+ if (this.#closed || this.#process !== child) return;
1363
+ dispatchLines(this.#emitter, [line]);
1364
+ });
1332
1365
  child.exit.then((exit) => this.#onExit(child, exit));
1333
- this.#pump(child);
1334
1366
  }
1335
1367
  /**
1336
1368
  * Sends one newline-delimited JSON-RPC message to the live child.
@@ -1345,7 +1377,7 @@ var StdioClientTransport = class {
1345
1377
  async send(message) {
1346
1378
  const child = this.#closed ? void 0 : this.#process;
1347
1379
  if (child === void 0) throw new Error("stdio transport is not connected");
1348
- if (!await child.send(JSON.stringify(message))) throw new Error("stdio transport could not deliver the message");
1380
+ if (!await child.deliver(Buffer.from(`${JSON.stringify(message)}\n`, "utf8"))) throw new Error("stdio transport could not deliver the message");
1349
1381
  }
1350
1382
  async close() {
1351
1383
  if (this.#closed && this.#closing === void 0) return;
@@ -1357,17 +1389,19 @@ var StdioClientTransport = class {
1357
1389
  this.#closed = true;
1358
1390
  const child = this.#process;
1359
1391
  if (child !== void 0) {
1392
+ const expired = Promise.withResolvers();
1393
+ const timer = setTimeout(expired.resolve, MCP_STDIO_GRACE);
1394
+ try {
1395
+ child.end();
1396
+ await Promise.race([child.ending, expired.promise]);
1397
+ } finally {
1398
+ clearTimeout(timer);
1399
+ }
1360
1400
  await child.destroy();
1361
1401
  this.#report(await child.exit);
1362
1402
  }
1363
1403
  this.#emitter.emit("close");
1364
1404
  }
1365
- async #pump(child) {
1366
- for await (const line of child.lines) {
1367
- if (this.#closed || this.#process !== child) return;
1368
- dispatchLines(this.#emitter, [line]);
1369
- }
1370
- }
1371
1405
  #onExit(child, exit) {
1372
1406
  if (this.#process !== child) return;
1373
1407
  if (this.#closed) return;
@@ -1951,8 +1985,12 @@ function createStdioServer(mcp, options) {
1951
1985
  * live-session request. It then
1952
1986
  * forwards a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
1953
1987
  * already-consumed original — so the route re-reads the same body, and stamps the response
1954
- * with {@link MCP_SESSION_HEADER}. The entry's `touched` instant is read after that
1955
- * downstream response, because it means the last access: a request slower than `ttl` would
1988
+ * with {@link MCP_SESSION_HEADER}. A candidate entry is stored and advertised only when
1989
+ * `context.state.initialization` carries a result. An `initialize` that would mint a session
1990
+ * stores none and advertises none when refused; a live session's header is returned unchanged.
1991
+ * The POST handler records that dispatch response before JSON or SSE framing.
1992
+ * The entry's `touched` instant is read after the downstream response, because it
1993
+ * means the last access: a request slower than `ttl` would
1956
1994
  * otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
1957
1995
  * a `DELETE` arriving while the request was suspended is not undone.
1958
1996
  * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
@@ -2089,7 +2127,7 @@ function createMCPSession(options) {
2089
2127
  signal: request.signal
2090
2128
  }));
2091
2129
  if (created !== void 0) {
2092
- if (!response.ok) return response;
2130
+ if (!response.ok || context.state.initialization?.result === void 0) return response;
2093
2131
  store.set(created.session.id, {
2094
2132
  ...created,
2095
2133
  touched: clock()
@@ -2110,6 +2148,7 @@ exports.DEFAULT_MCP_SESSION_CAPACITY = DEFAULT_MCP_SESSION_CAPACITY;
2110
2148
  exports.DEFAULT_MCP_SESSION_TTL = DEFAULT_MCP_SESSION_TTL;
2111
2149
  exports.HTTPDisconnect = HTTPDisconnect;
2112
2150
  exports.MCPSession = MCPSession;
2151
+ exports.MCP_STDIO_GRACE = MCP_STDIO_GRACE;
2113
2152
  exports.SSE_BUFFERING_DISABLED = SSE_BUFFERING_DISABLED;
2114
2153
  exports.SSE_BUFFERING_HEADER = SSE_BUFFERING_HEADER;
2115
2154
  exports.SSE_KEEPALIVE_COMMENT = SSE_KEEPALIVE_COMMENT;