@orkestrel/mcp 0.0.1

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.
@@ -0,0 +1,673 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_emitter = require("@orkestrel/emitter");
4
+ let _orkestrel_agent = require("@orkestrel/agent");
5
+ //#region src/core/constants.ts
6
+ /** The MCP protocol revision this server implements (the default negotiated version). */
7
+ var MCP_PROTOCOL_VERSION = "2025-06-18";
8
+ /**
9
+ * The MCP protocol revisions this server can negotiate — the current
10
+ * {@link MCP_PROTOCOL_VERSION} plus a prior rev a client may still request.
11
+ *
12
+ * @remarks
13
+ * `initialize` echoes the client's requested `protocolVersion` when it appears in
14
+ * this list, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is
15
+ * an immutable contract.
16
+ */
17
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2025-06-18", "2025-03-26"]);
18
+ /** JSON-RPC 2.0 reserved error: invalid JSON was received (the message did not parse). */
19
+ var JSONRPC_PARSE_ERROR = -32700;
20
+ /** JSON-RPC 2.0 reserved error: the payload was not a valid Request object. */
21
+ var JSONRPC_INVALID_REQUEST = -32600;
22
+ /** JSON-RPC 2.0 reserved error: the requested method does not exist. */
23
+ var JSONRPC_METHOD_NOT_FOUND = -32601;
24
+ /** JSON-RPC 2.0 reserved error: the method's parameters were invalid. */
25
+ var JSONRPC_INVALID_PARAMS = -32602;
26
+ /** JSON-RPC 2.0 implementation-defined server error (the `-32000` to `-32099` range). */
27
+ var JSONRPC_SERVER_ERROR = -32e3;
28
+ /** The default client name reported in the MCP `initialize` handshake (`clientInfo.name`). */
29
+ var DEFAULT_MCP_CLIENT_NAME = "taverna";
30
+ /** The default client version reported in the MCP `initialize` handshake (`clientInfo.version`). */
31
+ var DEFAULT_MCP_CLIENT_VERSION = "1.0.0";
32
+ /**
33
+ * The default per-request deadline (ms) an `MCPClient` applies when `options.timeout`
34
+ * is unset — a request the remote server does not answer within it rejects.
35
+ */
36
+ var DEFAULT_MCP_REQUEST_TIMEOUT = 3e4;
37
+ //#endregion
38
+ //#region src/core/validators.ts
39
+ /**
40
+ * Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,
41
+ * or absent.
42
+ *
43
+ * @remarks
44
+ * A request id is a string, a number, or `undefined` (its ABSENCE marks a
45
+ * NOTIFICATION). `null` is NOT a valid request id — it is valid only on a RESPONSE.
46
+ * Total (§14): any other input returns `false`.
47
+ *
48
+ * @param value - The already-parsed value to test
49
+ * @returns `true` when `value` is a string, a number, or `undefined`
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * isRequestId(1) // true
54
+ * isRequestId('abc') // true
55
+ * isRequestId(undefined) // true — a notification
56
+ * isRequestId(null) // false — valid only on a response
57
+ * ```
58
+ */
59
+ function isRequestId(value) {
60
+ return (0, _orkestrel_contract.isUndefined)(value) || (0, _orkestrel_contract.isString)(value) || (0, _orkestrel_contract.isNumber)(value);
61
+ }
62
+ /**
63
+ * Determine whether a parsed value is a {@link JSONRPCRequest}.
64
+ *
65
+ * @remarks
66
+ * A request is a record with `jsonrpc === '2.0'` and a string `method`. `id`, when
67
+ * present, must be a string or number; its ABSENCE is valid — that marks a
68
+ * NOTIFICATION (a fire-and-forget request that yields no response). `params`, when
69
+ * present, must be a record. Total (§14): any other input returns `false`.
70
+ *
71
+ * @param value - The already-parsed value to test
72
+ * @returns `true` when `value` is a valid JSON-RPC request
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * isJSONRPCRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // true
77
+ * isJSONRPCRequest({ jsonrpc: '2.0', method: 'notifications/initialized' }) // true — a notification
78
+ * isJSONRPCRequest({ jsonrpc: '1.0', method: 'ping' }) // false
79
+ * ```
80
+ */
81
+ function isJSONRPCRequest(value) {
82
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
83
+ if (value["jsonrpc"] !== "2.0" || !(0, _orkestrel_contract.isString)(value["method"])) return false;
84
+ if (!isRequestId(value["id"])) return false;
85
+ const params = value["params"];
86
+ return (0, _orkestrel_contract.isUndefined)(params) || (0, _orkestrel_contract.isRecord)(params);
87
+ }
88
+ /**
89
+ * Determine whether a parsed value is a {@link JSONRPCResponse}.
90
+ *
91
+ * @remarks
92
+ * A response is a record with `jsonrpc === '2.0'`, an `id` that is a string,
93
+ * number, or `null`, and EXACTLY ONE of a `result` (any value, including
94
+ * `undefined`'s absence) or an `error` (a record with a numeric `code` and string
95
+ * `message`). Total (§14).
96
+ *
97
+ * @param value - The already-parsed value to test
98
+ * @returns `true` when `value` is a valid JSON-RPC response
99
+ */
100
+ function isJSONRPCResponse(value) {
101
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
102
+ if (value["jsonrpc"] !== "2.0") return false;
103
+ const id = value["id"];
104
+ if (id !== null && !(0, _orkestrel_contract.isString)(id) && !(0, _orkestrel_contract.isNumber)(id)) return false;
105
+ const hasResult = Object.hasOwn(value, "result");
106
+ const error = value["error"];
107
+ const hasError = !(0, _orkestrel_contract.isUndefined)(error);
108
+ if (hasResult === hasError) return false;
109
+ if (hasError) return (0, _orkestrel_contract.isRecord)(error) && (0, _orkestrel_contract.isNumber)(error["code"]) && (0, _orkestrel_contract.isString)(error["message"]);
110
+ return true;
111
+ }
112
+ /**
113
+ * Determine whether a parsed value is a {@link JSONRPCMessage} — a request or a
114
+ * response.
115
+ *
116
+ * @remarks
117
+ * The union of {@link isJSONRPCRequest} and {@link isJSONRPCResponse}. Total (§14).
118
+ *
119
+ * @param value - The already-parsed value to test
120
+ * @returns `true` when `value` is a valid JSON-RPC request or response
121
+ */
122
+ function isJSONRPCMessage(value) {
123
+ return isJSONRPCRequest(value) || isJSONRPCResponse(value);
124
+ }
125
+ /**
126
+ * Determine whether a parsed value is an MCP `initialize` request — a
127
+ * {@link JSONRPCRequest} whose `method` is `'initialize'`.
128
+ *
129
+ * @param value - The already-parsed value to test
130
+ * @returns `true` when `value` is a valid `initialize` request
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * isInitializeRequest({ jsonrpc: '2.0', method: 'initialize', id: 1 }) // true
135
+ * isInitializeRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // false
136
+ * ```
137
+ */
138
+ function isInitializeRequest(value) {
139
+ return isJSONRPCRequest(value) && value.method === "initialize";
140
+ }
141
+ //#endregion
142
+ //#region src/core/parsers.ts
143
+ /**
144
+ * Narrow an already-parsed value to a {@link JSONRPCMessage}, or `undefined` when
145
+ * it is not one.
146
+ *
147
+ * @remarks
148
+ * Total (§14) — a non-message returns `undefined`, never throws. The input must
149
+ * ALREADY be `JSON.parse`d: the raw-string parse (which can throw on malformed
150
+ * JSON) happens in `MCPServer.handle` inside a try/catch that maps a parse failure
151
+ * to a `-32700` response. Sound with {@link isJSONRPCMessage}: a guard-valid input
152
+ * is returned unchanged, and every non-`undefined` output satisfies the guard.
153
+ *
154
+ * @param value - The already-parsed value to narrow
155
+ * @returns The value as a {@link JSONRPCMessage}, or `undefined`
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * parseJSONRPCMessage({ jsonrpc: '2.0', method: 'ping', id: 1 }) // the request
160
+ * parseJSONRPCMessage({ method: 'ping' }) // undefined — missing jsonrpc
161
+ * ```
162
+ */
163
+ function parseJSONRPCMessage(value) {
164
+ return isJSONRPCMessage(value) ? value : void 0;
165
+ }
166
+ //#endregion
167
+ //#region src/core/helpers.ts
168
+ /**
169
+ * Build a JSON-RPC success {@link JSONRPCResponse} — the `id` echoed, the method's
170
+ * value as `result`.
171
+ *
172
+ * @param id - The request's id (`null` only for a parse / invalid-request error)
173
+ * @param result - The method's return value
174
+ * @returns The success response envelope
175
+ */
176
+ function jsonRPCResult(id, result) {
177
+ return {
178
+ jsonrpc: "2.0",
179
+ id,
180
+ result
181
+ };
182
+ }
183
+ /**
184
+ * Build a JSON-RPC error {@link JSONRPCResponse} — the `id` echoed, the failure as
185
+ * an `error` object.
186
+ *
187
+ * @param id - The request's id (`null` for a parse / invalid-request error)
188
+ * @param code - One of the reserved JSON-RPC codes (see `./constants.js`)
189
+ * @param message - A short human description of the failure
190
+ * @param data - An OPTIONAL machine-readable payload (omitted from the envelope when absent)
191
+ * @returns The error response envelope
192
+ */
193
+ function jsonRPCError(id, code, message, data) {
194
+ return {
195
+ jsonrpc: "2.0",
196
+ id,
197
+ error: data === void 0 ? {
198
+ code,
199
+ message
200
+ } : {
201
+ code,
202
+ message,
203
+ data
204
+ }
205
+ };
206
+ }
207
+ /**
208
+ * Map a {@link ToolManagerInterface}'s definitions to MCP `tools/list` descriptors
209
+ * — renaming `parameters` to the wire's `inputSchema`.
210
+ *
211
+ * @remarks
212
+ * Each {@link import('@orkestrel/agent').ToolDefinition} carries through its
213
+ * `name` and (when present) `description`; its open JSON-Schema `parameters`
214
+ * becomes `inputSchema`, defaulting to an empty object schema (`{ type: 'object' }`)
215
+ * when a tool declares none (MCP requires an `inputSchema`).
216
+ *
217
+ * @param manager - The tool registry to describe
218
+ * @returns One {@link MCPToolDescriptor} per registered tool, in registry order
219
+ */
220
+ function buildToolDescriptors(manager) {
221
+ return manager.definitions().map((definition) => {
222
+ const descriptor = {
223
+ name: definition.name,
224
+ inputSchema: definition.parameters ?? { type: "object" }
225
+ };
226
+ if (definition.description !== void 0) descriptor.description = definition.description;
227
+ return descriptor;
228
+ });
229
+ }
230
+ /**
231
+ * Map an executed tool's {@link ToolResult} to an MCP {@link MCPToolResult} — the
232
+ * value (or error) as a `text` content block.
233
+ *
234
+ * @remarks
235
+ * The {@link ToolManagerInterface} already isolates a thrown tool into
236
+ * `result.error` (so the server adds NO try/catch around `execute`): when `error`
237
+ * is present, this builds an `isError: true` result carrying the error text, so the
238
+ * model sees the failure as a tool result it can react to rather than a protocol
239
+ * error; otherwise it serializes `result.value` (via `JSON.stringify`) into one
240
+ * `text` block.
241
+ *
242
+ * @param result - The tool's execution outcome
243
+ * @returns The MCP tool-call result
244
+ */
245
+ function buildToolResult(result) {
246
+ if (result.error !== void 0) return {
247
+ content: [{
248
+ type: "text",
249
+ text: result.error
250
+ }],
251
+ isError: true
252
+ };
253
+ return { content: [{
254
+ type: "text",
255
+ text: result.value === void 0 ? "" : JSON.stringify(result.value)
256
+ }] };
257
+ }
258
+ /**
259
+ * Build the MCP `initialize` result — the negotiated protocol version, the
260
+ * advertised capabilities, and the server identity.
261
+ *
262
+ * @remarks
263
+ * Version negotiation echoes the client's `requested` version when it is one of the
264
+ * {@link SUPPORTED_PROTOCOL_VERSIONS}, else falls back to {@link MCP_PROTOCOL_VERSION}.
265
+ * `capabilities.tools` is an empty object — this server advertises the tools
266
+ * capability with no sub-options (no list-changed notification yet).
267
+ *
268
+ * @param name - The server name (echoed in `serverInfo`)
269
+ * @param version - The server version (echoed in `serverInfo`)
270
+ * @param requested - The client's requested protocol version (negotiated when supported)
271
+ * @returns The `initialize` result payload
272
+ */
273
+ function initializeResult(name, version, requested) {
274
+ return {
275
+ protocolVersion: requested !== void 0 && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : MCP_PROTOCOL_VERSION,
276
+ capabilities: { tools: {} },
277
+ serverInfo: {
278
+ name,
279
+ version
280
+ }
281
+ };
282
+ }
283
+ //#endregion
284
+ //#region src/core/MCPServer.ts
285
+ /**
286
+ * A transport-agnostic Model Context Protocol server — dispatches JSON-RPC 2.0
287
+ * requests over a live {@link ToolManagerInterface}, with NO transport coupling.
288
+ *
289
+ * @remarks
290
+ * - **Two entry points.** `dispatch(request)` runs an already-parsed request and
291
+ * resolves a {@link JSONRPCResponse} — or `undefined` for a NOTIFICATION (a
292
+ * request with no `id`). `handle(message)` is the string boundary: it
293
+ * `JSON.parse`s the raw message (a failure → a `-32700` response), narrows it to
294
+ * a request (a non-request → a `-32600` response), dispatches, and serializes the
295
+ * response back to a string (`undefined` for a notification).
296
+ * - **The method switch.** `initialize` negotiates the protocol version + advertises
297
+ * the tools capability; `notifications/initialized` is a notification (no
298
+ * response); `ping` returns `{}`; `tools/list` lists the registry's tools (its
299
+ * `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the
300
+ * {@link ToolManagerInterface} isolates a tool throw into the result `error`, which
301
+ * maps to an `isError: true` tool result — so the server adds NO try/catch). An
302
+ * unknown method → `-32601`; a `tools/call` with a missing / non-string `name` →
303
+ * `-32602`.
304
+ * - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,
305
+ * no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).
306
+ * - **Observable (§13).** The owned `emitter` fires `request` at the top of every
307
+ * dispatch; the emitter isolates a listener throw and routes it to its `error` handler
308
+ * (the `error` option), so a listener throw can never escape the dispatch.
309
+ *
310
+ * @example
311
+ * ```ts
312
+ * const tools = createToolManager()
313
+ * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
314
+ * const server = new MCPServer({ name: 'demo', version: '1.0.0', tools })
315
+ * await server.handle('{"jsonrpc":"2.0","method":"ping","id":1}') // '{"jsonrpc":"2.0","id":1,"result":{}}'
316
+ * ```
317
+ */
318
+ var MCPServer = class {
319
+ #emitter;
320
+ #name;
321
+ #version;
322
+ #tools;
323
+ constructor(options) {
324
+ this.#emitter = new _orkestrel_emitter.Emitter({
325
+ on: options.on,
326
+ error: options.error
327
+ });
328
+ this.#name = options.name;
329
+ this.#version = options.version;
330
+ this.#tools = options.tools;
331
+ }
332
+ get emitter() {
333
+ return this.#emitter;
334
+ }
335
+ get name() {
336
+ return this.#name;
337
+ }
338
+ get version() {
339
+ return this.#version;
340
+ }
341
+ async dispatch(request) {
342
+ const id = request.id ?? null;
343
+ this.#emitter.emit("request", request.method, id);
344
+ if (request.id === void 0) return;
345
+ switch (request.method) {
346
+ case "initialize": {
347
+ const requested = request.params?.["protocolVersion"];
348
+ return jsonRPCResult(id, initializeResult(this.#name, this.#version, (0, _orkestrel_contract.isString)(requested) ? requested : void 0));
349
+ }
350
+ case "ping": return jsonRPCResult(id, {});
351
+ case "tools/list": return jsonRPCResult(id, { tools: buildToolDescriptors(this.#tools) });
352
+ case "tools/call": return this.#call(request, id);
353
+ default: return jsonRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`);
354
+ }
355
+ }
356
+ async handle(message) {
357
+ let parsed;
358
+ try {
359
+ parsed = JSON.parse(message);
360
+ } catch {
361
+ return JSON.stringify(jsonRPCError(null, JSONRPC_PARSE_ERROR, "Parse error"));
362
+ }
363
+ const decoded = parseJSONRPCMessage(parsed);
364
+ if (decoded === void 0 || !("method" in decoded)) return JSON.stringify(jsonRPCError(null, JSONRPC_INVALID_REQUEST, "Invalid Request"));
365
+ const response = await this.dispatch(decoded);
366
+ return response === void 0 ? void 0 : JSON.stringify(response);
367
+ }
368
+ async #call(request, id) {
369
+ const params = request.params;
370
+ const name = params?.["name"];
371
+ if (!(0, _orkestrel_contract.isString)(name)) return jsonRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: a string `name` is required");
372
+ const rawArguments = params?.["arguments"];
373
+ const args = (0, _orkestrel_contract.isRecord)(rawArguments) ? rawArguments : {};
374
+ const callId = request.id === void 0 ? crypto.randomUUID() : String(request.id);
375
+ return jsonRPCResult(id, buildToolResult(await this.#tools.execute({
376
+ id: callId,
377
+ name,
378
+ arguments: args
379
+ })));
380
+ }
381
+ };
382
+ //#endregion
383
+ //#region src/core/MCPClient.ts
384
+ /**
385
+ * A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server
386
+ * over an injected {@link ClientTransportInterface}, runs the `initialize` handshake,
387
+ * and exposes the server's tools as local {@link ToolInterface}s an agent can run.
388
+ *
389
+ * @remarks
390
+ * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;
391
+ * this client ISSUES them over a transport. `connect` runs `initialize` then sends
392
+ * `notifications/initialized`; `tools()` lists the remote tools and wraps each as a
393
+ * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
394
+ * remote `tools/call` and returns the tool's value (a remote `isError: true` throws
395
+ * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}
396
+ * isolates it into a result `error` just like a local throw).
397
+ * - **Request↔response correlation.** Each request is tagged with a monotonic numeric
398
+ * `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects
399
+ * the matching {@link #pending} entry by `id`. A message that is NOT a response to a
400
+ * pending request is a server NOTIFICATION — re-surfaced on the `notification` event.
401
+ * - **Per-request deadline.** `#request` races `AbortSignal.timeout(this.#timeout)` (the
402
+ * taverna idiom — never a raw `setTimeout`): a server that never replies REJECTS the
403
+ * pending request once the deadline fires, never hanging.
404
+ * - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);
405
+ * the concrete transport is injected. Wire fields are narrowed via the contracts
406
+ * guards (no `as`).
407
+ * - **Observable (§13).** The owned `emitter` fires `connect` / `disconnect` /
408
+ * `notification` / `error`; the emitter isolates a listener throw and routes it to its
409
+ * `error` handler (the `error` option), so a listener throw can never escape.
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * const client = new MCPClient({ transport, name: 'agent', version: '1.0.0' })
414
+ * await client.connect()
415
+ * const tools = await client.tools()
416
+ * agent.context.tools.add(tools) // the remote tools are now the agent's
417
+ * const value = await client.call('search', { query: 'mcp' })
418
+ * ```
419
+ */
420
+ var MCPClient = class {
421
+ #emitter;
422
+ #transport;
423
+ #name;
424
+ #version;
425
+ #timeout;
426
+ #pending = /* @__PURE__ */ new Map();
427
+ #nextId = 0;
428
+ #connected = false;
429
+ constructor(options) {
430
+ this.#emitter = new _orkestrel_emitter.Emitter({
431
+ on: options.on,
432
+ error: options.error
433
+ });
434
+ this.#transport = options.transport;
435
+ this.#name = options.name ?? "taverna";
436
+ this.#version = options.version ?? "1.0.0";
437
+ this.#timeout = options.timeout ?? 3e4;
438
+ this.#transport.emitter.on("message", (message) => this.#receive(message));
439
+ }
440
+ get emitter() {
441
+ return this.#emitter;
442
+ }
443
+ get connected() {
444
+ return this.#connected;
445
+ }
446
+ get transport() {
447
+ return this.#transport;
448
+ }
449
+ on(event, handler) {
450
+ this.#emitter.on(event, handler);
451
+ }
452
+ async connect() {
453
+ if (this.#connected) return;
454
+ await this.#transport.start();
455
+ await this.#request("initialize", {
456
+ protocolVersion: MCP_PROTOCOL_VERSION,
457
+ capabilities: {},
458
+ clientInfo: {
459
+ name: this.#name,
460
+ version: this.#version
461
+ }
462
+ });
463
+ this.#connected = true;
464
+ await this.#transport.send({
465
+ jsonrpc: "2.0",
466
+ method: "notifications/initialized"
467
+ });
468
+ this.#emitter.emit("connect");
469
+ }
470
+ async disconnect() {
471
+ if (!this.#connected) return;
472
+ this.#connected = false;
473
+ for (const pending of this.#pending.values()) pending.reject(/* @__PURE__ */ new Error("MCP client disconnected"));
474
+ this.#pending.clear();
475
+ await this.#transport.close();
476
+ this.#emitter.emit("disconnect");
477
+ }
478
+ async tools() {
479
+ const result = await this.#request("tools/list");
480
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["tools"])) return [];
481
+ const tools = [];
482
+ for (const descriptor of result["tools"]) {
483
+ if (!(0, _orkestrel_contract.isRecord)(descriptor) || !(0, _orkestrel_contract.isString)(descriptor["name"])) continue;
484
+ const name = descriptor["name"];
485
+ tools.push(this.#tool(name, descriptor));
486
+ }
487
+ return tools;
488
+ }
489
+ async call(name, args) {
490
+ const result = await this.#request("tools/call", {
491
+ name,
492
+ arguments: args
493
+ });
494
+ const text = this.#text(result);
495
+ if ((0, _orkestrel_contract.isRecord)(result) && result["isError"] === true) throw new Error(text.length > 0 ? text : `MCP tool '${name}' failed`);
496
+ if (text.length === 0) return void 0;
497
+ try {
498
+ return JSON.parse(text);
499
+ } catch {
500
+ return text;
501
+ }
502
+ }
503
+ #request(method, params) {
504
+ this.#nextId += 1;
505
+ const id = this.#nextId;
506
+ const request = {
507
+ jsonrpc: "2.0",
508
+ id,
509
+ method,
510
+ ...params === void 0 ? {} : { params }
511
+ };
512
+ return new Promise((resolve, reject) => {
513
+ const deadline = AbortSignal.timeout(this.#timeout);
514
+ const settle = () => {
515
+ this.#pending.delete(id);
516
+ deadline.removeEventListener("abort", onDeadline);
517
+ };
518
+ const onDeadline = () => {
519
+ settle();
520
+ reject(/* @__PURE__ */ new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`));
521
+ };
522
+ deadline.addEventListener("abort", onDeadline, { once: true });
523
+ this.#pending.set(id, {
524
+ resolve: (value) => {
525
+ settle();
526
+ resolve(value);
527
+ },
528
+ reject: (error) => {
529
+ settle();
530
+ reject(error);
531
+ }
532
+ });
533
+ this.#transport.send(request).catch((error) => {
534
+ const pending = this.#pending.get(id);
535
+ if (pending === void 0) return;
536
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
537
+ });
538
+ });
539
+ }
540
+ #receive(message) {
541
+ if (isJSONRPCResponse(message) && isRequestId(message.id)) {
542
+ const pending = this.#pending.get(message.id);
543
+ if (pending !== void 0) {
544
+ if (message.error !== void 0) pending.reject(/* @__PURE__ */ new Error(`MCP error ${message.error.code}: ${message.error.message}`));
545
+ else pending.resolve(message.result);
546
+ return;
547
+ }
548
+ }
549
+ this.#emitter.emit("notification", message);
550
+ }
551
+ #tool(name, descriptor) {
552
+ const inputSchema = descriptor["inputSchema"];
553
+ const description = descriptor["description"];
554
+ const options = {
555
+ name,
556
+ execute: (args) => this.call(name, args)
557
+ };
558
+ if ((0, _orkestrel_contract.isString)(description)) options.description = description;
559
+ if ((0, _orkestrel_contract.isRecord)(inputSchema)) options.parameters = inputSchema;
560
+ return new _orkestrel_agent.Tool(options);
561
+ }
562
+ #text(result) {
563
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["content"])) return "";
564
+ const parts = [];
565
+ for (const block of result["content"]) if ((0, _orkestrel_contract.isRecord)(block) && (0, _orkestrel_contract.isString)(block["text"])) parts.push(block["text"]);
566
+ return parts.join("\n");
567
+ }
568
+ };
569
+ //#endregion
570
+ //#region src/core/factories.ts
571
+ /**
572
+ * Create a transport-agnostic Model Context Protocol server — exposes a live
573
+ * {@link import('@orkestrel/agent').ToolManagerInterface} over JSON-RPC 2.0
574
+ * (`initialize` / `ping` / `tools/list` / `tools/call`).
575
+ *
576
+ * @remarks
577
+ * Pump raw message strings through `handle` (parse → dispatch → serialize) from a
578
+ * transport, or call the typed `dispatch` directly with an already-parsed request.
579
+ * The server is provider-agnostic — JSON-RPC plus the tool registry, with no HTTP
580
+ * and no model. The {@link import('@orkestrel/agent').ToolManagerInterface} already
581
+ * isolates a thrown tool into a result error (surfaced as an MCP `isError: true`
582
+ * tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the
583
+ * `request` event via `server.emitter.on('request', …)` for tracing.
584
+ *
585
+ * @param options - `name` / `version` (the server identity), `tools` (the live
586
+ * registry to expose), an optional `description`, and the reserved `on`
587
+ * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})
588
+ * @returns A working {@link MCPServerInterface}
589
+ *
590
+ * @example
591
+ * ```ts
592
+ * import { createMCPServer, createTool, createToolManager } from '@src/core'
593
+ *
594
+ * const tools = createToolManager()
595
+ * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
596
+ *
597
+ * const server = createMCPServer({ name: 'calculator', version: '1.0.0', tools })
598
+ * server.emitter.on('request', (method, id) => log(method, id))
599
+ *
600
+ * // A transport pumps message strings through `handle`:
601
+ * const reply = await server.handle('{"jsonrpc":"2.0","method":"tools/list","id":1}')
602
+ * // reply → '{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"add","inputSchema":{"type":"object"}}]}}'
603
+ * ```
604
+ */
605
+ function createMCPServer(options) {
606
+ return new MCPServer(options);
607
+ }
608
+ /**
609
+ * Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE
610
+ * MCP server over an injected {@link import('./types.js').ClientTransportInterface},
611
+ * runs the `initialize` handshake, and exposes the server's tools as local
612
+ * {@link import('@orkestrel/agent').ToolInterface}s an agent can run.
613
+ *
614
+ * @remarks
615
+ * The egress mirror of {@link createMCPServer}: where the server exposes a local tool
616
+ * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,
617
+ * `tools()` lists + wraps the remote tools (each `execute` calls back over the wire),
618
+ * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws
619
+ * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}
620
+ * isolates it). The transport is injected — a concrete one (the HTTP transport over
621
+ * `fetch`) lives in `@src/server`; the client itself is provider-agnostic. Subscribe
622
+ * to `connect` / `disconnect` / `notification` via `client.on(...)` (or
623
+ * `client.emitter.on(...)`).
624
+ *
625
+ * @param options - `transport` (the carrier; REQUIRED), `name` / `version` (the client
626
+ * identity), `timeout` (the per-request deadline), and the reserved `on`
627
+ * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})
628
+ * @returns A working {@link MCPClientInterface}
629
+ *
630
+ * @example
631
+ * ```ts
632
+ * import { createMCPClient } from '@src/core'
633
+ * import { createHTTPClientTransport } from '@src/server'
634
+ *
635
+ * const client = createMCPClient({
636
+ * transport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),
637
+ * })
638
+ * await client.connect()
639
+ * agent.context.tools.add(await client.tools()) // give the agent the remote tools
640
+ * const value = await client.call('search', { query: 'mcp' })
641
+ * ```
642
+ */
643
+ function createMCPClient(options) {
644
+ return new MCPClient(options);
645
+ }
646
+ //#endregion
647
+ exports.DEFAULT_MCP_CLIENT_NAME = DEFAULT_MCP_CLIENT_NAME;
648
+ exports.DEFAULT_MCP_CLIENT_VERSION = DEFAULT_MCP_CLIENT_VERSION;
649
+ exports.DEFAULT_MCP_REQUEST_TIMEOUT = DEFAULT_MCP_REQUEST_TIMEOUT;
650
+ exports.JSONRPC_INVALID_PARAMS = JSONRPC_INVALID_PARAMS;
651
+ exports.JSONRPC_INVALID_REQUEST = JSONRPC_INVALID_REQUEST;
652
+ exports.JSONRPC_METHOD_NOT_FOUND = JSONRPC_METHOD_NOT_FOUND;
653
+ exports.JSONRPC_PARSE_ERROR = JSONRPC_PARSE_ERROR;
654
+ exports.JSONRPC_SERVER_ERROR = JSONRPC_SERVER_ERROR;
655
+ exports.MCPClient = MCPClient;
656
+ exports.MCPServer = MCPServer;
657
+ exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
658
+ exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
659
+ exports.buildToolDescriptors = buildToolDescriptors;
660
+ exports.buildToolResult = buildToolResult;
661
+ exports.createMCPClient = createMCPClient;
662
+ exports.createMCPServer = createMCPServer;
663
+ exports.initializeResult = initializeResult;
664
+ exports.isInitializeRequest = isInitializeRequest;
665
+ exports.isJSONRPCMessage = isJSONRPCMessage;
666
+ exports.isJSONRPCRequest = isJSONRPCRequest;
667
+ exports.isJSONRPCResponse = isJSONRPCResponse;
668
+ exports.isRequestId = isRequestId;
669
+ exports.jsonRPCError = jsonRPCError;
670
+ exports.jsonRPCResult = jsonRPCResult;
671
+ exports.parseJSONRPCMessage = parseJSONRPCMessage;
672
+
673
+ //# sourceMappingURL=index.cjs.map