@orkestrel/mcp 0.0.23 → 0.0.25

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.
@@ -100,6 +100,24 @@ var DEFAULT_MCP_DELIVERY = 1e4;
100
100
  //#endregion
101
101
  //#region src/server/helpers.ts
102
102
  /**
103
+ * Builds the error for a non-success HTTP response that carried no JSON-RPC message.
104
+ *
105
+ * @param response - The response whose status is reported
106
+ * @param type - The response's content type, or an empty string when absent
107
+ * @returns An error naming the HTTP status and unsupported response shape
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const error = buildResponseError(new Response('', { status: 500 }), '')
112
+ * ```
113
+ */
114
+ function buildResponseError(response, type) {
115
+ if (type.includes("application/json")) return /* @__PURE__ */ new Error(`HTTP ${response.status} response contained an application/json body that was not a JSON-RPC message`);
116
+ if (type.includes("text/event-stream")) return /* @__PURE__ */ new Error(`HTTP ${response.status} response contained a text/event-stream body without a JSON-RPC message`);
117
+ const shape = type === "" ? "a body without a content type" : `an unsupported '${type}' body`;
118
+ return /* @__PURE__ */ new Error(`HTTP ${response.status} response contained ${shape}`);
119
+ }
120
+ /**
103
121
  * Creates a readable stream from its pull and cancellation behaviours.
104
122
  *
105
123
  * @param pull - The behaviour that supplies the stream's next chunk
@@ -371,6 +389,14 @@ function extractLines(buffer, chunk) {
371
389
  * The completion callback is the writable channel's backpressure boundary. A callback error and
372
390
  * a synchronous `write` throw reject the returned promise with the original value.
373
391
  *
392
+ * That callback is the ONLY thing that settles the promise: this helper holds no timer and no
393
+ * abort, so an output that neither confirms nor fails the write parks the promise for as long as
394
+ * the caller-owned stream holds the callback. A caller wanting a bound races this promise against
395
+ * one it owns — {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
396
+ * registers such a bound per send and rejects it on `close()`, so closing the transport settles
397
+ * the CALLER's `send` while the abandoned write stays with the stream that still holds its
398
+ * callback, reachable from nothing the transport retains.
399
+ *
374
400
  * @param output - The writable stream that receives the line
375
401
  * @param line - The complete line to write
376
402
  * @returns Resolves when the stream confirms the write; rejects when the write fails
@@ -539,7 +565,7 @@ function inferHeaderIssue(request, reference) {
539
565
  return {
540
566
  header: "MCP-Protocol-Version",
541
567
  reason: "missing",
542
- message: `Required MCP-Protocol-Version header is missing; this server offers '${_src_core.MCP_PROTOCOL_VERSION}'.`
568
+ message: `Required MCP-Protocol-Version header is missing; this server offers '${_src_core.MCP_HANDSHAKE_VERSION}'.`
543
569
  };
544
570
  }
545
571
  const message = reference;
@@ -586,16 +612,20 @@ function inferHeaderIssue(request, reference) {
586
612
  *
587
613
  * @remarks
588
614
  * A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
589
- * request selects the newest supported legacy revision, matching the core initialize result.
615
+ * request selects the newest supported legacy revision. The read is deliberately the SAME one
616
+ * {@link import('@orkestrel/mcp').buildInitializeResult} performs — `isMCPLegacyVersion` over
617
+ * the requested revision — because the session version this pins and the version that result
618
+ * echoes must be the one value. Routing through `inferVersion` cannot do it: that inferer is
619
+ * modern-only, so it answers `undefined` for every legacy offer and the session would pin
620
+ * `2025-11-25` while the handshake echoed `2025-06-18`, which the client's own protocol
621
+ * header then contradicts.
590
622
  *
591
623
  * @param request - The legacy initialize invocation
592
624
  * @returns The negotiated legacy protocol revision
593
625
  */
594
626
  function inferLegacyVersion(request) {
595
627
  const requested = request.params?.["protocolVersion"];
596
- const version = (0, _src_core.inferVersion)((0, _orkestrel_contract.isString)(requested) ? [requested] : []);
597
- if (version !== void 0 && (0, _src_core.inferEra)(version) === "legacy") return version;
598
- return _src_core.MCP_PROTOCOL_VERSION;
628
+ return (0, _src_core.isMCPLegacyVersion)(requested) ? requested : _src_core.MCP_HANDSHAKE_VERSION;
599
629
  }
