@orkestrel/mcp 0.0.4 → 0.0.6

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.
@@ -5,6 +5,81 @@ import { ToolInterface } from '@orkestrel/agent';
5
5
  import { ToolManagerInterface } from '@orkestrel/agent';
6
6
  import { ToolResult } from '@orkestrel/agent';
7
7
 
8
+ /**
9
+ * Pipe an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every
10
+ * inbound message is decoded and delivered onto the client's OWN transport
11
+ * (`client.transport.emitter`'s `message` / `close` events), resolving/rejecting the
12
+ * client's correlated pending requests exactly as a direct reply would.
13
+ *
14
+ * @remarks
15
+ * The client's outbound writes flow through `client.transport.send` — its existing,
16
+ * unmodified request/response correlation — so `client` must have been constructed
17
+ * with a {@link import('./types.js').ClientTransportInterface} that itself carries
18
+ * the SAME `transport` (see {@link import('./factories.js').createDuplexClientTransport},
19
+ * the additive factory that adapts an {@link MCPTransportInterface} into that shape);
20
+ * this binder then completes the inbound half by decoding each message and pushing it
21
+ * onto `client.transport.emitter` (an {@link import('@orkestrel/emitter').EmitterInterface}
22
+ * exposes `emit`, so no client modification is needed). A malformed / non-JSON-RPC
23
+ * inbound message is DROPPED (§14, total — never throws); a delivery fault is routed to
24
+ * `client.transport.emitter`'s `error` event (never rethrown). The returned unbind
25
+ * DETACHES this binder (further inbound messages and the transport's `closed` signal are
26
+ * ignored) WITHOUT closing the transport.
27
+ *
28
+ * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind
29
+ * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent
30
+ * `bindClient` call on the SAME transport is never double-dispatched by a stale
31
+ * subscription left behind — an unbind→rebind cycle delivers exactly one `message`
32
+ * emit per inbound reply.
33
+ *
34
+ * @param client - The transport-agnostic client whose transport to deliver messages onto
35
+ * @param transport - The duplex channel to pipe the client over
36
+ * @returns Detach this binder from the transport (does not close it)
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })
41
+ * const unbind = bindClient(client, transport)
42
+ * await client.connect()
43
+ * // ... later, detach without closing:
44
+ * unbind()
45
+ * ```
46
+ */
47
+ export declare function bindClient(client: MCPClientInterface, transport: MCPTransportInterface): () => void;
48
+
49
+ /**
50
+ * Pipe an {@link MCPTransportInterface} into an {@link MCPServerInterface} — every
51
+ * inbound message runs through `server.handle`, and a defined reply is written back
52
+ * via `transport.send`.
53
+ *
54
+ * @remarks
55
+ * `server.handle` already turns a malformed message into a serialized `-32700` /
56
+ * `-32600` reply and a notification into `undefined` (no reply), so this binder adds
57
+ * no parsing of its own. A `transport.send` throw or rejection is caught and routed
58
+ * to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);
59
+ * a listener on that event that itself throws is swallowed (the end of the line —
60
+ * the caller's own bug, never this binder's). The returned unbind DETACHES this
61
+ * binder (further inbound messages and the transport's `closed` signal are ignored)
62
+ * WITHOUT closing the transport — closing is the caller's decision.
63
+ *
64
+ * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind
65
+ * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent
66
+ * `bindServer` call on the SAME transport is never double-dispatched by a stale
67
+ * subscription left behind — an unbind→rebind cycle yields exactly one reply per
68
+ * request.
69
+ *
70
+ * @param server - The transport-agnostic server to dispatch inbound messages over
71
+ * @param transport - The duplex channel to pipe the server over
72
+ * @returns Detach this binder from the transport (does not close it)
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const unbind = bindServer(server, transport)
77
+ * // ... later, detach without closing:
78
+ * unbind()
79
+ * ```
80
+ */
81
+ export declare function bindServer(server: MCPServerInterface, transport: MCPTransportInterface): () => void;
82
+
8
83
  /**
9
84
  * Map a {@link ToolManagerInterface}'s definitions to MCP `tools/list` descriptors
10
85
  * — renaming `parameters` to the wire's `inputSchema`.
@@ -69,10 +144,10 @@ export declare type ClientTransportEventMap = {
69
144
  *
70
145
  * @remarks
71
146
  * The mirror of the server's "a transport pumps strings through `handle`": here the
72
- * {@link MCPClientInterface} hands the transport a {@link JSONRPCMessage} (or a batch)
73
- * via `send`, and the transport delivers each decoded reply back through the
147
+ * {@link MCPClientInterface} hands the transport one {@link JSONRPCMessage} via
148
+ * `send`, and the transport delivers each decoded reply back through the
74
149
  * `message` event the client subscribed to. The minimal carrier surface (§21): a
75
- * `start` (open the connection / arm any reader), `send` (write a message or batch),
150
+ * `start` (open the connection / arm any reader), `send` (write one message),
76
151
  * and `close` (tear down). `session` exposes a server-assigned session id once a
77
152
  * stateful transport has one (`undefined` for the stateless v1) — reserved for the
78
153
  * later sessions tier. Concrete transports (the HTTP transport over `fetch`, a future
@@ -90,7 +165,7 @@ export declare interface ClientTransportInterface {
90
165
  */
