@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.
@@ -11,6 +11,7 @@ import { MCPClientTransportInterface as MCPClientTransportInterface_2 } from '@o
11
11
  import { MCPContinuationInterface } from '@orkestrel/mcp';
12
12
  import { MCPDispatcherInterface } from '@orkestrel/mcp';
13
13
  import { MCPEra } from '@orkestrel/mcp';
14
+ import { MCPLegacyVersion } from '@orkestrel/mcp';
14
15
  import { MCPStreamControllerInterface } from '@orkestrel/mcp';
15
16
  import { MCPTransportInterface } from '@orkestrel/mcp';
16
17
  import { MCPVersion } from '@orkestrel/mcp';
@@ -108,6 +109,20 @@ export declare function allowsOrigin(request: Request, options?: MCPOriginOption
108
109
  */
109
110
  export declare function bridgeMessageTransport(transport: MCPClientTransportInterface): MCPTransportInterface;
110
111
 
112
+ /**
113
+ * Builds the error for a non-success HTTP response that carried no JSON-RPC message.
114
+ *
115
+ * @param response - The response whose status is reported
116
+ * @param type - The response's content type, or an empty string when absent
117
+ * @returns An error naming the HTTP status and unsupported response shape
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const error = buildResponseError(new Response('', { status: 500 }), '')
122
+ * ```
123
+ */
124
+ export declare function buildResponseError(response: Response, type: string): Error;
125
+
111
126
  /**
112
127
  * Creates the HTTP CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
113
128
  * — a {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
@@ -160,7 +175,9 @@ export declare function createMCPContinuation(secret: TokenSecret): MCPContinuat
160
175
  * Modern requests require matching protocol/method headers and a matching name header only
161
176
  * for `tools/call`; mismatch returns HTTP `400` + `-32020`. Headerless `initialize` is
162
177
  * accepted, while every other headerless request needs a live legacy session to supply its
163
- * pinned version. A present origin must occur in `origin.origins` unless validation is
178
+ * pinned version. A legacy-shaped request carrying a protocol header is admitted only for a
179
+ * legacy revision; any other value, the modern revision included, returns HTTP `400` + `-32022`
180
+ * whose `supported` names the legacy revisions this door accepts. A present origin must occur in `origin.origins` unless validation is
164
181
  * explicitly delegated upstream. Modern dispatch errors use their protocol status map; legacy
165
182
  * errors remain in-band at HTTP `200`. A streamed response composes the fetch-standard request
166
183
  * signal with response-body cancellation and supplies the result to every dispatched modern
@@ -648,8 +665,10 @@ export declare function extractLines(buffer: string, chunk: string): LineExtract
648
665
  * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
649
666
  * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
650
667
  * - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
651
- * the SSE decoder) a non-message reply is dropped, never asserted; a `fetch` /
652
- * decode failure surfaces on the `error` event rather than escaping `send`.
668
+ * the SSE decoder). A non-message success reply is dropped, never asserted. A non-success
669
+ * reply that carries no valid JSON-RPC message rejects `send` with its HTTP status and body
670
+ * shape. A valid JSON-RPC error body is emitted at any HTTP status. A `fetch` / decode failure
671
+ * on a success response surfaces on the `error` event rather than escaping `send`.
653
672
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
654
673
  * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
655
674
  *
@@ -840,12 +859,18 @@ export declare function inferHeaderIssue(request: Request, reference: JSONRPCInv
840
859
  *
841
860
  * @remarks
842
861
  * A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
843
- * request selects the newest supported legacy revision, matching the core initialize result.
862
+ * request selects the newest supported legacy revision. The read is deliberately the SAME one
863
+ * {@link import('@orkestrel/mcp').buildInitializeResult} performs — `isMCPLegacyVersion` over
864
+ * the requested revision — because the session version this pins and the version that result
865
+ * echoes must be the one value. Routing through `inferVersion` cannot do it: that inferer is
866
+ * modern-only, so it answers `undefined` for every legacy offer and the session would pin
867
+ * `2025-11-25` while the handshake echoed `2025-06-18`, which the client's own protocol
868
+ * header then contradicts.
844
869
  *
845
870
  * @param request - The legacy initialize invocation
846
871
  * @returns The negotiated legacy protocol revision
847
872
  */
848
- export declare function inferLegacyVersion(request: JSONRPCInvocation): MCPVersion;
873
+ export declare function inferLegacyVersion(request: JSONRPCInvocation): MCPLegacyVersion;
849
874
 
850
875
  /**
851
876
  * Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body.
@@ -1314,7 +1339,7 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1314
1339
  * ```ts
1315
1340
  * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })
1316
1341
  * const client = new MCPClient({ transport })
1317
- * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
1342
+ * await client.connect() // start() spawns the child, then modern discovery runs over stdio
1318
1343
  * ```
1319
1344
  */
1320
1345
  export declare class StdioClientTransport implements StdioClientTransportInterface {
@@ -1571,7 +1596,11 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
1571
1596
  * event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
1572
1597
  * non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
1573
1598
  * / `error` bridge to this transport's events.
1574
- * - **Outbound (`send`).** `send(message)` writes one masked text frame.
1599
+ * - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
1600
+ * confirmed, so this transport answers a closed channel from its OWN state: a `send` with no
1601
+ * bound socket — before `start()`, after `close()`, or after the peer ended the socket —
1602
+ * REJECTS with `WebSocket transport is not connected`. It neither drops the message (the
1603
+ * browser face's posture) nor queues it for a connection this transport is not holding.
1575
1604
  * - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
1576
1605
  * upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
1577
1606
  * transport at once instead of waiting for a peer that may never answer — the suspended
@@ -1706,6 +1735,14 @@ export declare class WebSocketServerTransport implements MCPClientTransportInter
1706
1735
  * The completion callback is the writable channel's backpressure boundary. A callback error and
1707
1736
  * a synchronous `write` throw reject the returned promise with the original value.
1708
1737
  *
1738
+ * That callback is the ONLY thing that settles the promise: this helper holds no timer and no
1739
+ * abort, so an output that neither confirms nor fails the write parks the promise for as long as
1740
+ * the caller-owned stream holds the callback. A caller wanting a bound races this promise against
1741
+ * one it owns — {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
1742
+ * registers such a bound per send and rejects it on `close()`, so closing the transport settles
1743
+ * the CALLER's `send` while the abandoned write stays with the stream that still holds its
1744
+ * callback, reachable from nothing the transport retains.
1745
+ *
1709
1746
  * @param output - The writable stream that receives the line
1710
1747
  * @param line - The complete line to write
1711
1748
  * @returns Resolves when the stream confirms the write; rejects when the write fails
@@ -11,6 +11,7 @@ import { MCPClientTransportInterface as MCPClientTransportInterface_2 } from '@o
11
11
  import { MCPContinuationInterface } from '@orkestrel/mcp';
12
12
  import { MCPDispatcherInterface } from '@orkestrel/mcp';
13
13
  import { MCPEra } from '@orkestrel/mcp';
14
+ import { MCPLegacyVersion } from '@orkestrel/mcp';
14
15
  import { MCPStreamControllerInterface } from '@orkestrel/mcp';
15
16
  import { MCPTransportInterface } from '@orkestrel/mcp';
16
17
  import { MCPVersion } from '@orkestrel/mcp';
@@ -108,6 +109,20 @@ export declare function allowsOrigin(request: Request, options?: MCPOriginOption
108
109
  */
109
110
  export declare function bridgeMessageTransport(transport: MCPClientTransportInterface): MCPTransportInterface;
110
111
 
112
+ /**
113
+ * Builds the error for a non-success HTTP response that carried no JSON-RPC message.
114
+ *
115
+ * @param response - The response whose status is reported
116
+ * @param type - The response's content type, or an empty string when absent
117
+ * @returns An error naming the HTTP status and unsupported response shape
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const error = buildResponseError(new Response('', { status: 500 }), '')
122
+ * ```
123
+ */
124
+ export declare function buildResponseError(response: Response, type: string): Error;
125
+
111
126
  /**
112
127
  * Creates the HTTP CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
113
128
  * — a {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
@@ -160,7 +175,9 @@ export declare function createMCPContinuation(secret: TokenSecret): MCPContinuat
160
175
  * Modern requests require matching protocol/method headers and a matching name header only
161
176
  * for `tools/call`; mismatch returns HTTP `400` + `-32020`. Headerless `initialize` is
162
177
  * accepted, while every other headerless request needs a live legacy session to supply its
163
- * pinned version. A present origin must occur in `origin.origins` unless validation is
178
+ * pinned version. A legacy-shaped request carrying a protocol header is admitted only for a
179
+ * legacy revision; any other value, the modern revision included, returns HTTP `400` + `-32022`
180
+ * whose `supported` names the legacy revisions this door accepts. A present origin must occur in `origin.origins` unless validation is
164
181
  * explicitly delegated upstream. Modern dispatch errors use their protocol status map; legacy
165
182
  * errors remain in-band at HTTP `200`. A streamed response composes the fetch-standard request
166
183
  * signal with response-body cancellation and supplies the result to every dispatched modern
@@ -648,8 +665,10 @@ export declare function extractLines(buffer: string, chunk: string): LineExtract
648
665
  * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
649
666
  * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
650
667
  * - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
651
- * the SSE decoder) a non-message reply is dropped, never asserted; a `fetch` /
652
- * decode failure surfaces on the `error` event rather than escaping `send`.
668
+ * the SSE decoder). A non-message success reply is dropped, never asserted. A non-success
669
+ * reply that carries no valid JSON-RPC message rejects `send` with its HTTP status and body
670
+ * shape. A valid JSON-RPC error body is emitted at any HTTP status. A `fetch` / decode failure
671
+ * on a success response surfaces on the `error` event rather than escaping `send`.
653
672
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
654
673
  * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
655
674
  *
@@ -840,12 +859,18 @@ export declare function inferHeaderIssue(request: Request, reference: JSONRPCInv
840
859
  *
841
860
  * @remarks
842
861
  * A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
843
- * request selects the newest supported legacy revision, matching the core initialize result.
862
+ * request selects the newest supported legacy revision. The read is deliberately the SAME one
863
+ * {@link import('@orkestrel/mcp').buildInitializeResult} performs — `isMCPLegacyVersion` over
864
+ * the requested revision — because the session version this pins and the version that result
865
+ * echoes must be the one value. Routing through `inferVersion` cannot do it: that inferer is
866
+ * modern-only, so it answers `undefined` for every legacy offer and the session would pin
867
+ * `2025-11-25` while the handshake echoed `2025-06-18`, which the client's own protocol
868
+ * header then contradicts.
844
869
  *
845
870
  * @param request - The legacy initialize invocation
846
871
  * @returns The negotiated legacy protocol revision
847
872
  */
848
- export declare function inferLegacyVersion(request: JSONRPCInvocation): MCPVersion;
873
+ export declare function inferLegacyVersion(request: JSONRPCInvocation): MCPLegacyVersion;
849
874
 
850
875
  /**
851
876
  * Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body.
@@ -1314,7 +1339,7 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1314
1339
  * ```ts
1315
1340
  * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })
1316
1341
  * const client = new MCPClient({ transport })
1317
- * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
1342
+ * await client.connect() // start() spawns the child, then modern discovery runs over stdio
1318
1343
  * ```
1319
1344
  */
1320
1345
  export declare class StdioClientTransport implements StdioClientTransportInterface {
@@ -1571,7 +1596,11 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
1571
1596
  * event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
1572
1597
  * non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
1573
1598
  * / `error` bridge to this transport's events.
1574
- * - **Outbound (`send`).** `send(message)` writes one masked text frame.
1599
+ * - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
1600
+ * confirmed, so this transport answers a closed channel from its OWN state: a `send` with no
1601
+ * bound socket — before `start()`, after `close()`, or after the peer ended the socket —
1602
+ * REJECTS with `WebSocket transport is not connected`. It neither drops the message (the
1603
+ * browser face's posture) nor queues it for a connection this transport is not holding.
1575
1604
  * - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
1576
1605
  * upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
1577
1606
  * transport at once instead of waiting for a peer that may never answer — the suspended
@@ -1706,6 +1735,14 @@ export declare class WebSocketServerTransport implements MCPClientTransportInter
1706
1735
  * The completion callback is the writable channel's backpressure boundary. A callback error and
1707
1736
  * a synchronous `write` throw reject the returned promise with the original value.
1708
1737
  *
1738
+ * That callback is the ONLY thing that settles the promise: this helper holds no timer and no
1739
+ * abort, so an output that neither confirms nor fails the write parks the promise for as long as
1740
+ * the caller-owned stream holds the callback. A caller wanting a bound races this promise against
1741
+ * one it owns — {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
1742
+ * registers such a bound per send and rejects it on `close()`, so closing the transport settles
1743
+ * the CALLER's `send` while the abandoned write stays with the stream that still holds its
1744
+ * callback, reachable from nothing the transport retains.
1745
+ *
1709
1746
  * @param output - The writable stream that receives the line
1710
1747
  * @param line - The complete line to write
1711
1748
  * @returns Resolves when the stream confirms the write; rejects when the write fails
@@ -1,5 +1,5 @@
1
1
  import { createSSEParser } from "@orkestrel/sse";
2
- import { JSONRPC_INVALID_PARAMS, JSONRPC_INVALID_REQUEST, JSONRPC_METHOD_NOT_FOUND, JSONRPC_PARSE_ERROR, MCP_HEADER_MISMATCH, MCP_META_VERSION, MCP_MISSING_CAPABILITY, MCP_PROTOCOL_VERSION, MCP_UNSUPPORTED_VERSION, SUPPORTED_PROTOCOL_VERSIONS, bindServer, buildJSONRPCError, inferEra, inferRequestVersion, inferVersion, isInitializeRequest, isJSONRPCInvocation, isJSONRPCResponse, isMCPVersion, isModernRequest, parseJSONRPCMessage, parseRequestContext } from "../core/index.js";
2
+ import { JSONRPC_INVALID_PARAMS, JSONRPC_INVALID_REQUEST, JSONRPC_METHOD_NOT_FOUND, JSONRPC_PARSE_ERROR, MCP_HANDSHAKE_VERSION, MCP_HEADER_MISMATCH, MCP_META_VERSION, MCP_MISSING_CAPABILITY, MCP_UNSUPPORTED_VERSION, SUPPORTED_LEGACY_PROTOCOL_VERSIONS, bindServer, buildJSONRPCError, inferRequestVersion, isInitializeRequest, isJSONRPCInvocation, isJSONRPCResponse, isMCPLegacyVersion, isMCPVersion, isModernRequest, parseJSONRPCMessage, parseRequestContext } from "../core/index.js";
3
3
  import { isRecord, isString, sanitizeBudget } from "@orkestrel/contract";
4
4
  import { openStream, signToken, verifyToken } from "@orkestrel/server";
5
5
  import { Emitter } from "@orkestrel/emitter";
@@ -99,6 +99,24 @@ var DEFAULT_MCP_DELIVERY = 1e4;
99
99
  //#endregion
100
100
  //#region src/server/helpers.ts
101
101
  /**
102
+ * Builds the error for a non-success HTTP response that carried no JSON-RPC message.
103
+ *
104
+ * @param response - The response whose status is reported
105
+ * @param type - The response's content type, or an empty string when absent
106
+ * @returns An error naming the HTTP status and unsupported response shape
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const error = buildResponseError(new Response('', { status: 500 }), '')
111
+ * ```
112
+ */
113
+ function buildResponseError(response, type) {
114
+ 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`);
115
+ 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`);
116
+ const shape = type === "" ? "a body without a content type" : `an unsupported '${type}' body`;
117
+ return /* @__PURE__ */ new Error(`HTTP ${response.status} response contained ${shape}`);
118
+ }
119
+ /**
102
120
  * Creates a readable stream from its pull and cancellation behaviours.
103
121
  *
104
122
  * @param pull - The behaviour that supplies the stream's next chunk
@@ -370,6 +388,14 @@ function extractLines(buffer, chunk) {
370
388
  * The completion callback is the writable channel's backpressure boundary. A callback error and
371
389
  * a synchronous `write` throw reject the returned promise with the original value.
372
390
  *
391
+ * That callback is the ONLY thing that settles the promise: this helper holds no timer and no
392
+ * abort, so an output that neither confirms nor fails the write parks the promise for as long as
393
+ * the caller-owned stream holds the callback. A caller wanting a bound races this promise against
394
+ * one it owns — {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
395
+ * registers such a bound per send and rejects it on `close()`, so closing the transport settles
396
+ * the CALLER's `send` while the abandoned write stays with the stream that still holds its
397
+ * callback, reachable from nothing the transport retains.
398
+ *
373
399
  * @param output - The writable stream that receives the line
374
400
  * @param line - The complete line to write
375
401
  * @returns Resolves when the stream confirms the write; rejects when the write fails
@@ -538,7 +564,7 @@ function inferHeaderIssue(request, reference) {
538
564
  return {
539
565
  header: "MCP-Protocol-Version",
540
566
  reason: "missing",
541
- message: `Required MCP-Protocol-Version header is missing; this server offers '${MCP_PROTOCOL_VERSION}'.`
567
+ message: `Required MCP-Protocol-Version header is missing; this server offers '${MCP_HANDSHAKE_VERSION}'.`
542
568
  };
543
569
  }
544
570
  const message = reference;
@@ -585,16 +611,20 @@ function inferHeaderIssue(request, reference) {
585
611
  *
586
612
  * @remarks
587
613
  * A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
588
- * request selects the newest supported legacy revision, matching the core initialize result.
614
+ * request selects the newest supported legacy revision. The read is deliberately the SAME one
615
+ * {@link import('@orkestrel/mcp').buildInitializeResult} performs — `isMCPLegacyVersion` over
616
+ * the requested revision — because the session version this pins and the version that result
617
+ * echoes must be the one value. Routing through `inferVersion` cannot do it: that inferer is
618
+ * modern-only, so it answers `undefined` for every legacy offer and the session would pin
619
+ * `2025-11-25` while the handshake echoed `2025-06-18`, which the client's own protocol
620
+ * header then contradicts.
589
621
  *
590
622
  * @param request - The legacy initialize invocation
591
623
  * @returns The negotiated legacy protocol revision
592
624
  */
593
625
  function inferLegacyVersion(request) {
594
626
  const requested = request.params?.["protocolVersion"];
595
- const version = inferVersion(isString(requested) ? [requested] : []);
596
- if (version !== void 0 && inferEra(version) === "legacy") return version;
597
- return MCP_PROTOCOL_VERSION;
627
+ return isMCPLegacyVersion(requested) ? requested : MCP_HANDSHAKE_VERSION;
598
628
  }
599
629
  /**
600
630
  * Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body.
@@ -757,7 +787,9 @@ var HTTPDisconnect = class {
757
787
  * Modern requests require matching protocol/method headers and a matching name header only
758
788
  * for `tools/call`; mismatch returns HTTP `400` + `-32020`. Headerless `initialize` is
759
789
  * accepted, while every other headerless request needs a live legacy session to supply its
760
- * pinned version. A present origin must occur in `origin.origins` unless validation is
790
+ * pinned version. A legacy-shaped request carrying a protocol header is admitted only for a
791
+ * legacy revision; any other value, the modern revision included, returns HTTP `400` + `-32022`
792
+ * whose `supported` names the legacy revisions this door accepts. A present origin must occur in `origin.origins` unless validation is
761
793
  * explicitly delegated upstream. Modern dispatch errors use their protocol status map; legacy
762
794
  * errors remain in-band at HTTP `200`. A streamed response composes the fetch-standard request
763
795
  * signal with response-body cancellation and supplies the result to every dispatched modern
@@ -812,8 +844,8 @@ function createMCPPostHandler(mcp, options) {
812
844
  const issue = inferHeaderIssue(request, invocation);
813
845
  if (issue !== void 0) return Response.json(buildJSONRPCError(id, MCP_HEADER_MISMATCH, issue.message), { status: 400 });
814
846
  if (era === "legacy") {
815
- if (protocol !== null && !isMCPVersion(protocol)) return Response.json(buildJSONRPCError(id, MCP_UNSUPPORTED_VERSION, `Unsupported MCP protocol version '${protocol}'`, {
816
- supported: SUPPORTED_PROTOCOL_VERSIONS,
847
+ if (protocol !== null && !isMCPLegacyVersion(protocol)) return Response.json(buildJSONRPCError(id, MCP_UNSUPPORTED_VERSION, `Unsupported MCP protocol version '${protocol}'`, {
848
+ supported: SUPPORTED_LEGACY_PROTOCOL_VERSIONS,
817
849
  requested: protocol
818
850
  }), { status: 400 });
819
851
  }
@@ -881,8 +913,10 @@ function createMCPPostHandler(mcp, options) {
881
913
  * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
882
914
  * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
883
915
  * - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
884
- * the SSE decoder) a non-message reply is dropped, never asserted; a `fetch` /
885
- * decode failure surfaces on the `error` event rather than escaping `send`.
916
+ * the SSE decoder). A non-message success reply is dropped, never asserted. A non-success
917
+ * reply that carries no valid JSON-RPC message rejects `send` with its HTTP status and body
918
+ * shape. A valid JSON-RPC error body is emitted at any HTTP status. A `fetch` / decode failure
919
+ * on a success response surfaces on the `error` event rather than escaping `send`.
886
920
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
887
921
  * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
888
922
  *
@@ -977,18 +1011,20 @@ var HTTPClientTransport = class {
977
1011
  async #deliver(response) {
978
1012
  if (response.status === 202) return;
979
1013
  const type = response.headers.get("content-type") ?? "";
1014
+ let messages = [];
1015
+ let failure;
980
1016
  try {
981
- if (type.includes("text/event-stream")) {
982
- for (const message of await readEventStream(response)) this.#capture(message);
983
- return;
984
- }
985
- if (type.includes("application/json")) {
1017
+ if (type.includes("text/event-stream")) messages = await readEventStream(response);
1018
+ else if (type.includes("application/json")) {
986
1019
  const message = parseJSONRPCMessage(await response.json());
987
- if (message !== void 0) this.#capture(message);
1020
+ if (message !== void 0) messages = [message];
988
1021
  }
989
1022
  } catch (error) {
990
- this.#emitter.emit("error", error);
1023
+ failure = { error };
991
1024
  }
1025
+ for (const message of messages) this.#capture(message);
1026
+ if (!response.ok && messages.length === 0) throw buildResponseError(response, type);
1027
+ if (failure !== void 0) this.#emitter.emit("error", failure.error);
992
1028
  }
993
1029
  #capture(message) {
994
1030
  if (isJSONRPCResponse(message) && isRecord(message.result) && isMCPVersion(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
@@ -1239,7 +1275,11 @@ var WebSocketServerTransport = class {
1239
1275
  * event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
1240
1276
  * non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
1241
1277
  * / `error` bridge to this transport's events.
1242
- * - **Outbound (`send`).** `send(message)` writes one masked text frame.
1278
+ * - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
1279
+ * confirmed, so this transport answers a closed channel from its OWN state: a `send` with no
1280
+ * bound socket — before `start()`, after `close()`, or after the peer ended the socket —
1281
+ * REJECTS with `WebSocket transport is not connected`. It neither drops the message (the
1282
+ * browser face's posture) nor queues it for a connection this transport is not holding.
1243
1283
  * - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
1244
1284
  * upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
1245
1285
  * transport at once instead of waiting for a peer that may never answer — the suspended
@@ -1455,7 +1495,7 @@ var WebSocketClientTransport = class {
1455
1495
  * ```ts
1456
1496
  * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })
1457
1497
  * const client = new MCPClient({ transport })
1458
- * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
1498
+ * await client.connect() // start() spawns the child, then modern discovery runs over stdio
1459
1499
  * ```
1460
1500
  */
1461
1501
  var StdioClientTransport = class {
@@ -2192,6 +2232,6 @@ function createMCPSession(options) {
2192
2232
  };
2193
2233
  }
2194
2234
  //#endregion
2195
- export { DEFAULT_MCP_DELIVERY, DEFAULT_MCP_KEEPALIVE_INTERVAL, DEFAULT_MCP_PATH, DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL, HTTPClientTransport, HTTPDisconnect, MCPSession, MCP_METHOD_HEADER, MCP_NAME_HEADER, MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER, MCP_WEBSOCKET_SUBPROTOCOL, SSE_BUFFERING_DISABLED, SSE_BUFFERING_HEADER, SSE_KEEPALIVE_COMMENT, StdioClientTransport, StdioServerTransport, WebSocketClientTransport, WebSocketServerTransport, acceptsEventStream, allowsOrigin, bridgeMessageTransport, createHTTPClientTransport, createMCPContinuation, createMCPPostHandler, createMCPRoutes, createMCPSession, createReadableStream, createStdioClientTransport, createStdioServer, createWebSocketClientTransport, createWebSocketServer, decodeEvent, dispatchLines, extractLines, inferHeaderIssue, inferLegacyVersion, inferStatus, readEventStream, readLastEventId, readSessionHeader, rejectUnknownSession, sendEventStream, upgradeRequestPath, writeLine };
2235
+ export { DEFAULT_MCP_DELIVERY, DEFAULT_MCP_KEEPALIVE_INTERVAL, DEFAULT_MCP_PATH, DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL, HTTPClientTransport, HTTPDisconnect, MCPSession, MCP_METHOD_HEADER, MCP_NAME_HEADER, MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER, MCP_WEBSOCKET_SUBPROTOCOL, SSE_BUFFERING_DISABLED, SSE_BUFFERING_HEADER, SSE_KEEPALIVE_COMMENT, StdioClientTransport, StdioServerTransport, WebSocketClientTransport, WebSocketServerTransport, acceptsEventStream, allowsOrigin, bridgeMessageTransport, buildResponseError, createHTTPClientTransport, createMCPContinuation, createMCPPostHandler, createMCPRoutes, createMCPSession, createReadableStream, createStdioClientTransport, createStdioServer, createWebSocketClientTransport, createWebSocketServer, decodeEvent, dispatchLines, extractLines, inferHeaderIssue, inferLegacyVersion, inferStatus, readEventStream, readLastEventId, readSessionHeader, rejectUnknownSession, sendEventStream, upgradeRequestPath, writeLine };
2196
2236
 
2197
2237
  //# sourceMappingURL=index.js.map