600
630
  /**
601
631
  * Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body.
@@ -758,7 +788,9 @@ var HTTPDisconnect = class {
758
788
  * Modern requests require matching protocol/method headers and a matching name header only
759
789
  * for `tools/call`; mismatch returns HTTP `400` + `-32020`. Headerless `initialize` is
760
790
  * accepted, while every other headerless request needs a live legacy session to supply its
761
- * pinned version. A present origin must occur in `origin.origins` unless validation is
791
+ * pinned version. A legacy-shaped request carrying a protocol header is admitted only for a
792
+ * legacy revision; any other value, the modern revision included, returns HTTP `400` + `-32022`
793
+ * whose `supported` names the legacy revisions this door accepts. A present origin must occur in `origin.origins` unless validation is
762
794
  * explicitly delegated upstream. Modern dispatch errors use their protocol status map; legacy
763
795
  * errors remain in-band at HTTP `200`. A streamed response composes the fetch-standard request
764
796
  * signal with response-body cancellation and supplies the result to every dispatched modern
@@ -813,8 +845,8 @@ function createMCPPostHandler(mcp, options) {
813
845
  const issue = inferHeaderIssue(request, invocation);
814
846
  if (issue !== void 0) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_HEADER_MISMATCH, issue.message), { status: 400 });
815
847
  if (era === "legacy") {
816
- if (protocol !== null && !(0, _src_core.isMCPVersion)(protocol)) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_UNSUPPORTED_VERSION, `Unsupported MCP protocol version '${protocol}'`, {
817
- supported: _src_core.SUPPORTED_PROTOCOL_VERSIONS,
848
+ if (protocol !== null && !(0, _src_core.isMCPLegacyVersion)(protocol)) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_UNSUPPORTED_VERSION, `Unsupported MCP protocol version '${protocol}'`, {
849
+ supported: _src_core.SUPPORTED_LEGACY_PROTOCOL_VERSIONS,
818
850
  requested: protocol
819
851
  }), { status: 400 });
820
852
  }
@@ -882,8 +914,10 @@ function createMCPPostHandler(mcp, options) {
882
914
  * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
883
915
  * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
884
916
  * - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
885
- * the SSE decoder) a non-message reply is dropped, never asserted; a `fetch` /
886
- * decode failure surfaces on the `error` event rather than escaping `send`.
917
+ * the SSE decoder). A non-message success reply is dropped, never asserted. A non-success
918
+ * reply that carries no valid JSON-RPC message rejects `send` with its HTTP status and body
919
+ * shape. A valid JSON-RPC error body is emitted at any HTTP status. A `fetch` / decode failure
920
+ * on a success response surfaces on the `error` event rather than escaping `send`.
887
921
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
888
922
  * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
889
923
  *
@@ -978,18 +1012,20 @@ var HTTPClientTransport = class {
978
1012
  async #deliver(response) {
979
1013
  if (response.status === 202) return;
980
1014
  const type = response.headers.get("content-type") ?? "";
1015
+ let messages = [];
1016
+ let failure;
981
1017
  try {
982
- if (type.includes("text/event-stream")) {
983
- for (const message of await readEventStream(response)) this.#capture(message);
984
- return;
985
- }
986
- if (type.includes("application/json")) {
1018
+ if (type.includes("text/event-stream")) messages = await readEventStream(response);
1019
+ else if (type.includes("application/json")) {
987
1020
  const message = (0, _src_core.parseJSONRPCMessage)(await response.json());
988
- if (message !== void 0) this.#capture(message);
1021
+ if (message !== void 0) messages = [message];
989
1022
  }
990
1023
  } catch (error) {
991
- this.#emitter.emit("error", error);
1024
+ failure = { error };
992
1025
  }
1026
+ for (const message of messages) this.#capture(message);
1027
+ if (!response.ok && messages.length === 0) throw buildResponseError(response, type);
1028
+ if (failure !== void 0) this.#emitter.emit("error", failure.error);
993
1029
  }
994
1030
  #capture(message) {
995
1031
  if ((0, _src_core.isJSONRPCResponse)(message) && (0, _orkestrel_contract.isRecord)(message.result) && (0, _src_core.isMCPVersion)(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
@@ -1240,7 +1276,11 @@ var WebSocketServerTransport = class {
1240
1276
  * event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
1241
1277
  * non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
1242
1278
  * / `error` bridge to this transport's events.
1243
- * - **Outbound (`send`).** `send(message)` writes one masked text frame.
1279
+ * - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
1280
+ * confirmed, so this transport answers a closed channel from its OWN state: a `send` with no
1281
+ * bound socket — before `start()`, after `close()`, or after the peer ended the socket —
1282
+ * REJECTS with `WebSocket transport is not connected`. It neither drops the message (the
1283
+ * browser face's posture) nor queues it for a connection this transport is not holding.
1244
1284
  * - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
1245
1285
  * upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
1246
1286
  * transport at once instead of waiting for a peer that may never answer — the suspended
@@ -1456,7 +1496,7 @@ var WebSocketClientTransport = class {
1456
1496
  * ```ts
1457
1497
  * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })
1458
1498
  * const client = new MCPClient({ transport })
1459
- * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
1499
+ * await client.connect() // start() spawns the child, then modern discovery runs over stdio
1460
1500
  * ```
1461
1501
  */
1462
1502
  var StdioClientTransport = class {
@@ -2216,6 +2256,7 @@ exports.WebSocketServerTransport = WebSocketServerTransport;
2216
2256
  exports.acceptsEventStream = acceptsEventStream;
2217
2257
  exports.allowsOrigin = allowsOrigin;
2218
2258
  exports.bridgeMessageTransport = bridgeMessageTransport;
2259
+ exports.buildResponseError = buildResponseError;
2219
2260
  exports.createHTTPClientTransport = createHTTPClientTransport;
2220
2261
  exports.createMCPContinuation = createMCPContinuation;
2221
2262
  exports.createMCPPostHandler = createMCPPostHandler;