@orkestrel/mcp 0.0.35 → 0.0.37
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/dist/src/core/index.cjs +50 -6
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +52 -1
- package/dist/src/core/index.d.ts +52 -1
- package/dist/src/core/index.js +50 -7
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +81 -34
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +58 -23
- package/dist/src/server/index.d.ts +58 -23
- package/dist/src/server/index.js +84 -38
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -9
|
@@ -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
|
|
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
|
|
@@ -362,8 +372,8 @@ function inferHeaderTarget(request) {
|
|
|
362
372
|
* {@link import('@orkestrel/mcp').decodeSentinel} before the comparison, so a peer that had
|
|
363
373
|
* to encode its value still matches; a sentinel whose payload is invalid decodes to nothing
|
|
364
374
|
* and therefore mismatches, which is how an invalid header value is refused. A legacy request
|
|
365
|
-
* body requires a protocol header
|
|
366
|
-
* never echo the client-supplied one.
|
|
375
|
+
* body requires a protocol header except for `initialize` and an id-bearing legacy `ping`.
|
|
376
|
+
* Messages name the expected value but never echo the client-supplied one.
|
|
367
377
|
*
|
|
368
378
|
* The expectation a live session supplies is a different rule over a different input, so it
|
|
369
379
|
* is {@link inferSessionHeaderIssue} rather than a second arm of this one.
|
|
@@ -381,7 +391,7 @@ function inferHeaderTarget(request) {
|
|
|
381
391
|
function inferHeaderIssue(request, invocation) {
|
|
382
392
|
const protocol = request.headers.get(_src_core.MCP_PROTOCOL_VERSION_HEADER);
|
|
383
393
|
if (!(0, _src_core.isModernRequest)(invocation)) {
|
|
384
|
-
if ((0, _src_core.isInitializeRequest)(invocation) || protocol !== null) return void 0;
|
|
394
|
+
if ((0, _src_core.isInitializeRequest)(invocation) || (0, _src_core.isPingRequest)(invocation) || protocol !== null) return void 0;
|
|
385
395
|
return {
|
|
386
396
|
header: "MCP-Protocol-Version",
|
|
387
397
|
reason: "missing",
|
|
@@ -709,8 +719,8 @@ var HTTPDisconnect = class {
|
|
|
709
719
|
* comparison; a missing, mismatched, or invalidly encoded value returns HTTP `400` + `-32020`.
|
|
710
720
|
* A protocol header naming a modern revision holds the request to that revision whatever shape
|
|
711
721
|
* its body arrived in, so a body with no parsable modern `_meta` returns HTTP `400` + `-32602`.
|
|
712
|
-
* Headerless `initialize`
|
|
713
|
-
* session to supply its pinned version. A legacy-shaped request carrying a protocol header is
|
|
722
|
+
* Headerless `initialize` and id-bearing legacy `ping` are accepted; other headerless requests
|
|
723
|
+
* need a live legacy session to supply its pinned version. A legacy-shaped request carrying a protocol header is
|
|
714
724
|
* otherwise admitted only for a legacy revision; a revision this server does not implement
|
|
715
725
|
* returns HTTP `400` + `-32022` whose `supported` names the legacy revisions this door accepts.
|
|
716
726
|
* A present origin must occur in `origin.origins` unless validation is
|
|
@@ -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').
|
|
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
|
|
1235
|
-
* - **Inbound (`message`).** Standard output is drained eagerly through
|
|
1236
|
-
*
|
|
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 `
|
|
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()`**
|
|
1248
|
-
*
|
|
1249
|
-
* `evidence`,
|
|
1250
|
-
*
|
|
1251
|
-
* A
|
|
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
|
|
1257
|
-
*
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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;
|
|
@@ -1945,14 +1979,20 @@ function createStdioServer(mcp, options) {
|
|
|
1945
1979
|
* readSessionHeader}: a valid id touches the entry and sets `context.state.session`; an
|
|
1946
1980
|
* absent / unknown id whose (guarded) body parses to an `initialize` request ({@link
|
|
1947
1981
|
* isInitializeRequest}) mints a fresh {@link MCPSession} (`crypto.randomUUID()`, the `session`
|
|
1948
|
-
* options group) and sets `context.state.session
|
|
1982
|
+
* options group) and sets `context.state.session`. An id-bearing legacy `ping` with no
|
|
1983
|
+
* session header passes through without session state, a supplied protocol header, or a
|
|
1984
|
+
* response stamp. Other unresolved requests → {@link rejectUnknownSession}
|
|
1949
1985
|
* (`404`). The
|
|
1950
1986
|
* minted entry pins the negotiated legacy revision, which is supplied to a later headerless
|
|
1951
1987
|
* live-session request. It then
|
|
1952
1988
|
* forwards a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
|
|
1953
1989
|
* already-consumed original — so the route re-reads the same body, and stamps the response
|
|
1954
|
-
* with {@link MCP_SESSION_HEADER}.
|
|
1955
|
-
*
|
|
1990
|
+
* with {@link MCP_SESSION_HEADER}. A candidate entry is stored and advertised only when
|
|
1991
|
+
* `context.state.initialization` carries a result. An `initialize` that would mint a session
|
|
1992
|
+
* stores none and advertises none when refused; a live session's header is returned unchanged.
|
|
1993
|
+
* The POST handler records that dispatch response before JSON or SSE framing.
|
|
1994
|
+
* The entry's `touched` instant is read after the downstream response, because it
|
|
1995
|
+
* means the last access: a request slower than `ttl` would
|
|
1956
1996
|
* otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
|
|
1957
1997
|
* a `DELETE` arriving while the request was suspended is not undone.
|
|
1958
1998
|
* - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
|
|
@@ -2070,7 +2110,13 @@ function createMCPSession(options) {
|
|
|
2070
2110
|
version: inferLegacyVersion(parsed)
|
|
2071
2111
|
};
|
|
2072
2112
|
entry = created;
|
|
2073
|
-
} else return
|
|
2113
|
+
} else if (id === void 0 && (0, _src_core.isPingRequest)(parsed)) return next(new Request(context.url, {
|
|
2114
|
+
method: "POST",
|
|
2115
|
+
headers: request.headers,
|
|
2116
|
+
body: text,
|
|
2117
|
+
signal: request.signal
|
|
2118
|
+
}));
|
|
2119
|
+
else return rejectUnknownSession();
|
|
2074
2120
|
}
|
|
2075
2121
|
if (!Reflect.set(context.state, "session", entry.session)) throw new Error("MCP session state is not writable");
|
|
2076
2122
|
const headers = new Headers(request.headers);
|
|
@@ -2089,7 +2135,7 @@ function createMCPSession(options) {
|
|
|
2089
2135
|
signal: request.signal
|
|
2090
2136
|
}));
|
|
2091
2137
|
if (created !== void 0) {
|
|
2092
|
-
if (!response.ok) return response;
|
|
2138
|
+
if (!response.ok || context.state.initialization?.result === void 0) return response;
|
|
2093
2139
|
store.set(created.session.id, {
|
|
2094
2140
|
...created,
|
|
2095
2141
|
touched: clock()
|
|
@@ -2110,6 +2156,7 @@ exports.DEFAULT_MCP_SESSION_CAPACITY = DEFAULT_MCP_SESSION_CAPACITY;
|
|
|
2110
2156
|
exports.DEFAULT_MCP_SESSION_TTL = DEFAULT_MCP_SESSION_TTL;
|
|
2111
2157
|
exports.HTTPDisconnect = HTTPDisconnect;
|
|
2112
2158
|
exports.MCPSession = MCPSession;
|
|
2159
|
+
exports.MCP_STDIO_GRACE = MCP_STDIO_GRACE;
|
|
2113
2160
|
exports.SSE_BUFFERING_DISABLED = SSE_BUFFERING_DISABLED;
|
|
2114
2161
|
exports.SSE_BUFFERING_HEADER = SSE_BUFFERING_HEADER;
|
|
2115
2162
|
exports.SSE_KEEPALIVE_COMMENT = SSE_KEEPALIVE_COMMENT;
|