@orkestrel/mcp 0.0.4 → 0.0.5

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,12 +1,13 @@
1
1
  import { ClientTransportEventMap } from '../core/index.ts';
2
2
  import { ClientTransportEventMap as ClientTransportEventMap_2 } from '../../core/index.ts';
3
- import { ClientTransportInterface } from '../../core/index.ts';
4
- import { ClientTransportInterface as ClientTransportInterface_2 } from '../core/index.ts';
3
+ import { ClientTransportInterface } from '../core/index.ts';
4
+ import { ClientTransportInterface as ClientTransportInterface_2 } from '../../core/index.ts';
5
5
  import { EmitterInterface } from '@orkestrel/emitter';
6
6
  import { IncomingMessage } from 'node:http';
7
7
  import { JSONRPCMessage } from '../core/index.ts';
8
8
  import { JSONRPCMessage as JSONRPCMessage_2 } from '../../core/index.ts';
9
9
  import { MCPServerInterface } from '../core/index.ts';
10
+ import { MCPTransportInterface } from '../core/index.ts';
10
11
  import { MiddlewareHandler } from '@orkestrel/server';
11
12
  import { NodeWebSocketInterface } from '@orkestrel/websocket';
12
13
  import { RouteInput } from '@orkestrel/router';
@@ -28,6 +29,49 @@ import { UpgradeHandler } from '@orkestrel/server';
28
29
  */
29
30
  export declare function acceptsEventStream(request: Request): boolean;
30
31
 
32
+ /**
33
+ * Bridge a message-channel {@link ClientTransportInterface} (the shape the stdio and
34
+ * WebSocket SERVER transports already implement) into the environment-agnostic
35
+ * {@link import('@src/core').MCPTransportInterface} port — the adapter
36
+ * {@link import('./factories.js').createStdioServer} and {@link
37
+ * import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
38
+ * request/reply/error pump those two factories used to hand-roll identically now
39
+ * lives ONCE in the core binder.
40
+ *
41
+ * @remarks
42
+ * `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
43
+ * and writes it via `transport.send` (the SAME `JSON.stringify` the underlying
44
+ * transport already performs, so the wire bytes are unchanged). `listen` filters
45
+ * `transport`'s `message` event to REQUESTS ONLY — a stray response is ignored,
46
+ * exactly as the prior hand-rolled pumps did — and re-serializes each one back to a
47
+ * string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
48
+ * closes the underlying `transport`.
49
+ *
50
+ * @remarks Per {@link import('@src/core').MCPTransportInterface}, `listen`/`closed`
51
+ * each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
52
+ * Since the underlying `transport.emitter` is ADD-based (`on` subscribes, never
53
+ * replaces), this bridge installs ONE stable emitter listener per event on first use
54
+ * and re-routes it to whichever handler is CURRENTLY registered (`undefined` while
55
+ * none is), so rebinding never double-dispatches.
56
+ *
57
+ * @remarks A response whose `result` serializes away (e.g. `undefined`) is dropped by
58
+ * the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
59
+ * shares with the streamable-HTTP face, since both round-trip through `JSON.stringify`
60
+ * / `JSON.parse` before re-validation.
61
+ *
62
+ * @param transport - The message-channel transport to bridge (stdio or WebSocket)
63
+ * @returns An {@link import('@src/core').MCPTransportInterface} `bindServer` can drive
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { bindServer } from '@src/core'
68
+ *
69
+ * const transport = new StdioServerTransport(process.stdin, process.stdout)
70
+ * bindServer(mcp, bridgeMessageTransport(transport))
71
+ * ```
72
+ */
73
+ export declare function bridgeMessageTransport(transport: ClientTransportInterface): MCPTransportInterface;
74
+
31
75
  /**
32
76
  * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}
33
77
  * — a {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
@@ -61,7 +105,7 @@ export declare function acceptsEventStream(request: Request): boolean;
61
105
  * const tools = await client.tools()
62
106
  * ```
63
107
  */
64
- export declare function createHTTPClientTransport(options: HTTPClientTransportOptions): ClientTransportInterface_2;
108
+ export declare function createHTTPClientTransport(options: HTTPClientTransportOptions): ClientTransportInterface;
65
109
 
66
110
  /**
67
111
  * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
@@ -202,7 +246,7 @@ export declare function createMCPSession<TState extends MCPSessionState>(options
202
246
  * const tools = await client.tools()
203
247
  * ```
204
248
  */