91
166
  start(): Promise<void>;
92
167
  /**
93
- * Send one JSON-RPC message (or a batch) to the remote server.
168
+ * Send one JSON-RPC message to the remote server.
94
169
  *
95
170
  * @remarks
96
171
  * Each decoded reply is surfaced on the `emitter`'s `message` event — `send`
@@ -98,10 +173,10 @@ export declare interface ClientTransportInterface {
98
173
  * transport, its synchronous reply emitted), not when a logical response arrives;
99
174
  * the {@link MCPClientInterface} awaits the response through its `id` correlation.
100
175
  *
101
- * @param message - One message, or a batch of them, to write to the wire
102
- * @returns Resolves once the message(s) have been sent
176
+ * @param message - The message to write to the wire
177
+ * @returns Resolves once the message has been sent
103
178
  */
104
- send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void>;
179
+ send(message: JSONRPCMessage): Promise<void>;
105
180
  /**
106
181
  * Close the transport — end the connection and release resources.
107
182
  *
@@ -110,6 +185,36 @@ export declare interface ClientTransportInterface {
110
185
  close(): Promise<void>;
111
186
  }
112
187
 
188
+ /**
189
+ * Adapt an {@link MCPTransportInterface} (the environment-agnostic duplex message
190
+ * channel) into a {@link ClientTransportInterface} — the additive bridge that lets
191
+ * `createMCPClient` run over the new port without any change to `MCPClient`'s
192
+ * existing shape.
193
+ *
194
+ * @remarks
195
+ * Hand the RESULT to `createMCPClient({ transport })`, then pass the SAME
196
+ * `transport` to {@link import('./helpers.js').bindClient} to complete the inbound
197
+ * wiring: `send` serializes each outbound {@link JSONRPCMessage} and writes it via
198
+ * `transport.send`; `close` closes the underlying
199
+ * `transport`; `start` is a no-op (the duplex channel is already open by the time
200
+ * it is handed in — there is no separate connect step at this layer); `session` is
201
+ * always `undefined` (session correlation is a higher-level concern the duplex port
202
+ * does not carry). Inbound delivery (`emitter`'s `message` / `close` events) is
203
+ * `bindClient`'s job, not this factory's — the returned object exposes a `message`-
204
+ * capable emitter for `bindClient` to push onto.
205
+ *
206
+ * @param transport - The duplex channel to adapt
207
+ * @returns A {@link ClientTransportInterface} `createMCPClient` can drive
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })
212
+ * const unbind = bindClient(client, transport)
213
+ * await client.connect()
214
+ * ```
215
+ */
216
+ export declare function createDuplexClientTransport(transport: MCPTransportInterface): ClientTransportInterface;
217
+
113
218
  /**
114
219
  * Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE
115
220
  * MCP server over an injected {@link import('./types.js').ClientTransportInterface},
@@ -119,7 +224,8 @@ export declare interface ClientTransportInterface {
119
224
  * @remarks
120
225
  * The egress mirror of {@link createMCPServer}: where the server exposes a local tool
121
226
  * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,
122
- * `tools()` lists + wraps the remote tools (each `execute` calls back over the wire),
227
+ * validates and exposes the negotiated protocol, `tools()` lists + wraps the remote
228
+ * tools (each `execute` calls back over the wire),
123
229
  * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws
124
230
  * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}
125
231
  * isolates it). The transport is injected — a concrete one (the HTTP transport over
@@ -274,6 +380,20 @@ export declare function isJSONRPCRequest(value: unknown): value is JSONRPCReques
274
380
  */
275
381
  export declare function isJSONRPCResponse(value: unknown): value is JSONRPCResponse;
276
382
 
