@orkestrel/mcp 0.0.13 → 0.0.15

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.
@@ -18,6 +18,7 @@ import { MiddlewareHandler } from '@orkestrel/server';
18
18
  import { NodeWebSocketInterface } from '@orkestrel/websocket';
19
19
  import { RouteContext } from '@orkestrel/router';
20
20
  import { RouteInput } from '@orkestrel/router';
21
+ import { ServerEventMap } from '@orkestrel/server';
21
22
  import { StreamInterface } from '@orkestrel/server';
22
23
  import { TokenSecret } from '@orkestrel/server';
23
24
  import { UpgradeHandler } from '@orkestrel/server';
@@ -242,7 +243,7 @@ export declare function createMCPPostHandler<TState = unknown>(mcp: MCPDispatche
242
243
  * const routes = createMCPRoutes(createMCPLegacy(mcp)) // both eras; pass `mcp` for modern only
243
244
  * ```
244
245
  */
245
- export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInterface, options?: HTTPTransportOptions<TState>): readonly RouteInput<string, TState>[];
246
+ export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInterface, options?: HTTPTransportOptions<TState>): ReadonlyArray<RouteInput<string, TState>>;
246
247
 
247
248
  /**
248
249
  * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
@@ -445,13 +446,21 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
445
446
  * as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
446
447
  * ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
447
448
  * escaping the (async) message pump.
449
+ * - **Closes on the spine's `stop`.** It holds every socket it claimed and, on `options.emitter`'s
450
+ * `stop` event, closes each one with the RFC 6455 close handshake, so the spine's drain settles
451
+ * at once and each client reads a clean goodbye. Node detaches an upgraded socket from the
452
+ * connection set the spine's own close walks, so the claimant is the only thing that can end
453
+ * it: an ingress that held its sockets open would cost `stop()` the whole `drain` budget and
454
+ * then have the connection cut mid-protocol. A socket the peer already dropped is gone from
455
+ * the set (its transport's `close` removes it), and closing a dead one is a no-op either way.
448
456
  *
449
457
  * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
450
458
  * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
451
459
  * upgrade so it never reaches this pump.
452
460
  *
453
461
  * @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over WebSocket
454
- * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
462
+ * @param options - The spine's `emitter` (REQUIRED the `stop` event this ingress closes its
463
+ * sockets on), plus optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
455
464
  * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}
456
465
  * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam
457
466
  *
@@ -461,10 +470,10 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
461
470
  * import { createWebSocketServer } from '@src/server'
462
471
  *
463
472
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
464
- * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp
473
+ * server.upgrade(createWebSocketServer(mcp, { emitter: server.emitter })) // ws://…/mcp
465
474
  * ```
466
475
  */
467
- export declare function createWebSocketServer(mcp: MCPDispatcherInterface, options?: WebSocketServerOptions): UpgradeHandler;
476
+ export declare function createWebSocketServer(mcp: MCPDispatcherInterface, options: WebSocketServerOptions): UpgradeHandler;
468
477
 
469
478
  /**
470
479
  * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
@@ -1427,10 +1436,16 @@ export declare interface WebSocketClientTransportOptions {
1427
1436
  }
1428
1437
 
1429
1438
  /**
1430
- * Options for `createWebSocketServer` — where the WebSocket upgrade is accepted and the
1431
- * subprotocol negotiated.
1439
+ * Options for `createWebSocketServer` — the spine lifecycle the ingress follows, plus where
1440
+ * the WebSocket upgrade is accepted and the subprotocol negotiated.
1432
1441
  *
1433
1442
  * @remarks
1443
+ * - `emitter` — the emitter of the `@orkestrel/server` spine this handler is registered on
1444
+ * (`server.emitter`). REQUIRED: on its `stop` event the handler closes every socket it
1445
+ * still owns with the RFC 6455 close handshake, so the spine's drain settles at once. An
1446
+ * upgraded socket is detached from the connection set the spine's own close walks, so
1447
+ * nothing but the claimant can end it — leave it open and `stop()` spends its whole
1448
+ * `drain` budget and then cuts the connection mid-protocol.
1434
1449
  * - `path` — the request path the upgrade handler CLAIMS; defaults to
1435
1450
  * {@link import('./constants.js').DEFAULT_MCP_PATH} (`'/mcp'`, the same path the HTTP
1436
1451
  * transport mounts at). A protocol-upgrade request to any OTHER path is DECLINED
@@ -1445,6 +1460,7 @@ export declare interface WebSocketClientTransportOptions {
1445
1460
  * before this one can decline an unauthenticated upgrade).
1446
1461
  */
1447
1462
  export declare interface WebSocketServerOptions {
1463
+ readonly emitter: EmitterInterface<ServerEventMap>;
1448
1464
  readonly path?: string;
1449
1465
  readonly subprotocol?: string;
1450
1466
  }
@@ -18,6 +18,7 @@ import { MiddlewareHandler } from '@orkestrel/server';
18
18
  import { NodeWebSocketInterface } from '@orkestrel/websocket';
19
19
  import { RouteContext } from '@orkestrel/router';
20
20
  import { RouteInput } from '@orkestrel/router';
21
+ import { ServerEventMap } from '@orkestrel/server';
21
22
  import { StreamInterface } from '@orkestrel/server';
22
23
  import { TokenSecret } from '@orkestrel/server';
23
24
  import { UpgradeHandler } from '@orkestrel/server';
@@ -242,7 +243,7 @@ export declare function createMCPPostHandler<TState = unknown>(mcp: MCPDispatche
242
243
  * const routes = createMCPRoutes(createMCPLegacy(mcp)) // both eras; pass `mcp` for modern only
243
244
  * ```
244
245
  */
245
- export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInterface, options?: HTTPTransportOptions<TState>): readonly RouteInput<string, TState>[];
246
+ export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInterface, options?: HTTPTransportOptions<TState>): ReadonlyArray<RouteInput<string, TState>>;
246
247
 
247
248
  /**
248
249
  * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
@@ -445,13 +446,21 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
445
446
  * as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
446
447
  * ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
447
448
  * escaping the (async) message pump.
449
+ * - **Closes on the spine's `stop`.** It holds every socket it claimed and, on `options.emitter`'s
450
+ * `stop` event, closes each one with the RFC 6455 close handshake, so the spine's drain settles
451
+ * at once and each client reads a clean goodbye. Node detaches an upgraded socket from the
452
+ * connection set the spine's own close walks, so the claimant is the only thing that can end
453
+ * it: an ingress that held its sockets open would cost `stop()` the whole `drain` budget and
454
+ * then have the connection cut mid-protocol. A socket the peer already dropped is gone from
455
+ * the set (its transport's `close` removes it), and closing a dead one is a no-op either way.
448
456
  *
449
457
  * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
450
458
  * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
451
459
  * upgrade so it never reaches this pump.
452
460
  *
453
461
  * @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over WebSocket
454
- * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
462
+ * @param options - The spine's `emitter` (REQUIRED the `stop` event this ingress closes its
463
+ * sockets on), plus optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
455
464
  * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}
456
465
  * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam
457
466
  *
@@ -461,10 +470,10 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
461
470
  * import { createWebSocketServer } from '@src/server'
462
471
  *
463
472
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
464
- * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp
473
+ * server.upgrade(createWebSocketServer(mcp, { emitter: server.emitter })) // ws://…/mcp
465
474
  * ```
466
475
  */
467
- export declare function createWebSocketServer(mcp: MCPDispatcherInterface, options?: WebSocketServerOptions): UpgradeHandler;
476
+ export declare function createWebSocketServer(mcp: MCPDispatcherInterface, options: WebSocketServerOptions): UpgradeHandler;
468
477
 
469
478
  /**
470
479
  * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
@@ -1427,10 +1436,16 @@ export declare interface WebSocketClientTransportOptions {
1427
1436
  }
1428
1437
 
1429
1438
  /**
1430
- * Options for `createWebSocketServer` — where the WebSocket upgrade is accepted and the
1431
- * subprotocol negotiated.
1439
+ * Options for `createWebSocketServer` — the spine lifecycle the ingress follows, plus where
1440
+ * the WebSocket upgrade is accepted and the subprotocol negotiated.
1432
1441
  *
1433
1442
  * @remarks
1443
+ * - `emitter` — the emitter of the `@orkestrel/server` spine this handler is registered on
1444
+ * (`server.emitter`). REQUIRED: on its `stop` event the handler closes every socket it
1445
+ * still owns with the RFC 6455 close handshake, so the spine's drain settles at once. An
1446
+ * upgraded socket is detached from the connection set the spine's own close walks, so
1447
+ * nothing but the claimant can end it — leave it open and `stop()` spends its whole
1448
+ * `drain` budget and then cuts the connection mid-protocol.
1434
1449
  * - `path` — the request path the upgrade handler CLAIMS; defaults to
1435
1450
  * {@link import('./constants.js').DEFAULT_MCP_PATH} (`'/mcp'`, the same path the HTTP
1436
1451
  * transport mounts at). A protocol-upgrade request to any OTHER path is DECLINED
@@ -1445,6 +1460,7 @@ export declare interface WebSocketClientTransportOptions {
1445
1460
  * before this one can decline an unauthenticated upgrade).
1446
1461
  */
1447
1462
  export declare interface WebSocketServerOptions {
1463
+ readonly emitter: EmitterInterface<ServerEventMap>;
1448
1464
  readonly path?: string;
1449
1465
  readonly subprotocol?: string;
1450
1466
  }
@@ -1601,13 +1601,21 @@ function createHTTPClientTransport(options) {
1601
1601
  * as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
1602
1602
  * ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
1603
1603
  * escaping the (async) message pump.
1604
+ * - **Closes on the spine's `stop`.** It holds every socket it claimed and, on `options.emitter`'s
1605
+ * `stop` event, closes each one with the RFC 6455 close handshake, so the spine's drain settles
1606
+ * at once and each client reads a clean goodbye. Node detaches an upgraded socket from the
1607
+ * connection set the spine's own close walks, so the claimant is the only thing that can end
1608
+ * it: an ingress that held its sockets open would cost `stop()` the whole `drain` budget and
1609
+ * then have the connection cut mid-protocol. A socket the peer already dropped is gone from
1610
+ * the set (its transport's `close` removes it), and closing a dead one is a no-op either way.
1604
1611
  *
1605
1612
  * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
1606
1613
  * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
1607
1614
  * upgrade so it never reaches this pump.
1608
1615
  *
1609
1616
  * @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over WebSocket
1610
- * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
1617
+ * @param options - The spine's `emitter` (REQUIRED the `stop` event this ingress closes its
1618
+ * sockets on), plus optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
1611
1619
  * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}
1612
1620
  * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam
1613
1621
  *
@@ -1617,12 +1625,16 @@ function createHTTPClientTransport(options) {
1617
1625
  * import { createWebSocketServer } from '@src/server'
1618
1626
  *
1619
1627
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
1620
- * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp
1628
+ * server.upgrade(createWebSocketServer(mcp, { emitter: server.emitter })) // ws://…/mcp
1621
1629
  * ```
1622
1630
  */
1623
1631
  function createWebSocketServer(mcp, options) {
1624
- const path = options?.path ?? "/mcp";
1625
- const subprotocol = options?.subprotocol ?? "mcp";
1632
+ const path = options.path ?? "/mcp";
1633
+ const subprotocol = options.subprotocol ?? "mcp";
1634
+ const live = /* @__PURE__ */ new Set();
1635
+ options.emitter.on("stop", () => {
1636
+ for (const transport of live) transport.close();
1637
+ });
1626
1638
  return (request, socket, head) => {
1627
1639
  const upgrade = request.headers["upgrade"];
1628
1640
  if (!isString(upgrade) || upgrade.toLowerCase() !== "websocket") return false;
@@ -1637,6 +1649,8 @@ function createWebSocketServer(mcp, options) {
1637
1649
  head,
1638
1650
  protocol: subprotocol
1639
1651
  }));
1652
+ live.add(transport);
1653
+ transport.emitter.on("close", () => live.delete(transport));
1640
1654
  bindServer(mcp, bridgeMessageTransport(transport));
1641
1655
  transport.start();
1642
1656
  return true;