205
- export declare function createStdioClientTransport(options: StdioClientTransportOptions): ClientTransportInterface_2;
249
+ export declare function createStdioClientTransport(options: StdioClientTransportOptions): ClientTransportInterface;
206
250
 
207
251
  /**
208
252
  * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
@@ -212,12 +256,13 @@ export declare function createStdioClientTransport(options: StdioClientTransport
212
256
  * @remarks
213
257
  * Wraps `options.input` (default `process.stdin`) / `options.output` (default
214
258
  * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
215
- * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a
216
- * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a
217
- * newline-terminated line a NOTIFICATION (`dispatch` → `undefined`) writes
218
- * nothing. A non-request message is ignored. The dispatch is guarded so a
219
- * `dispatch` / `send` fault surfaces on the transport's `error` event rather than
220
- * escaping the (async) message listener.
259
+ * and pipes it through the core {@link import('@src/core').MCPTransportInterface} port
260
+ * via {@link import('./helpers.js').bridgeMessageTransport} + {@link
261
+ * import('@src/core').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
262
+ * a defined response is written back as a newline-terminated line a NOTIFICATION
263
+ * writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
264
+ * surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
265
+ * pump.
221
266
  *
222
267
  * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio
223
268
  * @param options - Optional injectable `input` / `output` streams; see
@@ -270,7 +315,7 @@ export declare function createStdioServer(mcp: MCPServerInterface, options?: Std
270
315
  * const tools = await client.tools()
271
316
  * ```
272
317
  */
273
- export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): ClientTransportInterface_2;
318
+ export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): ClientTransportInterface;
274
319
 
275
320
  /**
276
321
  * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
@@ -290,12 +335,13 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
290
335
  * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
291
336
  * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default
292
337
  * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a
293
- * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link
294
- * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a
295
- * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)
296
- * sends nothing. A non-request message (a stray response) is ignored. The dispatch is
297
- * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather
298
- * than escaping the (async) message listener.
338
+ * {@link WebSocketServerTransport}, and pipes it through the core {@link
339
+ * import('@src/core').MCPTransportInterface} port via {@link
340
+ * import('./helpers.js').bridgeMessageTransport} + {@link import('@src/core').bindServer}:
341
+ * each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
342
+ * as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
343
+ * ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
344
+ * escaping the (async) message pump.
299
345
  *
300
346
  * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
301
347
  * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
@@ -454,7 +500,7 @@ export declare function extractLines(buffer: string, chunk: string): LineExtract
454
500
  * await client.connect()
455
501
  * ```
456
502
  */