383
+ /**
384
+ * Determine whether an unknown value is an {@link MCPError}.
385
+ *
386
+ * @param value - The unknown value to inspect
387
+ * @returns `true` only when the value is an `MCPError`
388
+ *
389
+ * @example
390
+ * ```ts
391
+ * isMCPError(new MCPError('Method not found', -32601)) // true
392
+ * isMCPError(new Error('Method not found')) // false
393
+ * ```
394
+ */
395
+ export declare function isMCPError(value: unknown): value is MCPError;
396
+
277
397
  /**
278
398
  * Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,
279
399
  * or absent.
@@ -397,8 +517,9 @@ export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
397
517
  *
398
518
  * @remarks
399
519
  * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;
400
- * this client ISSUES them over a transport. `connect` runs `initialize` then sends
401
- * `notifications/initialized`; `tools()` lists the remote tools and wraps each as a
520
+ * this client ISSUES them over a transport. `connect` runs `initialize`, validates and
521
+ * exposes the negotiated `protocol`, then sends `notifications/initialized`; `tools()`
522
+ * lists the remote tools and wraps each as a
402
523
  * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
403
524
  * remote `tools/call` and returns the tool's value (a remote `isError: true` throws
404
525
  * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}
@@ -431,6 +552,7 @@ export declare class MCPClient implements MCPClientInterface {
431
552
  constructor(options: MCPClientOptions);
432
553
  get emitter(): EmitterInterface<MCPClientEventMap>;
433
554
  get connected(): boolean;
555
+ get protocol(): string | undefined;
434
556
  get transport(): ClientTransportInterface;
435
557
  on<K extends keyof MCPClientEventMap>(event: K, handler: (...args: MCPClientEventMap[K]) => void): void;
436
558
  connect(): Promise<void>;
@@ -475,7 +597,8 @@ export declare type MCPClientEventMap = {
475
597
  * @remarks
476
598
  * - **The mirror of {@link MCPServerInterface}.** Where the server DISPATCHES requests
477
599
  * over a tool registry, the client ISSUES them over a transport: `connect` runs the
478
- * `initialize` handshake (then sends `notifications/initialized`); `tools()` lists
600
+ * `initialize` handshake, validates and exposes the negotiated `protocol` (then sends
601
+ * `notifications/initialized`); `tools()` lists
479
602
  * the remote tools and wraps each as a local {@link ToolInterface} whose `execute`
480
603
  * calls back through `call`; `call(name, args)` runs a remote `tools/call` and
481
604
  * returns the tool's value (a remote tool FAILURE — `isError: true` — throws locally,
@@ -499,6 +622,11 @@ export declare interface MCPClientInterface {
499
622
  readonly emitter: EmitterInterface<MCPClientEventMap>;
500
623
  /** Whether the `initialize` handshake has completed and the client is connected. */
501
624
  readonly connected: boolean;
625
+ /**
626
+ * The MCP protocol revision negotiated by {@link connect}, or `undefined` before
627
+ * connecting and after {@link disconnect}.
628
+ */
629
+ readonly protocol: string | undefined;
502
630
  /** The injected transport the client drives the remote server over. */
503
631
  readonly transport: ClientTransportInterface;
504
632
  /**
@@ -511,11 +639,14 @@ export declare interface MCPClientInterface {
511
639
  on<K extends keyof MCPClientEventMap>(event: K, handler: (...args: MCPClientEventMap[K]) => void): void;
512
640
  /**
513
641
  * Connect to the remote server — open the transport and run the `initialize`
514
- * handshake (then send `notifications/initialized`).
642
+ * handshake, validate its negotiated protocol, then send
643
+ * `notifications/initialized`.
515
644
  *
516
645
  * @remarks
517
646
  * Idempotent — a second `connect` while already connected is a no-op. On success
518
- * the `connect` event fires.
647
+ * {@link protocol} contains a supported revision and the `connect` event fires. A
648
+ * non-string or unsupported revision closes the transport and rejects without
649
+ * connecting or sending the initialized notification.
519
650
  *
520
651
  * @returns Resolves once the handshake completes and the client is connected
521
652
  */
