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