457
- export declare class HTTPClientTransport implements ClientTransportInterface {
503
+ export declare class HTTPClientTransport implements ClientTransportInterface_2 {
458
504
  #private;
459
505
  constructor(options: HTTPClientTransportOptions);
460
506
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -814,7 +860,7 @@ export declare function rejectUnknownSession(): Response;
814
860
  * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
815
861
  * ```
816
862
  */
817
- export declare class StdioClientTransport implements ClientTransportInterface {
863
+ export declare class StdioClientTransport implements ClientTransportInterface_2 {
818
864
  #private;
819
865
  constructor(options: StdioClientTransportOptions);
820
866
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -886,7 +932,7 @@ export declare interface StdioServerOptions {
886
932
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
887
933
  * fault), distinct from the emitter's own listener-error channel.
888
934
  */
889
- export declare class StdioServerTransport implements ClientTransportInterface {
935
+ export declare class StdioServerTransport implements ClientTransportInterface_2 {
890
936
  #private;
891
937
  constructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream);
892
938
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -949,7 +995,7 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
949
995
  * await client.connect() // start() handshakes, then the MCP initialize runs over WS frames
950
996
  * ```
951
997
  */
952
- export declare class WebSocketClientTransport implements ClientTransportInterface {
998
+ export declare class WebSocketClientTransport implements ClientTransportInterface_2 {
953
999
  #private;
954
1000
  constructor(options: WebSocketClientTransportOptions);
955
1001
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -1031,7 +1077,7 @@ export declare interface WebSocketServerOptions {
1031
1077
  * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
1032
1078
  * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.
1033
1079
  */
1034
- export declare class WebSocketServerTransport implements ClientTransportInterface {
1080
+ export declare class WebSocketServerTransport implements ClientTransportInterface_2 {
1035
1081
  #private;
1036
1082
  constructor(socket: NodeWebSocketInterface);
1037
1083
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -1,12 +1,13 @@
1
1
  import { ClientTransportEventMap } from '../core/index.ts';
2
2
  import { ClientTransportEventMap as ClientTransportEventMap_2 } from '../../core/index.ts';
3
- import { ClientTransportInterface } from '../../core/index.ts';
4
- import { ClientTransportInterface as ClientTransportInterface_2 } from '../core/index.ts';
3
+ import { ClientTransportInterface } from '../core/index.ts';
4
+ import { ClientTransportInterface as ClientTransportInterface_2 } from '../../core/index.ts';
5
5
  import { EmitterInterface } from '@orkestrel/emitter';
6
6
  import { IncomingMessage } from 'node:http';
7
7
  import { JSONRPCMessage } from '../core/index.ts';
8
8
  import { JSONRPCMessage as JSONRPCMessage_2 } from '../../core/index.ts';
9
9
  import { MCPServerInterface } from '../core/index.ts';
10
+ import { MCPTransportInterface } from '../core/index.ts';
10
11
  import { MiddlewareHandler } from '@orkestrel/server';
11
12
  import { NodeWebSocketInterface } from '@orkestrel/websocket';
12
13
  import { RouteInput } from '@orkestrel/router';
@@ -28,6 +29,49 @@ import { UpgradeHandler } from '@orkestrel/server';
28
29
  */
29
30
  export declare function acceptsEventStream(request: Request): boolean;
30
31
 
32
+ /**
33
+ * Bridge a message-channel {@link ClientTransportInterface} (the shape the stdio and
34
+ * WebSocket SERVER transports already implement) into the environment-agnostic
35
+ * {@link import('@src/core').MCPTransportInterface} port — the adapter
36
+ * {@link import('./factories.js').createStdioServer} and {@link
37
+ * import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
38
+ * request/reply/error pump those two factories used to hand-roll identically now
39
+ * lives ONCE in the core binder.
40
+ *
41
+ * @remarks
42
+ * `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
43
+ * and writes it via `transport.send` (the SAME `JSON.stringify` the underlying
44
+ * transport already performs, so the wire bytes are unchanged). `listen` filters
45
+ * `transport`'s `message` event to REQUESTS ONLY — a stray response is ignored,
46
+ * exactly as the prior hand-rolled pumps did — and re-serializes each one back to a
47
+ * string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
48
+ * closes the underlying `transport`.
49
+ *
50
+ * @remarks Per {@link import('@src/core').MCPTransportInterface}, `listen`/`closed`
51
+ * each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
52
+ * Since the underlying `transport.emitter` is ADD-based (`on` subscribes, never
53
+ * replaces), this bridge installs ONE stable emitter listener per event on first use
54
+ * and re-routes it to whichever handler is CURRENTLY registered (`undefined` while
55
+ * none is), so rebinding never double-dispatches.
56
+ *
57
+ * @remarks A response whose `result` serializes away (e.g. `undefined`) is dropped by
58
+ * the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
59
+ * shares with the streamable-HTTP face, since both round-trip through `JSON.stringify`
60
+ * / `JSON.parse` before re-validation.
61
+ *
62
+ * @param transport - The message-channel transport to bridge (stdio or WebSocket)
63
+ * @returns An {@link import('@src/core').MCPTransportInterface} `bindServer` can drive
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { bindServer } from '@src/core'
68
+ *
69
+ * const transport = new StdioServerTransport(process.stdin, process.stdout)
70
+ * bindServer(mcp, bridgeMessageTransport(transport))
71
+ * ```
72
+ */
73
+ export declare function bridgeMessageTransport(transport: ClientTransportInterface): MCPTransportInterface;
74
+
31
75
  /**
32
76
  * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}
33
77
  * — a {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
@@ -61,7 +105,7 @@ export declare function acceptsEventStream(request: Request): boolean;
61
105
  * const tools = await client.tools()
62
106
  * ```
63
107
  */
64
- export declare function createHTTPClientTransport(options: HTTPClientTransportOptions): ClientTransportInterface_2;
108
+ export declare function createHTTPClientTransport(options: HTTPClientTransportOptions): ClientTransportInterface;
65
109
 
66
110
  /**
67
111
  * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
@@ -202,7 +246,7 @@ export declare function createMCPSession<TState extends MCPSessionState>(options
202
246
  * const tools = await client.tools()
203
247
  * ```
204
248
  */
205
- export declare function createStdioClientTransport(options: StdioClientTransportOptions): ClientTransportInterface_2;
249
+ export declare function createStdioClientTransport(options: StdioClientTransportOptions): ClientTransportInterface;
206
250
 
207
251
  /**
208
252
  * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
@@ -212,12 +256,13 @@ export declare function createStdioClientTransport(options: StdioClientTransport
212
256
  * @remarks
213
257
  * Wraps `options.input` (default `process.stdin`) / `options.output` (default
214
258
  * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
215
- * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a
216
- * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a
217
- * newline-terminated line a NOTIFICATION (`dispatch` → `undefined`) writes
218
- * nothing. A non-request message is ignored. The dispatch is guarded so a
219
- * `dispatch` / `send` fault surfaces on the transport's `error` event rather than
220
- * escaping the (async) message listener.
259
+ * and pipes it through the core {@link import('@src/core').MCPTransportInterface} port
260
+ * via {@link import('./helpers.js').bridgeMessageTransport} + {@link
261
+ * import('@src/core').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
262
+ * a defined response is written back as a newline-terminated line a NOTIFICATION
263
+ * writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
264
+ * surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
265
+ * pump.
221
266
  *
222
267
  * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio
223
268
  * @param options - Optional injectable `input` / `output` streams; see
@@ -270,7 +315,7 @@ export declare function createStdioServer(mcp: MCPServerInterface, options?: Std
270
315
  * const tools = await client.tools()
271
316
  * ```
272
317
  */
273
- export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): ClientTransportInterface_2;
318
+ export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): ClientTransportInterface;
274
319
 
275
320
  /**
276
321
  * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
@@ -290,12 +335,13 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
290
335
  * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
291
336
  * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default
292
337
  * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a
293
- * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link
294
- * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a
295
- * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)
296
- * sends nothing. A non-request message (a stray response) is ignored. The dispatch is
297
- * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather
298
- * than escaping the (async) message listener.
338
+ * {@link WebSocketServerTransport}, and pipes it through the core {@link
339
+ * import('@src/core').MCPTransportInterface} port via {@link
340
+ * import('./helpers.js').bridgeMessageTransport} + {@link import('@src/core').bindServer}:
341
+ * each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
342
+ * as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
343
+ * ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
344
+ * escaping the (async) message pump.
299
345
  *
300
346
  * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
301
347
  * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
@@ -454,7 +500,7 @@ export declare function extractLines(buffer: string, chunk: string): LineExtract
454
500
  * await client.connect()
455
501
  * ```
456
502
  */
457
- export declare class HTTPClientTransport implements ClientTransportInterface {
503
+ export declare class HTTPClientTransport implements ClientTransportInterface_2 {
458
504
  #private;
459
505
  constructor(options: HTTPClientTransportOptions);
460
506
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -814,7 +860,7 @@ export declare function rejectUnknownSession(): Response;
814
860
  * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
815
861
  * ```
816
862
  */
817
- export declare class StdioClientTransport implements ClientTransportInterface {
863
+ export declare class StdioClientTransport implements ClientTransportInterface_2 {
818
864
  #private;
819
865
  constructor(options: StdioClientTransportOptions);
820
866
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -886,7 +932,7 @@ export declare interface StdioServerOptions {
886
932
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
887
933
  * fault), distinct from the emitter's own listener-error channel.
888
934
  */
889
- export declare class StdioServerTransport implements ClientTransportInterface {
935
+ export declare class StdioServerTransport implements ClientTransportInterface_2 {
890
936
  #private;
891
937
  constructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream);
892
938
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -949,7 +995,7 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
949
995
  * await client.connect() // start() handshakes, then the MCP initialize runs over WS frames
950
996
  * ```
951
997
  */
952
- export declare class WebSocketClientTransport implements ClientTransportInterface {
998
+ export declare class WebSocketClientTransport implements ClientTransportInterface_2 {
953
999
  #private;
954
1000
  constructor(options: WebSocketClientTransportOptions);
955
1001
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -1031,7 +1077,7 @@ export declare interface WebSocketServerOptions {
1031
1077
  * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
1032
1078
  * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.
1033
1079
  */
1034
- export declare class WebSocketServerTransport implements ClientTransportInterface {
1080
+ export declare class WebSocketServerTransport implements ClientTransportInterface_2 {
1035
1081
  #private;
1036
1082
  constructor(socket: NodeWebSocketInterface);
1037
1083
  get emitter(): EmitterInterface<ClientTransportEventMap_2>;
@@ -1,5 +1,5 @@
1
1
  import { createSSEParser } from "@orkestrel/sse";
2
- import { JSONRPC_INVALID_REQUEST, JSONRPC_PARSE_ERROR, isInitializeRequest, isJSONRPCRequest, jsonRPCError, parseJSONRPCMessage } from "../core/index.js";
2
+ import { JSONRPC_INVALID_REQUEST, JSONRPC_PARSE_ERROR, bindServer, isInitializeRequest, isJSONRPCRequest, jsonRPCError, parseJSONRPCMessage } from "../core/index.js";
3
3
  import { isString } from "@orkestrel/contract";
4
4
  import { Emitter } from "@orkestrel/emitter";
5
5
  import { randomBytes } from "node:crypto";
@@ -263,6 +263,81 @@ function dispatchLines(emitter, lines) {
263
263
  emitter.emit("message", message);
264
264
  }
265
265
  }
266
+ /**
267
+ * Bridge a message-channel {@link ClientTransportInterface} (the shape the stdio and
268
+ * WebSocket SERVER transports already implement) into the environment-agnostic
269
+ * {@link import('@src/core').MCPTransportInterface} port — the adapter
270
+ * {@link import('./factories.js').createStdioServer} and {@link
271
+ * import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
272
+ * request/reply/error pump those two factories used to hand-roll identically now
273
+ * lives ONCE in the core binder.
274
+ *
275
+ * @remarks
276
+ * `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
277
+ * and writes it via `transport.send` (the SAME `JSON.stringify` the underlying
278
+ * transport already performs, so the wire bytes are unchanged). `listen` filters
279
+ * `transport`'s `message` event to REQUESTS ONLY — a stray response is ignored,
280
+ * exactly as the prior hand-rolled pumps did — and re-serializes each one back to a
281
+ * string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
282
+ * closes the underlying `transport`.
283
+ *
284
+ * @remarks Per {@link import('@src/core').MCPTransportInterface}, `listen`/`closed`
285
+ * each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
286
+ * Since the underlying `transport.emitter` is ADD-based (`on` subscribes, never
287
+ * replaces), this bridge installs ONE stable emitter listener per event on first use
288
+ * and re-routes it to whichever handler is CURRENTLY registered (`undefined` while
289
+ * none is), so rebinding never double-dispatches.
290
+ *
291
+ * @remarks A response whose `result` serializes away (e.g. `undefined`) is dropped by
292
+ * the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
293
+ * shares with the streamable-HTTP face, since both round-trip through `JSON.stringify`
294
+ * / `JSON.parse` before re-validation.
295
+ *
296
+ * @param transport - The message-channel transport to bridge (stdio or WebSocket)
297
+ * @returns An {@link import('@src/core').MCPTransportInterface} `bindServer` can drive
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * import { bindServer } from '@src/core'
302
+ *
303
+ * const transport = new StdioServerTransport(process.stdin, process.stdout)
304
+ * bindServer(mcp, bridgeMessageTransport(transport))
305
+ * ```
306
+ */
307
+ function bridgeMessageTransport(transport) {
308
+ let onMessage;
309
+ let onClosed;
310
+ let subscribed = false;
311
+ function subscribe() {
312
+ if (subscribed) return;
313
+ subscribed = true;
314
+ transport.emitter.on("message", (message) => {
315
+ if (!isJSONRPCRequest(message)) return;
316
+ onMessage?.(JSON.stringify(message));
317
+ });
318
+ transport.emitter.on("close", () => {
319
+ onClosed?.();
320
+ });
321
+ }
322
+ return {
323
+ async send(message) {
324
+ const decoded = decodeEvent(message);
325
+ if (decoded === void 0) return;
326
+ await transport.send(decoded);
327
+ },
328
+ listen(handler) {
329
+ subscribe();
330
+ onMessage = handler;
331
+ },
332
+ closed(handler) {
333
+ subscribe();
334
+ onClosed = handler;
335
+ },
336
+ async close() {
337
+ await transport.close();
338
+ }
339
+ };
340
+ }
266
341
  //#endregion
267
342
  //#region src/server/transports/HTTPClientTransport.ts
268
343
  /**
@@ -1030,12 +1105,13 @@ function createHTTPClientTransport(options) {
1030
1105
  * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
1031
1106
  * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default
1032
1107
  * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a
1033
- * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link
1034
- * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a
1035
- * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)
1036
- * sends nothing. A non-request message (a stray response) is ignored. The dispatch is
1037
- * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather
1038
- * than escaping the (async) message listener.
1108
+ * {@link WebSocketServerTransport}, and pipes it through the core {@link
1109
+ * import('@src/core').MCPTransportInterface} port via {@link
1110
+ * import('./helpers.js').bridgeMessageTransport} + {@link import('@src/core').bindServer}:
1111
+ * each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
1112
+ * as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
1113
+ * ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
1114
+ * escaping the (async) message pump.
1039
1115
  *
1040
1116
  * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
1041
1117
  * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
@@ -1072,19 +1148,7 @@ function createWebSocketServer(mcp, options) {
1072
1148
  head,
1073
1149
  protocol: subprotocol
1074
1150
  }));
1075
- transport.emitter.on("message", (message) => {
1076
- if (!isJSONRPCRequest(message)) return;
1077
- (async () => {
1078
- try {
1079
- const response = await mcp.dispatch(message);
1080
- if (response !== void 0) await transport.send(response);
1081
- } catch (error) {
1082
- try {
1083
- transport.emitter.emit("error", error);
1084
- } catch {}
1085
- }
1086
- })();
1087
- });
1151
+ bindServer(mcp, bridgeMessageTransport(transport));
1088
1152
  transport.start();
1089
1153
  return true;
1090
1154
  };
@@ -1166,12 +1230,13 @@ function createStdioClientTransport(options) {
1166
1230
  * @remarks
1167
1231
  * Wraps `options.input` (default `process.stdin`) / `options.output` (default
1168
1232
  * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
1169
- * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a
1170
- * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a
1171
- * newline-terminated line a NOTIFICATION (`dispatch` → `undefined`) writes
1172
- * nothing. A non-request message is ignored. The dispatch is guarded so a
1173
- * `dispatch` / `send` fault surfaces on the transport's `error` event rather than
1174
- * escaping the (async) message listener.
1233
+ * and pipes it through the core {@link import('@src/core').MCPTransportInterface} port
1234
+ * via {@link import('./helpers.js').bridgeMessageTransport} + {@link
1235
+ * import('@src/core').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
1236
+ * a defined response is written back as a newline-terminated line a NOTIFICATION
1237
+ * writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
1238
+ * surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
1239
+ * pump.
1175
1240
  *
1176
1241
  * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio
1177
1242
  * @param options - Optional injectable `input` / `output` streams; see
@@ -1189,19 +1254,7 @@ function createStdioClientTransport(options) {
1189
1254
  */
1190
1255
  function createStdioServer(mcp, options) {
1191
1256
  const transport = new StdioServerTransport(options?.input ?? process.stdin, options?.output ?? process.stdout);
1192
- transport.emitter.on("message", (message) => {
1193
- if (!isJSONRPCRequest(message)) return;
1194
- (async () => {
1195
- try {
1196
- const response = await mcp.dispatch(message);
1197
- if (response !== void 0) await transport.send(response);
1198
- } catch (error) {
1199
- try {
1200
- transport.emitter.emit("error", error);
1201
- } catch {}
1202
- }
1203
- })();
1204
- });
1257
+ bindServer(mcp, bridgeMessageTransport(transport));
1205
1258
  return {
1206
1259
  start() {
1207
1260
  transport.start();
@@ -1339,6 +1392,6 @@ function createMCPSession(options) {
1339
1392
  }
1340
1393
  }
1341
1394
  //#endregion
1342
- export { DEFAULT_MCP_PATH, DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL, HTTPClientTransport, MCPSession, MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER, MCP_WEBSOCKET_SUBPROTOCOL, StdioClientTransport, StdioServerTransport, WebSocketClientTransport, WebSocketServerTransport, acceptsEventStream, createHTTPClientTransport, createMCPRoutes, createMCPSession, createStdioClientTransport, createStdioServer, createWebSocketClientTransport, createWebSocketServer, decodeEvent, dispatchLines, extractLines, readEventStream, readLastEventId, readSessionHeader, rejectUnknownSession, upgradeRequestPath };
1395
+ export { DEFAULT_MCP_PATH, DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL, HTTPClientTransport, MCPSession, MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER, MCP_WEBSOCKET_SUBPROTOCOL, StdioClientTransport, StdioServerTransport, WebSocketClientTransport, WebSocketServerTransport, acceptsEventStream, bridgeMessageTransport, createHTTPClientTransport, createMCPRoutes, createMCPSession, createStdioClientTransport, createStdioServer, createWebSocketClientTransport, createWebSocketServer, decodeEvent, dispatchLines, extractLines, readEventStream, readLastEventId, readSessionHeader, rejectUnknownSession, upgradeRequestPath };
1343
1396
 
1344
1397
  //# sourceMappingURL=index.js.map