@@ -526,7 +657,7 @@ export declare interface MCPClientInterface {
526
657
  *
527
658
  * @remarks
528
659
  * Idempotent — a second `disconnect` while already disconnected is a no-op. The
529
- * `disconnect` event fires.
660
+ * `disconnect` event fires and {@link protocol} becomes `undefined`.
530
661
  *
531
662
  * @returns Resolves once the transport is closed
532
663
  */
@@ -598,6 +729,37 @@ export declare interface MCPContent {
598
729
  readonly text: string;
599
730
  }
600
731
 
732
+ /**
733
+ * A remote Model Context Protocol JSON-RPC error, preserving its machine-readable
734
+ * numeric code and optional structured context.
735
+ *
736
+ * @remarks
737
+ * {@link MCPClient} throws this error only for a remote JSON-RPC `error` response.
738
+ * Local lifecycle and transport conditions such as disconnects and request timeouts
739
+ * remain plain `Error`s. `context` carries the response's optional `error.data`
740
+ * unchanged and is `undefined` when the peer omitted it.
741
+ *
742
+ * @example
743
+ * ```ts
744
+ * const error = new MCPError('Method not found', -32601, { method: 'missing' })
745
+ * error.code // -32601
746
+ * error.context // { method: 'missing' }
747
+ * ```
748
+ */
749
+ export declare class MCPError extends Error {
750
+ readonly name = "MCPError";
751
+ readonly code: number;
752
+ readonly context: unknown;
753
+ /**
754
+ * Create a remote MCP protocol error.
755
+ *
756
+ * @param message - The human-readable JSON-RPC error message
757
+ * @param code - The machine-readable numeric JSON-RPC error code
758
+ * @param context - The optional JSON-RPC `error.data` payload
759
+ */
760
+ constructor(message: string, code: number, context?: unknown);
761
+ }
762
+
601
763
  /**
602
764
  * A transport-agnostic Model Context Protocol server — dispatches JSON-RPC 2.0
603
765
  * requests over a live {@link ToolManagerInterface}, with NO transport coupling.
@@ -657,6 +819,12 @@ export declare class MCPServer implements MCPServerInterface {
657
819
  export declare type MCPServerEventMap = {
658
820
  /** A request is being dispatched — its `method` and correlating `id` (`null` for a notification). */
659
821
  readonly request: readonly [method: string, id: string | number | null];
822
+ /**
823
+ * A transport-level fault surfaced while a bound {@link MCPTransportInterface} was
824
+ * piping a reply out (a `send` throw or rejection from {@link bindServer}). A DOMAIN
825
+ * event (a genuine I/O fault), distinct from the emitter's own listener-error channel.
826
+ */
827
+ readonly error: readonly [error: unknown];
660
828
  };
661
829
 
662
830
  /** The server identity echoed in the MCP `initialize` result's `serverInfo`. */
@@ -769,6 +937,31 @@ export declare interface MCPToolResult {
769
937
  readonly isError?: boolean;
770
938
  }
771
939
 
940
+ /**
941
+ * A duplex message channel an environment face provides to the pure engine — the
942
+ * one port `bindServer` and `bindClient` (`./helpers.js`) pipe an
943
+ * {@link MCPServerInterface} / {@link MCPClientInterface} over.
944
+ *
945
+ * @remarks
946
+ * Messages are already-serialized JSON-RPC strings; the transport owns framing
947
+ * (a WS text frame, an SSE `data:` event, a newline-terminated stdio line, a
948
+ * `postMessage` payload) and never parses the string itself. `listen` and
949
+ * `closed` each register THE SINGLE handler for their event — a second call
950
+ * REPLACES the first (matching the emitter-free, minimal-surface carrier idiom
951
+ * `bindServer` / `bindClient` themselves rely on), not an additive subscription
952
+ * list.
953
+ */
954
+ export declare interface MCPTransportInterface {
955
+ /** Deliver one outbound JSON-RPC message (already serialized). */
956
+ readonly send: (message: string) => void | Promise<void>;
957
+ /** Register the single inbound-message handler — a second call REPLACES the first. */
958
+ readonly listen: (handler: (message: string) => void) => void;
959
+ /** Register the single closed handler — a second call REPLACES the first. */
960
+ readonly closed: (handler: () => void) => void;
961
+ /** Close the underlying channel. */
962
+ readonly close: () => void | Promise<void>;
963
+ }
964
+
772
965
  /**
773
966
  * Narrow an already-parsed value to a {@link JSONRPCMessage}, or `undefined` when
774
967
  * it is not one.
@@ -792,13 +985,14 @@ export declare interface MCPToolResult {
792
985
  export declare function parseJSONRPCMessage(value: unknown): JSONRPCMessage | undefined;
793
986
 
794
987
  /**
795
- * The MCP protocol revisions this server can negotiate — the current
796
- * {@link MCP_PROTOCOL_VERSION} plus a prior rev a client may still request.
988
+ * The MCP protocol revisions this server can negotiate.
797
989
  *
798
990
  * @remarks
799
991
  * `initialize` echoes the client's requested `protocolVersion` when it appears in
800
992
  * this list, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is
801
- * an immutable contract.
993
+ * an immutable contract. The package does not advertise `2025-03-26` because that
994
+ * revision mandates JSON-RPC batching, while this package accepts only individual
995
+ * JSON-RPC messages.
802
996
  */
803
997
  export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
804
998