@orkestrel/mcp 0.0.7 → 0.0.8
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.
- package/README.md +7 -7
- package/dist/src/browser/index.d.ts +1 -1
- package/dist/src/core/index.cjs +21 -21
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +29 -29
- package/dist/src/core/index.d.ts +29 -29
- package/dist/src/core/index.js +20 -20
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1 -1
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1 -1
- package/dist/src/server/index.d.ts +1 -1
- package/dist/src/server/index.js +1 -1
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# @orkestrel/mcp
|
|
2
2
|
|
|
3
3
|
A typed [Model Context Protocol](https://modelcontextprotocol.io) client/server
|
|
4
|
-
for the `@orkestrel` line,
|
|
5
|
-
transports. `createMCPServer`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
for the `@orkestrel` line, bridging the `@orkestrel/tool` registry to MCP with
|
|
5
|
+
pluggable HTTP, WebSocket, and stdio transports. `createMCPServer` exposes a
|
|
6
|
+
live `ToolManagerInterface`; `createMCPClient` drives a remote MCP server and
|
|
7
|
+
surfaces its tools as local `ToolInterface`s. No agent runtime is required.
|
|
8
|
+
The dispatch core is transport- and provider-agnostic
|
|
9
9
|
(`src/core` — JSON-RPC 2.0, no HTTP, no `as`); every transport (Streamable
|
|
10
10
|
HTTP over `@orkestrel/router` / `@orkestrel/server`, WebSocket over
|
|
11
11
|
`@orkestrel/websocket`, and stdio over `node:child_process`) lives one layer
|
|
@@ -31,10 +31,10 @@ Expose a tool registry over MCP, mounted on the HTTP spine:
|
|
|
31
31
|
```ts
|
|
32
32
|
import { createMCPServer } from '@orkestrel/mcp'
|
|
33
33
|
import { createMCPRoutes } from '@orkestrel/mcp/server'
|
|
34
|
-
import { createToolManager } from '@orkestrel/
|
|
34
|
+
import { createTool, createToolManager } from '@orkestrel/tool'
|
|
35
35
|
|
|
36
36
|
const tools = createToolManager()
|
|
37
|
-
tools.add({
|
|
37
|
+
tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
|
|
38
38
|
|
|
39
39
|
const mcp = createMCPServer({ name: 'calculator', version: '1.0.0', tools })
|
|
40
40
|
const routes = createMCPRoutes(mcp) // POST /mcp dispatches JSON-RPC (JSON or SSE per Accept)
|
|
@@ -7,7 +7,7 @@ import { JSONRPCMessage as JSONRPCMessage_2 } from '../../core/index.ts';
|
|
|
7
7
|
import { MCPServerInterface } from '../core/index.ts';
|
|
8
8
|
import { MCPTransportInterface } from '../core/index.ts';
|
|
9
9
|
import { MCPTransportInterface as MCPTransportInterface_2 } from '../../core/index.ts';
|
|
10
|
-
import { ToolManagerInterface } from '@orkestrel/
|
|
10
|
+
import { ToolManagerInterface } from '@orkestrel/tool';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Create the browser-face HTTP CLIENT transport for an
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
3
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
4
|
-
let
|
|
4
|
+
let _orkestrel_tool = require("@orkestrel/tool");
|
|
5
5
|
//#region src/core/constants.ts
|
|
6
6
|
/** The MCP protocol revision this server implements (the default negotiated version). */
|
|
7
7
|
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
@@ -265,7 +265,7 @@ function jsonRPCError(id, code, message, data) {
|
|
|
265
265
|
* — renaming `parameters` to the wire's `inputSchema`.
|
|
266
266
|
*
|
|
267
267
|
* @remarks
|
|
268
|
-
* Each {@link import('@orkestrel/
|
|
268
|
+
* Each {@link import('@orkestrel/tool').ToolDefinition} carries through its
|
|
269
269
|
* `name` and (when present) `description`; its open JSON-Schema `parameters`
|
|
270
270
|
* becomes `inputSchema`, defaulting to an empty object schema (`{ type: 'object' }`)
|
|
271
271
|
* when a tool declares none (MCP requires an `inputSchema`).
|
|
@@ -288,18 +288,18 @@ function buildToolDescriptors(manager) {
|
|
|
288
288
|
* value (or error) as a `text` content block.
|
|
289
289
|
*
|
|
290
290
|
* @remarks
|
|
291
|
-
* The {@link ToolManagerInterface} already isolates a thrown tool into
|
|
292
|
-
* `
|
|
293
|
-
*
|
|
291
|
+
* The {@link ToolManagerInterface} already isolates a thrown tool into a
|
|
292
|
+
* `success: false` result (so the server adds NO try/catch around `execute`):
|
|
293
|
+
* that branch builds an `isError: true` result carrying `result.error`, so the
|
|
294
294
|
* model sees the failure as a tool result it can react to rather than a protocol
|
|
295
|
-
* error;
|
|
296
|
-
* `text` block.
|
|
295
|
+
* error; the `success: true` branch serializes `result.value` (via
|
|
296
|
+
* `JSON.stringify`) into one `text` block.
|
|
297
297
|
*
|
|
298
298
|
* @param result - The tool's execution outcome
|
|
299
299
|
* @returns The MCP tool-call result
|
|
300
300
|
*/
|
|
301
301
|
function buildToolResult(result) {
|
|
302
|
-
if (result.
|
|
302
|
+
if (!result.success) return {
|
|
303
303
|
content: [{
|
|
304
304
|
type: "text",
|
|
305
305
|
text: result.error
|
|
@@ -479,10 +479,10 @@ function bindClient(client, transport) {
|
|
|
479
479
|
* the tools capability; `notifications/initialized` is a notification (no
|
|
480
480
|
* response); `ping` returns `{}`; `tools/list` lists the registry's tools (its
|
|
481
481
|
* `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the
|
|
482
|
-
* {@link ToolManagerInterface} isolates a tool throw into
|
|
483
|
-
* maps to an `isError: true` tool result — so the server adds NO
|
|
484
|
-
* unknown method → `-32601`; a `tools/call` with a missing /
|
|
485
|
-
* `-32602`.
|
|
482
|
+
* {@link ToolManagerInterface} isolates a tool throw into a `success: false`
|
|
483
|
+
* result, which maps to an `isError: true` tool result — so the server adds NO
|
|
484
|
+
* try/catch). An unknown method → `-32601`; a `tools/call` with a missing /
|
|
485
|
+
* non-string `name` → `-32602`.
|
|
486
486
|
* - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,
|
|
487
487
|
* no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).
|
|
488
488
|
* - **Observable (§13).** The owned `emitter` fires `request` at the top of every
|
|
@@ -575,8 +575,8 @@ var MCPServer = class {
|
|
|
575
575
|
* lists the remote tools and wraps each as a
|
|
576
576
|
* local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
|
|
577
577
|
* remote `tools/call` and returns the tool's value (a remote `isError: true` throws
|
|
578
|
-
* locally, so an agent's {@link import('@orkestrel/
|
|
579
|
-
* isolates it into a
|
|
578
|
+
* locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}
|
|
579
|
+
* isolates it into a `success: false` result just like a local throw).
|
|
580
580
|
* - **Request↔response correlation.** Each request is tagged with a monotonic numeric
|
|
581
581
|
* `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects
|
|
582
582
|
* the matching {@link #pending} entry by `id`. A message that is NOT a response to a
|
|
@@ -737,7 +737,7 @@ var MCPClient = class {
|
|
|
737
737
|
};
|
|
738
738
|
if ((0, _orkestrel_contract.isString)(description)) options.description = description;
|
|
739
739
|
if ((0, _orkestrel_contract.isRecord)(inputSchema)) options.parameters = inputSchema;
|
|
740
|
-
return new
|
|
740
|
+
return new _orkestrel_tool.Tool(options);
|
|
741
741
|
}
|
|
742
742
|
#text(result) {
|
|
743
743
|
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["content"])) return "";
|
|
@@ -761,16 +761,16 @@ var MCPClient = class {
|
|
|
761
761
|
//#region src/core/factories.ts
|
|
762
762
|
/**
|
|
763
763
|
* Create a transport-agnostic Model Context Protocol server — exposes a live
|
|
764
|
-
* {@link import('@orkestrel/
|
|
764
|
+
* {@link import('@orkestrel/tool').ToolManagerInterface} over JSON-RPC 2.0
|
|
765
765
|
* (`initialize` / `ping` / `tools/list` / `tools/call`).
|
|
766
766
|
*
|
|
767
767
|
* @remarks
|
|
768
768
|
* Pump raw message strings through `handle` (parse → dispatch → serialize) from a
|
|
769
769
|
* transport, or call the typed `dispatch` directly with an already-parsed request.
|
|
770
770
|
* The server is provider-agnostic — JSON-RPC plus the tool registry, with no HTTP
|
|
771
|
-
* and no model. The {@link import('@orkestrel/
|
|
772
|
-
* isolates a thrown tool into a result
|
|
773
|
-
* tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the
|
|
771
|
+
* and no model. The {@link import('@orkestrel/tool').ToolManagerInterface} already
|
|
772
|
+
* isolates a thrown tool into a `success: false` result (surfaced as an MCP
|
|
773
|
+
* `isError: true` tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the
|
|
774
774
|
* `request` event via `server.emitter.on('request', …)` for tracing.
|
|
775
775
|
*
|
|
776
776
|
* @param options - `name` / `version` (the server identity), `tools` (the live
|
|
@@ -800,7 +800,7 @@ function createMCPServer(options) {
|
|
|
800
800
|
* Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE
|
|
801
801
|
* MCP server over an injected {@link import('./types.js').ClientTransportInterface},
|
|
802
802
|
* runs the `initialize` handshake, and exposes the server's tools as local
|
|
803
|
-
* {@link import('@orkestrel/
|
|
803
|
+
* {@link import('@orkestrel/tool').ToolInterface}s an agent can run.
|
|
804
804
|
*
|
|
805
805
|
* @remarks
|
|
806
806
|
* The egress mirror of {@link createMCPServer}: where the server exposes a local tool
|
|
@@ -808,7 +808,7 @@ function createMCPServer(options) {
|
|
|
808
808
|
* validates and exposes the negotiated protocol, `tools()` lists + wraps the remote
|
|
809
809
|
* tools (each `execute` calls back over the wire),
|
|
810
810
|
* and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws
|
|
811
|
-
* locally, so an agent's {@link import('@orkestrel/
|
|
811
|
+
* locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}
|
|
812
812
|
* isolates it). The transport is injected — a concrete one (the HTTP transport over
|
|
813
813
|
* `fetch`) lives in `@src/server`; the client itself is provider-agnostic. Subscribe
|
|
814
814
|
* to `connect` / `disconnect` / `notification` via `client.on(...)` (or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["#emitter","#name","#version","#tools","#call","#emitter","#transport","#name","#version","#timeout","#pending","#receive","#connected","#protocol","#request","#settle","#tool","#text","#nextId","#timeoutRequest"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/validators.ts","../../../src/core/parsers.ts","../../../src/core/helpers.ts","../../../src/core/MCPServer.ts","../../../src/core/MCPClient.ts","../../../src/core/factories.ts"],"sourcesContent":["// MCP protocol revisions + the reserved JSON-RPC 2.0 error codes. The negotiated\n// protocol version is the current rev unless the client requests a supported\n// one (see `initializeResult` in ./helpers.js). Transport-level header names\n// (session / version headers) belong to the HTTP transport sub-chunk, NOT here.\n\n/** The MCP protocol revision this server implements (the default negotiated version). */\nexport const MCP_PROTOCOL_VERSION = '2025-06-18'\n\n/**\n * The MCP protocol revisions this server can negotiate.\n *\n * @remarks\n * `initialize` echoes the client's requested `protocolVersion` when it appears in\n * this list, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is\n * an immutable contract. The package does not advertise `2025-03-26` because that\n * revision mandates JSON-RPC batching, while this package accepts only individual\n * JSON-RPC messages.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze(['2025-06-18'])\n\n/** JSON-RPC 2.0 reserved error: invalid JSON was received (the message did not parse). */\nexport const JSONRPC_PARSE_ERROR = -32700\n\n/** JSON-RPC 2.0 reserved error: the payload was not a valid Request object. */\nexport const JSONRPC_INVALID_REQUEST = -32600\n\n/** JSON-RPC 2.0 reserved error: the requested method does not exist. */\nexport const JSONRPC_METHOD_NOT_FOUND = -32601\n\n/** JSON-RPC 2.0 reserved error: the method's parameters were invalid. */\nexport const JSONRPC_INVALID_PARAMS = -32602\n\n/** JSON-RPC 2.0 implementation-defined server error (the `-32000` to `-32099` range). */\nexport const JSONRPC_SERVER_ERROR = -32000\n\n// MCP CLIENT defaults — the identity an `MCPClient` reports in the `initialize`\n// handshake (`clientInfo`) and the per-request deadline, when the caller supplies\n// none. The egress mirror of the server's protocol-version constants above.\n\n/** The default client name reported in the MCP `initialize` handshake (`clientInfo.name`). */\nexport const DEFAULT_MCP_CLIENT_NAME = 'taverna'\n\n/** The default client version reported in the MCP `initialize` handshake (`clientInfo.version`). */\nexport const DEFAULT_MCP_CLIENT_VERSION = '1.0.0'\n\n/**\n * The default per-request deadline (ms) an `MCPClient` applies when `options.timeout`\n * is unset — a request the remote server does not answer within it rejects.\n */\nexport const DEFAULT_MCP_REQUEST_TIMEOUT = 30_000\n","/**\n * A remote Model Context Protocol JSON-RPC error, preserving its machine-readable\n * numeric code and optional structured context.\n *\n * @remarks\n * {@link MCPClient} throws this error only for a remote JSON-RPC `error` response.\n * Local lifecycle and transport conditions such as disconnects and request timeouts\n * remain plain `Error`s. `context` carries the response's optional `error.data`\n * unchanged and is `undefined` when the peer omitted it.\n *\n * @example\n * ```ts\n * const error = new MCPError('Method not found', -32601, { method: 'missing' })\n * error.code // -32601\n * error.context // { method: 'missing' }\n * ```\n */\nexport class MCPError extends Error {\n\toverride readonly name = 'MCPError'\n\treadonly code: number\n\treadonly context: unknown\n\n\t/**\n\t * Create a remote MCP protocol error.\n\t *\n\t * @param message - The human-readable JSON-RPC error message\n\t * @param code - The machine-readable numeric JSON-RPC error code\n\t * @param context - The optional JSON-RPC `error.data` payload\n\t */\n\tconstructor(message: string, code: number, context?: unknown) {\n\t\tsuper(message)\n\t\tthis.code = code\n\t\tthis.context = context\n\t}\n}\n\n/**\n * Determine whether an unknown value is an {@link MCPError}.\n *\n * @param value - The unknown value to inspect\n * @returns `true` only when the value is an `MCPError`\n *\n * @example\n * ```ts\n * isMCPError(new MCPError('Method not found', -32601)) // true\n * isMCPError(new Error('Method not found')) // false\n * ```\n */\nexport function isMCPError(value: unknown): value is MCPError {\n\ttry {\n\t\t// A revoked Proxy or a hostile prototype can make `instanceof` throw — this guard\n\t\t// must stay total, so the check is wrapped rather than left to escape.\n\t\treturn value instanceof MCPError\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type { JSONRPCMessage, JSONRPCRequest, JSONRPCResponse } from './types.js'\nimport { isNumber, isRecord, isString, isUndefined } from '@orkestrel/contract'\n\n// AGENTS §14: every guard here is a TOTAL function over the already-`JSON.parse`d\n// value — adversarial input returns `false`, never throws. The raw-string\n// `JSON.parse` (which CAN throw) happens in `MCPServer.handle` inside a try/catch;\n// these guards only ever see a parsed `unknown`. Each is a flat structural test on\n// `isRecord` + field checks (no user callbacks), so totality is immediate.\n\n/**\n * Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,\n * or absent.\n *\n * @remarks\n * A request id is a string, a number, or `undefined` (its ABSENCE marks a\n * NOTIFICATION). `null` is NOT a valid request id — it is valid only on a RESPONSE.\n * Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a string, a number, or `undefined`\n *\n * @example\n * ```ts\n * isRequestId(1) // true\n * isRequestId('abc') // true\n * isRequestId(undefined) // true — a notification\n * isRequestId(null) // false — valid only on a response\n * ```\n */\nexport function isRequestId(value: unknown): value is string | number | undefined {\n\treturn isUndefined(value) || isString(value) || isNumber(value)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCRequest}.\n *\n * @remarks\n * A request is a record with `jsonrpc === '2.0'` and a string `method`. `id`, when\n * present, must be a string or number; its ABSENCE is valid — that marks a\n * NOTIFICATION (a fire-and-forget request that yields no response). `params`, when\n * present, must be a record. Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request\n *\n * @example\n * ```ts\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // true\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'notifications/initialized' }) // true — a notification\n * isJSONRPCRequest({ jsonrpc: '1.0', method: 'ping' }) // false\n * ```\n */\nexport function isJSONRPCRequest(value: unknown): value is JSONRPCRequest {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0' || !isString(value['method'])) {\n\t\treturn false\n\t}\n\tif (!isRequestId(value['id'])) {\n\t\treturn false\n\t}\n\tconst params = value['params']\n\treturn isUndefined(params) || isRecord(params)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCResponse}.\n *\n * @remarks\n * A response is a record with `jsonrpc === '2.0'`, an `id` that is a string,\n * number, or `null`, and EXACTLY ONE of a `result` (any value, including\n * `undefined`'s absence) or an `error` (a record with a numeric `code` and string\n * `message`). Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC response\n */\nexport function isJSONRPCResponse(value: unknown): value is JSONRPCResponse {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0') {\n\t\treturn false\n\t}\n\tconst id = value['id']\n\tif (id !== null && !isString(id) && !isNumber(id)) {\n\t\treturn false\n\t}\n\tconst hasResult = Object.hasOwn(value, 'result')\n\tconst error = value['error']\n\tconst hasError = !isUndefined(error)\n\t// Exactly one of result / error — never both, never neither.\n\tif (hasResult === hasError) {\n\t\treturn false\n\t}\n\tif (hasError) {\n\t\treturn isRecord(error) && isNumber(error['code']) && isString(error['message'])\n\t}\n\treturn true\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCMessage} — a request or a\n * response.\n *\n * @remarks\n * The union of {@link isJSONRPCRequest} and {@link isJSONRPCResponse}. Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request or response\n */\nexport function isJSONRPCMessage(value: unknown): value is JSONRPCMessage {\n\treturn isJSONRPCRequest(value) || isJSONRPCResponse(value)\n}\n\n/**\n * Determine whether a parsed value is an MCP `initialize` request — a\n * {@link JSONRPCRequest} whose `method` is `'initialize'`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid `initialize` request\n *\n * @example\n * ```ts\n * isInitializeRequest({ jsonrpc: '2.0', method: 'initialize', id: 1 }) // true\n * isInitializeRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // false\n * ```\n */\nexport function isInitializeRequest(value: unknown): value is JSONRPCRequest {\n\treturn isJSONRPCRequest(value) && value.method === 'initialize'\n}\n","import type { JSONRPCMessage } from './types.js'\nimport { isJSONRPCMessage } from './validators.js'\n\n/**\n * Narrow an already-parsed value to a {@link JSONRPCMessage}, or `undefined` when\n * it is not one.\n *\n * @remarks\n * Total (§14) — a non-message returns `undefined`, never throws. The input must\n * ALREADY be `JSON.parse`d: the raw-string parse (which can throw on malformed\n * JSON) happens in `MCPServer.handle` inside a try/catch that maps a parse failure\n * to a `-32700` response. Sound with {@link isJSONRPCMessage}: a guard-valid input\n * is returned unchanged, and every non-`undefined` output satisfies the guard.\n *\n * @param value - The already-parsed value to narrow\n * @returns The value as a {@link JSONRPCMessage}, or `undefined`\n *\n * @example\n * ```ts\n * parseJSONRPCMessage({ jsonrpc: '2.0', method: 'ping', id: 1 }) // the request\n * parseJSONRPCMessage({ method: 'ping' }) // undefined — missing jsonrpc\n * ```\n */\nexport function parseJSONRPCMessage(value: unknown): JSONRPCMessage | undefined {\n\treturn isJSONRPCMessage(value) ? value : undefined\n}\n","import type { ToolManagerInterface, ToolResult } from '@orkestrel/agent'\nimport type {\n\tJSONRPCResponse,\n\tMCPClientInterface,\n\tMCPServerInterface,\n\tMCPToolDescriptor,\n\tMCPToolResult,\n\tMCPTransportInterface,\n} from './types.js'\nimport { MCP_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from './constants.js'\nimport { parseJSONRPCMessage } from './parsers.js'\n\n// Pure dispatch builders (AGENTS §5: the dispatch branches stay exported helpers,\n// not hidden privates). Each turns a piece of MCP state into the JSON-RPC `result`\n// payload (or a response envelope) the server returns — independently testable.\n\n/**\n * Build a JSON-RPC success {@link JSONRPCResponse} — the `id` echoed, the method's\n * value as `result`.\n *\n * @param id - The request's id (`null` only for a parse / invalid-request error)\n * @param result - The method's return value\n * @returns The success response envelope\n */\nexport function jsonRPCResult(id: string | number | null, result: unknown): JSONRPCResponse {\n\treturn { jsonrpc: '2.0', id, result }\n}\n\n/**\n * Build a JSON-RPC error {@link JSONRPCResponse} — the `id` echoed, the failure as\n * an `error` object.\n *\n * @param id - The request's id (`null` for a parse / invalid-request error)\n * @param code - One of the reserved JSON-RPC codes (see `./constants.js`)\n * @param message - A short human description of the failure\n * @param data - An OPTIONAL machine-readable payload (omitted from the envelope when absent)\n * @returns The error response envelope\n */\nexport function jsonRPCError(\n\tid: string | number | null,\n\tcode: number,\n\tmessage: string,\n\tdata?: unknown,\n): JSONRPCResponse {\n\treturn {\n\t\tjsonrpc: '2.0',\n\t\tid,\n\t\terror: data === undefined ? { code, message } : { code, message, data },\n\t}\n}\n\n/**\n * Map a {@link ToolManagerInterface}'s definitions to MCP `tools/list` descriptors\n * — renaming `parameters` to the wire's `inputSchema`.\n *\n * @remarks\n * Each {@link import('@orkestrel/agent').ToolDefinition} carries through its\n * `name` and (when present) `description`; its open JSON-Schema `parameters`\n * becomes `inputSchema`, defaulting to an empty object schema (`{ type: 'object' }`)\n * when a tool declares none (MCP requires an `inputSchema`).\n *\n * @param manager - The tool registry to describe\n * @returns One {@link MCPToolDescriptor} per registered tool, in registry order\n */\nexport function buildToolDescriptors(manager: ToolManagerInterface): readonly MCPToolDescriptor[] {\n\treturn manager.definitions().map((definition) => {\n\t\tconst descriptor: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tinputSchema: Readonly<Record<string, unknown>>\n\t\t} = {\n\t\t\tname: definition.name,\n\t\t\tinputSchema: definition.parameters ?? { type: 'object' },\n\t\t}\n\t\tif (definition.description !== undefined) descriptor.description = definition.description\n\t\treturn descriptor\n\t})\n}\n\n/**\n * Map an executed tool's {@link ToolResult} to an MCP {@link MCPToolResult} — the\n * value (or error) as a `text` content block.\n *\n * @remarks\n * The {@link ToolManagerInterface} already isolates a thrown tool into\n * `result.error` (so the server adds NO try/catch around `execute`): when `error`\n * is present, this builds an `isError: true` result carrying the error text, so the\n * model sees the failure as a tool result it can react to rather than a protocol\n * error; otherwise it serializes `result.value` (via `JSON.stringify`) into one\n * `text` block.\n *\n * @param result - The tool's execution outcome\n * @returns The MCP tool-call result\n */\nexport function buildToolResult(result: ToolResult): MCPToolResult {\n\tif (result.error !== undefined) {\n\t\treturn { content: [{ type: 'text', text: result.error }], isError: true }\n\t}\n\t// A content block must carry a string `text`; `JSON.stringify(undefined)` is the value\n\t// `undefined` (which serializes away), so a value-less result becomes an empty text block.\n\tconst text = result.value === undefined ? '' : JSON.stringify(result.value)\n\treturn { content: [{ type: 'text', text }] }\n}\n\n/**\n * Build the MCP `initialize` result — the negotiated protocol version, the\n * advertised capabilities, and the server identity.\n *\n * @remarks\n * Version negotiation echoes the client's `requested` version when it is one of the\n * {@link SUPPORTED_PROTOCOL_VERSIONS}, else falls back to {@link MCP_PROTOCOL_VERSION}.\n * `capabilities.tools` is an empty object — this server advertises the tools\n * capability with no sub-options (no list-changed notification yet).\n *\n * @param name - The server name (echoed in `serverInfo`)\n * @param version - The server version (echoed in `serverInfo`)\n * @param requested - The client's requested protocol version (negotiated when supported)\n * @returns The `initialize` result payload\n */\nexport function initializeResult(\n\tname: string,\n\tversion: string,\n\trequested?: string,\n): Readonly<Record<string, unknown>> {\n\tconst protocolVersion =\n\t\trequested !== undefined && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)\n\t\t\t? requested\n\t\t\t: MCP_PROTOCOL_VERSION\n\treturn {\n\t\tprotocolVersion,\n\t\tcapabilities: { tools: {} },\n\t\tserverInfo: { name, version },\n\t}\n}\n\n// The environment-agnostic PORT binders — the keystone that lets an\n// {@link MCPServerInterface} / {@link MCPClientInterface} run over ANY\n// {@link MCPTransportInterface} (a Node stdio pair, a browser MessagePort, a Web\n// Worker `self`) with no per-environment dispatch/correlation wiring duplicated at\n// each face. Both are TOTAL: a `send` throw or rejection is caught and never\n// escapes as an unhandled rejection.\n\n/**\n * Pipe an {@link MCPTransportInterface} into an {@link MCPServerInterface} — every\n * inbound message runs through `server.handle`, and a defined reply is written back\n * via `transport.send`.\n *\n * @remarks\n * `server.handle` already turns a malformed message into a serialized `-32700` /\n * `-32600` reply and a notification into `undefined` (no reply), so this binder adds\n * no parsing of its own. A `transport.send` throw or rejection is caught and routed\n * to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);\n * a listener on that event that itself throws is swallowed (the end of the line —\n * the caller's own bug, never this binder's). The returned unbind DETACHES this\n * binder (further inbound messages and the transport's `closed` signal are ignored)\n * WITHOUT closing the transport — closing is the caller's decision.\n *\n * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind\n * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent\n * `bindServer` call on the SAME transport is never double-dispatched by a stale\n * subscription left behind — an unbind→rebind cycle yields exactly one reply per\n * request.\n *\n * @param server - The transport-agnostic server to dispatch inbound messages over\n * @param transport - The duplex channel to pipe the server over\n * @returns Detach this binder from the transport (does not close it)\n *\n * @example\n * ```ts\n * const unbind = bindServer(server, transport)\n * // ... later, detach without closing:\n * unbind()\n * ```\n */\nexport function bindServer(\n\tserver: MCPServerInterface,\n\ttransport: MCPTransportInterface,\n): () => void {\n\tlet active = true\n\ttransport.listen(async (message) => {\n\t\tif (!active) return\n\t\ttry {\n\t\t\tconst response = await server.handle(message)\n\t\t\tif (response !== undefined) await transport.send(response)\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tserver.emitter.emit('error', error)\n\t\t\t} catch {\n\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t}\n\t\t}\n\t})\n\ttransport.closed(() => {\n\t\tactive = false\n\t})\n\treturn () => {\n\t\tactive = false\n\t\ttransport.listen(() => {})\n\t\ttransport.closed(() => {})\n\t}\n}\n\n/**\n * Pipe an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every\n * inbound message is decoded and delivered onto the client's OWN transport\n * (`client.transport.emitter`'s `message` / `close` events), resolving/rejecting the\n * client's correlated pending requests exactly as a direct reply would.\n *\n * @remarks\n * The client's outbound writes flow through `client.transport.send` — its existing,\n * unmodified request/response correlation — so `client` must have been constructed\n * with a {@link import('./types.js').ClientTransportInterface} that itself carries\n * the SAME `transport` (see {@link import('./factories.js').createDuplexClientTransport},\n * the additive factory that adapts an {@link MCPTransportInterface} into that shape);\n * this binder then completes the inbound half by decoding each message and pushing it\n * onto `client.transport.emitter` (an {@link import('@orkestrel/emitter').EmitterInterface}\n * exposes `emit`, so no client modification is needed). A malformed / non-JSON-RPC\n * inbound message is DROPPED (§14, total — never throws); a delivery fault is routed to\n * `client.transport.emitter`'s `error` event (never rethrown). The returned unbind\n * DETACHES this binder (further inbound messages and the transport's `closed` signal are\n * ignored) WITHOUT closing the transport.\n *\n * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind\n * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent\n * `bindClient` call on the SAME transport is never double-dispatched by a stale\n * subscription left behind — an unbind→rebind cycle delivers exactly one `message`\n * emit per inbound reply.\n *\n * @param client - The transport-agnostic client whose transport to deliver messages onto\n * @param transport - The duplex channel to pipe the client over\n * @returns Detach this binder from the transport (does not close it)\n *\n * @example\n * ```ts\n * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })\n * const unbind = bindClient(client, transport)\n * await client.connect()\n * // ... later, detach without closing:\n * unbind()\n * ```\n */\nexport function bindClient(\n\tclient: MCPClientInterface,\n\ttransport: MCPTransportInterface,\n): () => void {\n\tlet active = true\n\ttransport.listen((message) => {\n\t\tif (!active) return\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(message)\n\t\t} catch {\n\t\t\treturn\n\t\t}\n\t\tconst decoded = parseJSONRPCMessage(parsed)\n\t\tif (decoded === undefined) return\n\t\ttry {\n\t\t\tclient.transport.emitter.emit('message', decoded)\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tclient.transport.emitter.emit('error', error)\n\t\t\t} catch {\n\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t}\n\t\t}\n\t})\n\ttransport.closed(() => {\n\t\tif (!active) return\n\t\tactive = false\n\t\ttry {\n\t\t\tclient.transport.emitter.emit('close')\n\t\t} catch {\n\t\t\t// A throwing `close` listener is the caller's own bug — the end of the line.\n\t\t}\n\t})\n\treturn () => {\n\t\tactive = false\n\t\ttransport.listen(() => {})\n\t\ttransport.closed(() => {})\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolManagerInterface } from '@orkestrel/agent'\nimport type {\n\tJSONRPCRequest,\n\tJSONRPCResponse,\n\tMCPServerEventMap,\n\tMCPServerInterface,\n\tMCPServerOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport {\n\tJSONRPC_INVALID_PARAMS,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_METHOD_NOT_FOUND,\n\tJSONRPC_PARSE_ERROR,\n} from './constants.js'\nimport {\n\tbuildToolDescriptors,\n\tbuildToolResult,\n\tinitializeResult,\n\tjsonRPCError,\n\tjsonRPCResult,\n} from './helpers.js'\nimport { parseJSONRPCMessage } from './parsers.js'\n\n/**\n * A transport-agnostic Model Context Protocol server — dispatches JSON-RPC 2.0\n * requests over a live {@link ToolManagerInterface}, with NO transport coupling.\n *\n * @remarks\n * - **Two entry points.** `dispatch(request)` runs an already-parsed request and\n * resolves a {@link JSONRPCResponse} — or `undefined` for a NOTIFICATION (a\n * request with no `id`). `handle(message)` is the string boundary: it\n * `JSON.parse`s the raw message (a failure → a `-32700` response), narrows it to\n * a request (a non-request → a `-32600` response), dispatches, and serializes the\n * response back to a string (`undefined` for a notification).\n * - **The method switch.** `initialize` negotiates the protocol version + advertises\n * the tools capability; `notifications/initialized` is a notification (no\n * response); `ping` returns `{}`; `tools/list` lists the registry's tools (its\n * `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the\n * {@link ToolManagerInterface} isolates a tool throw into the result `error`, which\n * maps to an `isError: true` tool result — so the server adds NO try/catch). An\n * unknown method → `-32601`; a `tools/call` with a missing / non-string `name` →\n * `-32602`.\n * - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,\n * no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `request` at the top of every\n * dispatch; the emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a listener throw can never escape the dispatch.\n *\n * @example\n * ```ts\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n * const server = new MCPServer({ name: 'demo', version: '1.0.0', tools })\n * await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}') // '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}'\n * ```\n */\nexport class MCPServer implements MCPServerInterface {\n\treadonly #emitter: Emitter<MCPServerEventMap>\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #tools: ToolManagerInterface\n\n\tconstructor(options: MCPServerOptions) {\n\t\tthis.#emitter = new Emitter<MCPServerEventMap>({\n\t\t\t...(options.on !== undefined ? { on: options.on } : {}),\n\t\t\t...(options.error !== undefined ? { error: options.error } : {}),\n\t\t})\n\t\tthis.#name = options.name\n\t\tthis.#version = options.version\n\t\tthis.#tools = options.tools\n\t}\n\n\tget emitter(): EmitterInterface<MCPServerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget name(): string {\n\t\treturn this.#name\n\t}\n\n\tget version(): string {\n\t\treturn this.#version\n\t}\n\n\tasync dispatch(request: JSONRPCRequest): Promise<JSONRPCResponse | undefined> {\n\t\tconst id = request.id ?? null\n\t\tthis.#emitter.emit('request', request.method, id)\n\t\t// JSON-RPC: a request with NO `id` is a NOTIFICATION — it is handled (the\n\t\t// `request` event already fired) but NEVER produces a response, whatever its\n\t\t// method (`notifications/initialized`, a fire-and-forget `ping`, an unknown\n\t\t// method — all silent). So short-circuit here, and the switch below only ever\n\t\t// runs for an id-bearing request that expects a reply.\n\t\tif (request.id === undefined) {\n\t\t\treturn undefined\n\t\t}\n\t\tswitch (request.method) {\n\t\t\tcase 'initialize': {\n\t\t\t\tconst requested = request.params?.['protocolVersion']\n\t\t\t\treturn jsonRPCResult(\n\t\t\t\t\tid,\n\t\t\t\t\tinitializeResult(this.#name, this.#version, isString(requested) ? requested : undefined),\n\t\t\t\t)\n\t\t\t}\n\t\t\tcase 'ping':\n\t\t\t\treturn jsonRPCResult(id, {})\n\t\t\tcase 'tools/list':\n\t\t\t\treturn jsonRPCResult(id, { tools: buildToolDescriptors(this.#tools) })\n\t\t\tcase 'tools/call':\n\t\t\t\treturn this.#call(request, id)\n\t\t\tdefault:\n\t\t\t\treturn jsonRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`)\n\t\t}\n\t}\n\n\tasync handle(message: string): Promise<string | undefined> {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(message)\n\t\t} catch {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'))\n\t\t}\n\t\tconst decoded = parseJSONRPCMessage(parsed)\n\t\t// Only a REQUEST is dispatchable — a response (or any non-message) is invalid input.\n\t\tif (decoded === undefined || !('method' in decoded)) {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'))\n\t\t}\n\t\tconst response = await this.dispatch(decoded)\n\t\treturn response === undefined ? undefined : JSON.stringify(response)\n\t}\n\n\t// Run a `tools/call`: narrow `params.name` (string) + `params.arguments` (record,\n\t// default `{}`) with no `as`, execute the tool (the manager isolates a throw into\n\t// `result.error`), and map the result to an MCP tool-call result.\n\tasync #call(request: JSONRPCRequest, id: string | number | null): Promise<JSONRPCResponse> {\n\t\tconst params = request.params\n\t\tconst name = params?.['name']\n\t\tif (!isString(name)) {\n\t\t\treturn jsonRPCError(id, JSONRPC_INVALID_PARAMS, 'Invalid params: a string `name` is required')\n\t\t}\n\t\tconst rawArguments = params?.['arguments']\n\t\tconst args = isRecord(rawArguments) ? rawArguments : {}\n\t\tconst callId = request.id === undefined ? crypto.randomUUID() : String(request.id)\n\t\tconst result = await this.#tools.execute({ id: callId, name, arguments: args })\n\t\treturn jsonRPCResult(id, buildToolResult(result))\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolInterface } from '@orkestrel/agent'\nimport type {\n\tClientTransportInterface,\n\tJSONRPCMessage,\n\tJSONRPCRequest,\n\tMCPClientEventMap,\n\tMCPClientInterface,\n\tMCPClientOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Tool } from '@orkestrel/agent'\nimport { isArray, isRecord, isString } from '@orkestrel/contract'\nimport {\n\tDEFAULT_MCP_CLIENT_NAME,\n\tDEFAULT_MCP_CLIENT_VERSION,\n\tDEFAULT_MCP_REQUEST_TIMEOUT,\n\tMCP_PROTOCOL_VERSION,\n\tSUPPORTED_PROTOCOL_VERSIONS,\n} from './constants.js'\nimport { MCPError } from './errors.js'\nimport { isJSONRPCResponse, isRequestId } from './validators.js'\n\n/**\n * A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server\n * over an injected {@link ClientTransportInterface}, runs the `initialize` handshake,\n * and exposes the server's tools as local {@link ToolInterface}s an agent can run.\n *\n * @remarks\n * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;\n * this client ISSUES them over a transport. `connect` runs `initialize`, validates and\n * exposes the negotiated `protocol`, then sends `notifications/initialized`; `tools()`\n * lists the remote tools and wraps each as a\n * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a\n * remote `tools/call` and returns the tool's value (a remote `isError: true` throws\n * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}\n * isolates it into a result `error` just like a local throw).\n * - **Request↔response correlation.** Each request is tagged with a monotonic numeric\n * `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects\n * the matching {@link #pending} entry by `id`. A message that is NOT a response to a\n * pending request is a server NOTIFICATION — re-surfaced on the `notification` event.\n * - **Per-request deadline.** `#request` races `AbortSignal.timeout(this.#timeout)` (the\n * taverna idiom — never a raw `setTimeout`): a server that never replies REJECTS the\n * pending request once the deadline fires, never hanging.\n * - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);\n * the concrete transport is injected. Wire fields are narrowed via the contracts\n * guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `connect` / `disconnect` /\n * `notification` / `error`; the emitter isolates a listener throw and routes it to its\n * `error` handler (the `error` option), so a listener throw can never escape.\n *\n * @example\n * ```ts\n * const client = new MCPClient({ transport, name: 'agent', version: '1.0.0' })\n * await client.connect()\n * const tools = await client.tools()\n * agent.context.tools.add(tools) // the remote tools are now the agent's\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport class MCPClient implements MCPClientInterface {\n\treadonly #emitter: Emitter<MCPClientEventMap>\n\treadonly #transport: ClientTransportInterface\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #timeout: number\n\t// The in-flight requests, keyed by JSON-RPC id, each holding its promise settlers —\n\t// resolved on the matching response, rejected on an error response, the deadline, or\n\t// `disconnect`. Genuinely private glue (§5): the settler shape lives inline here.\n\treadonly #pending = new Map<\n\t\tstring | number,\n\t\t{\n\t\t\treadonly resolve: (value: unknown) => void\n\t\t\treadonly reject: (reason?: unknown) => void\n\t\t\treadonly deadline: AbortSignal\n\t\t\treadonly timeout: () => void\n\t\t}\n\t>()\n\t#nextId = 0\n\t#connected = false\n\t#protocol: string | undefined = undefined\n\n\tconstructor(options: MCPClientOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientEventMap>({\n\t\t\t...(options.on !== undefined ? { on: options.on } : {}),\n\t\t\t...(options.error !== undefined ? { error: options.error } : {}),\n\t\t})\n\t\tthis.#transport = options.transport\n\t\tthis.#name = options.name ?? DEFAULT_MCP_CLIENT_NAME\n\t\tthis.#version = options.version ?? DEFAULT_MCP_CLIENT_VERSION\n\t\tthis.#timeout = options.timeout ?? DEFAULT_MCP_REQUEST_TIMEOUT\n\t\t// One message subscription for the client's whole life: a response settles its\n\t\t// pending request by id; anything else is a server notification.\n\t\tthis.#transport.emitter.on('message', (message) => this.#receive(message))\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget connected(): boolean {\n\t\treturn this.#connected\n\t}\n\n\tget protocol(): string | undefined {\n\t\treturn this.#protocol\n\t}\n\n\tget transport(): ClientTransportInterface {\n\t\treturn this.#transport\n\t}\n\n\ton<K extends keyof MCPClientEventMap>(\n\t\tevent: K,\n\t\thandler: (...args: MCPClientEventMap[K]) => void,\n\t): void {\n\t\tthis.#emitter.on(event, handler)\n\t}\n\n\tasync connect(): Promise<void> {\n\t\tif (this.#connected) return\n\t\tawait this.#transport.start()\n\t\t// The MCP handshake: negotiate the protocol version + advertise (empty) client\n\t\t// capabilities + identify ourselves, then mark connected and fire the no-args\n\t\t// `notifications/initialized` (a notification — no id, no response).\n\t\tconst result = await this.#request('initialize', {\n\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\tcapabilities: {},\n\t\t\tclientInfo: { name: this.#name, version: this.#version },\n\t\t})\n\t\tconst protocol = isRecord(result) ? result['protocolVersion'] : undefined\n\t\tif (!isString(protocol) || !SUPPORTED_PROTOCOL_VERSIONS.includes(protocol)) {\n\t\t\tawait this.#transport.close()\n\t\t\tif (isString(protocol)) {\n\t\t\t\tthrow new Error(`MCP server negotiated unsupported protocol version '${protocol}'`)\n\t\t\t}\n\t\t\tthrow new Error('MCP server returned a non-string protocol version')\n\t\t}\n\t\tthis.#protocol = protocol\n\t\tthis.#connected = true\n\t\tawait this.#transport.send({ jsonrpc: '2.0', method: 'notifications/initialized' })\n\t\tthis.#emitter.emit('connect')\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (!this.#connected) return\n\t\tthis.#connected = false\n\t\tthis.#protocol = undefined\n\t\t// Reject every still-pending request so no caller hangs past a disconnect, then\n\t\t// clear the map and tear the transport down.\n\t\tfor (const id of this.#pending.keys()) {\n\t\t\tthis.#settle(id, new Error('MCP client disconnected'), true)\n\t\t}\n\t\tawait this.#transport.close()\n\t\tthis.#emitter.emit('disconnect')\n\t}\n\n\tasync tools(): Promise<readonly ToolInterface[]> {\n\t\tconst result = await this.#request('tools/list')\n\t\t// The wire shape is `{ tools: MCPToolDescriptor[] }` — narrow it (§14): a\n\t\t// non-record / non-array `tools` yields no tools rather than throwing.\n\t\tif (!isRecord(result) || !isArray(result['tools'])) return []\n\t\tconst tools: ToolInterface[] = []\n\t\tfor (const descriptor of result['tools']) {\n\t\t\tif (!isRecord(descriptor) || !isString(descriptor['name'])) continue\n\t\t\tconst name = descriptor['name']\n\t\t\ttools.push(this.#tool(name, descriptor))\n\t\t}\n\t\treturn tools\n\t}\n\n\tasync call(name: string, args: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tconst result = await this.#request('tools/call', { name, arguments: args })\n\t\t// The inverse of the server's `buildToolResult`: concat the result's text blocks,\n\t\t// then either throw (a remote `isError`) or parse the JSON value.\n\t\tconst text = this.#text(result)\n\t\tif (isRecord(result) && result['isError'] === true) {\n\t\t\tthrow new Error(text.length > 0 ? text : `MCP tool '${name}' failed`)\n\t\t}\n\t\t// A success carries the value JSON-serialized into the text block(s); parse it,\n\t\t// falling back to the raw string when it is not JSON (the inverse of the server's\n\t\t// `JSON.stringify`, whose value-less result is an empty text block).\n\t\tif (text.length === 0) return undefined\n\t\ttry {\n\t\t\treturn JSON.parse(text)\n\t\t} catch {\n\t\t\treturn text\n\t\t}\n\t}\n\n\t// Issue a request and await its correlated response, bounded by the per-request\n\t// deadline. A monotonic numeric id keys the pending settlers; `AbortSignal.timeout`\n\t// (the taverna idiom — never a raw setTimeout) rejects the pending request if the\n\t// server never answers. The transport `send` is awaited so a write failure rejects\n\t// here rather than leaving a pending request to time out.\n\t#request(method: string, params?: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tthis.#nextId += 1\n\t\tconst id = this.#nextId\n\t\tconst request: JSONRPCRequest = {\n\t\t\tjsonrpc: '2.0',\n\t\t\tid,\n\t\t\tmethod,\n\t\t\t...(params === undefined ? {} : { params }),\n\t\t}\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst deadline = AbortSignal.timeout(this.#timeout)\n\t\t\tconst timeout = this.#timeoutRequest.bind(this, id, method)\n\t\t\tdeadline.addEventListener('abort', timeout, { once: true })\n\t\t\tthis.#pending.set(id, { resolve, reject, deadline, timeout })\n\t\t\tthis.#transport.send(request).catch((error: unknown) => {\n\t\t\t\tthis.#settle(id, error instanceof Error ? error : new Error(String(error)), true)\n\t\t\t})\n\t\t})\n\t}\n\n\t// Handle one inbound transport message: a response settles its pending request by\n\t// id (an error response rejects, a result resolves); anything else (a message with\n\t// no matching pending id) is a server-initiated notification, re-surfaced on the\n\t// `notification` event.\n\t#receive(message: JSONRPCMessage): void {\n\t\tif (isJSONRPCResponse(message) && isRequestId(message.id)) {\n\t\t\tif (this.#pending.has(message.id)) {\n\t\t\t\tif (message.error !== undefined) {\n\t\t\t\t\tthis.#settle(\n\t\t\t\t\t\tmessage.id,\n\t\t\t\t\t\tnew MCPError(message.error.message, message.error.code, message.error.data),\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tthis.#settle(message.id, message.result, false)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Not a correlated response — a server notification (or an unsolicited response).\n\t\tthis.#emitter.emit('notification', message)\n\t}\n\n\t// Wrap one remote tool descriptor as a local tool: map `inputSchema` → `parameters`\n\t// (the inverse of the server's rename, no `as`), carry `description` when present,\n\t// and bind `execute` to a remote `tools/call` via `call`.\n\t#tool(name: string, descriptor: Readonly<Record<string, unknown>>): ToolInterface {\n\t\tconst inputSchema = descriptor['inputSchema']\n\t\tconst description = descriptor['description']\n\t\tconst options: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tparameters?: Readonly<Record<string, unknown>>\n\t\t\texecute: (args: Readonly<Record<string, unknown>>) => Promise<unknown>\n\t\t} = {\n\t\t\tname,\n\t\t\texecute: this.call.bind(this, name),\n\t\t}\n\t\tif (isString(description)) options.description = description\n\t\tif (isRecord(inputSchema)) options.parameters = inputSchema\n\t\treturn new Tool(options)\n\t}\n\n\t// Concatenate an MCP tool-call result's text content blocks into one string — the\n\t// inverse of the server splitting a value into text block(s). Total (§14): a\n\t// non-record result, a non-array `content`, or a non-string `text` contributes\n\t// nothing rather than throwing.\n\t#text(result: unknown): string {\n\t\tif (!isRecord(result) || !isArray(result['content'])) return ''\n\t\tconst parts: string[] = []\n\t\tfor (const block of result['content']) {\n\t\t\tif (isRecord(block) && isString(block['text'])) parts.push(block['text'])\n\t\t}\n\t\treturn parts.join('\\n')\n\t}\n\n\t#timeoutRequest(id: string | number, method: string): void {\n\t\tthis.#settle(id, new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`), true)\n\t}\n\n\t#settle(id: string | number, value: unknown, failed: boolean): void {\n\t\tconst pending = this.#pending.get(id)\n\t\tif (pending === undefined) return\n\t\tthis.#pending.delete(id)\n\t\tpending.deadline.removeEventListener('abort', pending.timeout)\n\t\tif (failed) pending.reject(value)\n\t\telse pending.resolve(value)\n\t}\n}\n","import type {\n\tClientTransportEventMap,\n\tClientTransportInterface,\n\tJSONRPCMessage,\n\tMCPClientInterface,\n\tMCPClientOptions,\n\tMCPServerInterface,\n\tMCPServerOptions,\n\tMCPTransportInterface,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCPClient } from './MCPClient.js'\nimport { MCPServer } from './MCPServer.js'\n\n/**\n * Create a transport-agnostic Model Context Protocol server — exposes a live\n * {@link import('@orkestrel/agent').ToolManagerInterface} over JSON-RPC 2.0\n * (`initialize` / `ping` / `tools/list` / `tools/call`).\n *\n * @remarks\n * Pump raw message strings through `handle` (parse → dispatch → serialize) from a\n * transport, or call the typed `dispatch` directly with an already-parsed request.\n * The server is provider-agnostic — JSON-RPC plus the tool registry, with no HTTP\n * and no model. The {@link import('@orkestrel/agent').ToolManagerInterface} already\n * isolates a thrown tool into a result error (surfaced as an MCP `isError: true`\n * tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the\n * `request` event via `server.emitter.on('request', …)` for tracing.\n *\n * @param options - `name` / `version` (the server identity), `tools` (the live\n * registry to expose), an optional `description`, and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})\n * @returns A working {@link MCPServerInterface}\n *\n * @example\n * ```ts\n * import { createMCPServer, createTool, createToolManager } from '@src/core'\n *\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n *\n * const server = createMCPServer({ name: 'calculator', version: '1.0.0', tools })\n * server.emitter.on('request', (method, id) => log(method, id))\n *\n * // A transport pumps message strings through `handle`:\n * const reply = await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}')\n * // reply → '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"tools\":[{\"name\":\"add\",\"inputSchema\":{\"type\":\"object\"}}]}}'\n * ```\n */\nexport function createMCPServer(options: MCPServerOptions): MCPServerInterface {\n\treturn new MCPServer(options)\n}\n\n/**\n * Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE\n * MCP server over an injected {@link import('./types.js').ClientTransportInterface},\n * runs the `initialize` handshake, and exposes the server's tools as local\n * {@link import('@orkestrel/agent').ToolInterface}s an agent can run.\n *\n * @remarks\n * The egress mirror of {@link createMCPServer}: where the server exposes a local tool\n * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,\n * validates and exposes the negotiated protocol, `tools()` lists + wraps the remote\n * tools (each `execute` calls back over the wire),\n * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws\n * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}\n * isolates it). The transport is injected — a concrete one (the HTTP transport over\n * `fetch`) lives in `@src/server`; the client itself is provider-agnostic. Subscribe\n * to `connect` / `disconnect` / `notification` via `client.on(...)` (or\n * `client.emitter.on(...)`).\n *\n * @param options - `transport` (the carrier; REQUIRED), `name` / `version` (the client\n * identity), `timeout` (the per-request deadline), and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})\n * @returns A working {@link MCPClientInterface}\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createHTTPClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * agent.context.tools.add(await client.tools()) // give the agent the remote tools\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport function createMCPClient(options: MCPClientOptions): MCPClientInterface {\n\treturn new MCPClient(options)\n}\n\n/**\n * Adapt an {@link MCPTransportInterface} (the environment-agnostic duplex message\n * channel) into a {@link ClientTransportInterface} — the additive bridge that lets\n * `createMCPClient` run over the new port without any change to `MCPClient`'s\n * existing shape.\n *\n * @remarks\n * Hand the RESULT to `createMCPClient({ transport })`, then pass the SAME\n * `transport` to {@link import('./helpers.js').bindClient} to complete the inbound\n * wiring: `send` serializes each outbound {@link JSONRPCMessage} and writes it via\n * `transport.send`; `close` closes the underlying\n * `transport`; `start` is a no-op (the duplex channel is already open by the time\n * it is handed in — there is no separate connect step at this layer); `session` is\n * always `undefined` (session correlation is a higher-level concern the duplex port\n * does not carry). Inbound delivery (`emitter`'s `message` / `close` events) is\n * `bindClient`'s job, not this factory's — the returned object exposes a `message`-\n * capable emitter for `bindClient` to push onto.\n *\n * @param transport - The duplex channel to adapt\n * @returns A {@link ClientTransportInterface} `createMCPClient` can drive\n *\n * @example\n * ```ts\n * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })\n * const unbind = bindClient(client, transport)\n * await client.connect()\n * ```\n */\nexport function createDuplexClientTransport(\n\ttransport: MCPTransportInterface,\n): ClientTransportInterface {\n\tconst emitter = new Emitter<ClientTransportEventMap>()\n\treturn {\n\t\temitter,\n\t\tsession: undefined,\n\t\tasync start(): Promise<void> {\n\t\t\t// The duplex channel is already open by the time it is handed in — no separate\n\t\t\t// connect step at this layer.\n\t\t},\n\t\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\t\tawait transport.send(JSON.stringify(message))\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait transport.close()\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;AAMA,IAAa,uBAAuB;;;;;;;;;;;AAYpC,IAAa,8BAAiD,OAAO,OAAO,CAAC,YAAY,CAAC;;AAG1F,IAAa,sBAAsB;;AAGnC,IAAa,0BAA0B;;AAGvC,IAAa,2BAA2B;;AAGxC,IAAa,yBAAyB;;AAGtC,IAAa,uBAAuB;;AAOpC,IAAa,0BAA0B;;AAGvC,IAAa,6BAA6B;;;;;AAM1C,IAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;AChC3C,IAAa,WAAb,cAA8B,MAAM;CACnC,OAAyB;CACzB;CACA;;;;;;;;CASA,YAAY,SAAiB,MAAc,SAAmB;EAC7D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,UAAU;CAChB;AACD;;;;;;;;;;;;;AAcA,SAAgB,WAAW,OAAmC;CAC7D,IAAI;EAGH,OAAO,iBAAiB;CACzB,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;;;;;;;;;;;;;;;;;AC3BA,SAAgB,YAAY,OAAsD;CACjF,QAAA,GAAA,oBAAA,YAAA,CAAmB,KAAK,MAAA,GAAA,oBAAA,SAAA,CAAc,KAAK,MAAA,GAAA,oBAAA,SAAA,CAAc,KAAK;AAC/D;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,iBAAiB,OAAyC;CACzE,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,KAAK,GAClB,OAAO;CAER,IAAI,MAAM,eAAe,SAAS,EAAA,GAAA,oBAAA,SAAA,CAAU,MAAM,SAAS,GAC1D,OAAO;CAER,IAAI,CAAC,YAAY,MAAM,KAAK,GAC3B,OAAO;CAER,MAAM,SAAS,MAAM;CACrB,QAAA,GAAA,oBAAA,YAAA,CAAmB,MAAM,MAAA,GAAA,oBAAA,SAAA,CAAc,MAAM;AAC9C;;;;;;;;;;;;;AAcA,SAAgB,kBAAkB,OAA0C;CAC3E,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,KAAK,GAClB,OAAO;CAER,IAAI,MAAM,eAAe,OACxB,OAAO;CAER,MAAM,KAAK,MAAM;CACjB,IAAI,OAAO,QAAQ,EAAA,GAAA,oBAAA,SAAA,CAAU,EAAE,KAAK,EAAA,GAAA,oBAAA,SAAA,CAAU,EAAE,GAC/C,OAAO;CAER,MAAM,YAAY,OAAO,OAAO,OAAO,QAAQ;CAC/C,MAAM,QAAQ,MAAM;CACpB,MAAM,WAAW,EAAA,GAAA,oBAAA,YAAA,CAAa,KAAK;CAEnC,IAAI,cAAc,UACjB,OAAO;CAER,IAAI,UACH,QAAA,GAAA,oBAAA,SAAA,CAAgB,KAAK,MAAA,GAAA,oBAAA,SAAA,CAAc,MAAM,OAAO,MAAA,GAAA,oBAAA,SAAA,CAAc,MAAM,UAAU;CAE/E,OAAO;AACR;;;;;;;;;;;AAYA,SAAgB,iBAAiB,OAAyC;CACzE,OAAO,iBAAiB,KAAK,KAAK,kBAAkB,KAAK;AAC1D;;;;;;;;;;;;;;AAeA,SAAgB,oBAAoB,OAAyC;CAC5E,OAAO,iBAAiB,KAAK,KAAK,MAAM,WAAW;AACpD;;;;;;;;;;;;;;;;;;;;;;;AC5GA,SAAgB,oBAAoB,OAA4C;CAC/E,OAAO,iBAAiB,KAAK,IAAI,QAAQ,KAAA;AAC1C;;;;;;;;;;;ACDA,SAAgB,cAAc,IAA4B,QAAkC;CAC3F,OAAO;EAAE,SAAS;EAAO;EAAI;CAAO;AACrC;;;;;;;;;;;AAYA,SAAgB,aACf,IACA,MACA,SACA,MACkB;CAClB,OAAO;EACN,SAAS;EACT;EACA,OAAO,SAAS,KAAA,IAAY;GAAE;GAAM;EAAQ,IAAI;GAAE;GAAM;GAAS;EAAK;CACvE;AACD;;;;;;;;;;;;;;AAeA,SAAgB,qBAAqB,SAA6D;CACjG,OAAO,QAAQ,YAAY,CAAC,CAAC,KAAK,eAAe;EAChD,MAAM,aAIF;GACH,MAAM,WAAW;GACjB,aAAa,WAAW,cAAc,EAAE,MAAM,SAAS;EACxD;EACA,IAAI,WAAW,gBAAgB,KAAA,GAAW,WAAW,cAAc,WAAW;EAC9E,OAAO;CACR,CAAC;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,gBAAgB,QAAmC;CAClE,IAAI,OAAO,UAAU,KAAA,GACpB,OAAO;EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,OAAO;EAAM,CAAC;EAAG,SAAS;CAAK;CAKzE,OAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAQ,MADtB,OAAO,UAAU,KAAA,IAAY,KAAK,KAAK,UAAU,OAAO,KAAK;CAClC,CAAC,EAAE;AAC5C;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBACf,MACA,SACA,WACoC;CAKpC,OAAO;EACN,iBAJA,cAAc,KAAA,KAAa,4BAA4B,SAAS,SAAS,IACtE,YACA;EAGH,cAAc,EAAE,OAAO,CAAC,EAAE;EAC1B,YAAY;GAAE;GAAM;EAAQ;CAC7B;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,WACf,QACA,WACa;CACb,IAAI,SAAS;CACb,UAAU,OAAO,OAAO,YAAY;EACnC,IAAI,CAAC,QAAQ;EACb,IAAI;GACH,MAAM,WAAW,MAAM,OAAO,OAAO,OAAO;GAC5C,IAAI,aAAa,KAAA,GAAW,MAAM,UAAU,KAAK,QAAQ;EAC1D,SAAS,OAAO;GACf,IAAI;IACH,OAAO,QAAQ,KAAK,SAAS,KAAK;GACnC,QAAQ,CAER;EACD;CACD,CAAC;CACD,UAAU,aAAa;EACtB,SAAS;CACV,CAAC;CACD,aAAa;EACZ,SAAS;EACT,UAAU,aAAa,CAAC,CAAC;EACzB,UAAU,aAAa,CAAC,CAAC;CAC1B;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,WACf,QACA,WACa;CACb,IAAI,SAAS;CACb,UAAU,QAAQ,YAAY;EAC7B,IAAI,CAAC,QAAQ;EACb,IAAI;EACJ,IAAI;GACH,SAAS,KAAK,MAAM,OAAO;EAC5B,QAAQ;GACP;EACD;EACA,MAAM,UAAU,oBAAoB,MAAM;EAC1C,IAAI,YAAY,KAAA,GAAW;EAC3B,IAAI;GACH,OAAO,UAAU,QAAQ,KAAK,WAAW,OAAO;EACjD,SAAS,OAAO;GACf,IAAI;IACH,OAAO,UAAU,QAAQ,KAAK,SAAS,KAAK;GAC7C,QAAQ,CAER;EACD;CACD,CAAC;CACD,UAAU,aAAa;EACtB,IAAI,CAAC,QAAQ;EACb,SAAS;EACT,IAAI;GACH,OAAO,UAAU,QAAQ,KAAK,OAAO;EACtC,QAAQ,CAER;CACD,CAAC;CACD,aAAa;EACZ,SAAS;EACT,UAAU,aAAa,CAAC,CAAC;EACzB,UAAU,aAAa,CAAC,CAAC;CAC1B;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC7NA,IAAa,YAAb,MAAqD;CACpD;CACA;CACA;CACA;CAEA,YAAY,SAA2B;EACtC,KAAKA,WAAW,IAAI,mBAAA,QAA2B;GAC9C,GAAI,QAAQ,OAAO,KAAA,IAAY,EAAE,IAAI,QAAQ,GAAG,IAAI,CAAC;GACrD,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;EAC/D,CAAC;EACD,KAAKC,QAAQ,QAAQ;EACrB,KAAKC,WAAW,QAAQ;EACxB,KAAKC,SAAS,QAAQ;CACvB;CAEA,IAAI,UAA+C;EAClD,OAAO,KAAKH;CACb;CAEA,IAAI,OAAe;EAClB,OAAO,KAAKC;CACb;CAEA,IAAI,UAAkB;EACrB,OAAO,KAAKC;CACb;CAEA,MAAM,SAAS,SAA+D;EAC7E,MAAM,KAAK,QAAQ,MAAM;EACzB,KAAKF,SAAS,KAAK,WAAW,QAAQ,QAAQ,EAAE;EAMhD,IAAI,QAAQ,OAAO,KAAA,GAClB;EAED,QAAQ,QAAQ,QAAhB;GACC,KAAK,cAAc;IAClB,MAAM,YAAY,QAAQ,SAAS;IACnC,OAAO,cACN,IACA,iBAAiB,KAAKC,OAAO,KAAKC,WAAAA,GAAAA,oBAAAA,SAAAA,CAAmB,SAAS,IAAI,YAAY,KAAA,CAAS,CACxF;GACD;GACA,KAAK,QACJ,OAAO,cAAc,IAAI,CAAC,CAAC;GAC5B,KAAK,cACJ,OAAO,cAAc,IAAI,EAAE,OAAO,qBAAqB,KAAKC,MAAM,EAAE,CAAC;GACtE,KAAK,cACJ,OAAO,KAAKC,MAAM,SAAS,EAAE;GAC9B,SACC,OAAO,aAAa,IAAI,0BAA0B,qBAAqB,QAAQ,QAAQ;EACzF;CACD;CAEA,MAAM,OAAO,SAA8C;EAC1D,IAAI;EACJ,IAAI;GACH,SAAS,KAAK,MAAM,OAAO;EAC5B,QAAQ;GACP,OAAO,KAAK,UAAU,aAAa,MAAM,qBAAqB,aAAa,CAAC;EAC7E;EACA,MAAM,UAAU,oBAAoB,MAAM;EAE1C,IAAI,YAAY,KAAA,KAAa,EAAE,YAAY,UAC1C,OAAO,KAAK,UAAU,aAAa,MAAM,yBAAyB,iBAAiB,CAAC;EAErF,MAAM,WAAW,MAAM,KAAK,SAAS,OAAO;EAC5C,OAAO,aAAa,KAAA,IAAY,KAAA,IAAY,KAAK,UAAU,QAAQ;CACpE;CAKA,MAAMA,MAAM,SAAyB,IAAsD;EAC1F,MAAM,SAAS,QAAQ;EACvB,MAAM,OAAO,SAAS;EACtB,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,IAAI,GACjB,OAAO,aAAa,IAAI,wBAAwB,6CAA6C;EAE9F,MAAM,eAAe,SAAS;EAC9B,MAAM,QAAA,GAAA,oBAAA,SAAA,CAAgB,YAAY,IAAI,eAAe,CAAC;EACtD,MAAM,SAAS,QAAQ,OAAO,KAAA,IAAY,OAAO,WAAW,IAAI,OAAO,QAAQ,EAAE;EAEjF,OAAO,cAAc,IAAI,gBAAgB,MADpB,KAAKD,OAAO,QAAQ;GAAE,IAAI;GAAQ;GAAM,WAAW;EAAK,CAAC,CAC/B,CAAC;CACjD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxFA,IAAa,YAAb,MAAqD;CACpD;CACA;CACA;CACA;CACA;CAIA,2BAAoB,IAAI,IAQtB;CACF,UAAU;CACV,aAAa;CACb,YAAgC,KAAA;CAEhC,YAAY,SAA2B;EACtC,KAAKE,WAAW,IAAI,mBAAA,QAA2B;GAC9C,GAAI,QAAQ,OAAO,KAAA,IAAY,EAAE,IAAI,QAAQ,GAAG,IAAI,CAAC;GACrD,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;EAC/D,CAAC;EACD,KAAKC,aAAa,QAAQ;EAC1B,KAAKC,QAAQ,QAAQ,QAAA;EACrB,KAAKC,WAAW,QAAQ,WAAA;EACxB,KAAKC,WAAW,QAAQ,WAAA;EAGxB,KAAKH,WAAW,QAAQ,GAAG,YAAY,YAAY,KAAKK,SAAS,OAAO,CAAC;CAC1E;CAEA,IAAI,UAA+C;EAClD,OAAO,KAAKN;CACb;CAEA,IAAI,YAAqB;EACxB,OAAO,KAAKO;CACb;CAEA,IAAI,WAA+B;EAClC,OAAO,KAAKC;CACb;CAEA,IAAI,YAAsC;EACzC,OAAO,KAAKP;CACb;CAEA,GACC,OACA,SACO;EACP,KAAKD,SAAS,GAAG,OAAO,OAAO;CAChC;CAEA,MAAM,UAAyB;EAC9B,IAAI,KAAKO,YAAY;EACrB,MAAM,KAAKN,WAAW,MAAM;EAI5B,MAAM,SAAS,MAAM,KAAKQ,SAAS,cAAc;GAChD,iBAAiB;GACjB,cAAc,CAAC;GACf,YAAY;IAAE,MAAM,KAAKP;IAAO,SAAS,KAAKC;GAAS;EACxD,CAAC;EACD,MAAM,YAAA,GAAA,oBAAA,SAAA,CAAoB,MAAM,IAAI,OAAO,qBAAqB,KAAA;EAChE,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,QAAQ,KAAK,CAAC,4BAA4B,SAAS,QAAQ,GAAG;GAC3E,MAAM,KAAKF,WAAW,MAAM;GAC5B,KAAA,GAAA,oBAAA,SAAA,CAAa,QAAQ,GACpB,MAAM,IAAI,MAAM,uDAAuD,SAAS,EAAE;GAEnF,MAAM,IAAI,MAAM,mDAAmD;EACpE;EACA,KAAKO,YAAY;EACjB,KAAKD,aAAa;EAClB,MAAM,KAAKN,WAAW,KAAK;GAAE,SAAS;GAAO,QAAQ;EAA4B,CAAC;EAClF,KAAKD,SAAS,KAAK,SAAS;CAC7B;CAEA,MAAM,aAA4B;EACjC,IAAI,CAAC,KAAKO,YAAY;EACtB,KAAKA,aAAa;EAClB,KAAKC,YAAY,KAAA;EAGjB,KAAK,MAAM,MAAM,KAAKH,SAAS,KAAK,GACnC,KAAKK,QAAQ,oBAAI,IAAI,MAAM,yBAAyB,GAAG,IAAI;EAE5D,MAAM,KAAKT,WAAW,MAAM;EAC5B,KAAKD,SAAS,KAAK,YAAY;CAChC;CAEA,MAAM,QAA2C;EAChD,MAAM,SAAS,MAAM,KAAKS,SAAS,YAAY;EAG/C,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,MAAM,KAAK,EAAA,GAAA,oBAAA,QAAA,CAAS,OAAO,QAAQ,GAAG,OAAO,CAAC;EAC5D,MAAM,QAAyB,CAAC;EAChC,KAAK,MAAM,cAAc,OAAO,UAAU;GACzC,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,UAAU,KAAK,EAAA,GAAA,oBAAA,SAAA,CAAU,WAAW,OAAO,GAAG;GAC5D,MAAM,OAAO,WAAW;GACxB,MAAM,KAAK,KAAKE,MAAM,MAAM,UAAU,CAAC;EACxC;EACA,OAAO;CACR;CAEA,MAAM,KAAK,MAAc,MAA2D;EACnF,MAAM,SAAS,MAAM,KAAKF,SAAS,cAAc;GAAE;GAAM,WAAW;EAAK,CAAC;EAG1E,MAAM,OAAO,KAAKG,MAAM,MAAM;EAC9B,KAAA,GAAA,oBAAA,SAAA,CAAa,MAAM,KAAK,OAAO,eAAe,MAC7C,MAAM,IAAI,MAAM,KAAK,SAAS,IAAI,OAAO,aAAa,KAAK,SAAS;EAKrE,IAAI,KAAK,WAAW,GAAG,OAAO,KAAA;EAC9B,IAAI;GACH,OAAO,KAAK,MAAM,IAAI;EACvB,QAAQ;GACP,OAAO;EACR;CACD;CAOA,SAAS,QAAgB,QAA8D;EACtF,KAAKC,WAAW;EAChB,MAAM,KAAK,KAAKA;EAChB,MAAM,UAA0B;GAC/B,SAAS;GACT;GACA;GACA,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;EAC1C;EACA,OAAO,IAAI,SAAkB,SAAS,WAAW;GAChD,MAAM,WAAW,YAAY,QAAQ,KAAKT,QAAQ;GAClD,MAAM,UAAU,KAAKU,gBAAgB,KAAK,MAAM,IAAI,MAAM;GAC1D,SAAS,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;GAC1D,KAAKT,SAAS,IAAI,IAAI;IAAE;IAAS;IAAQ;IAAU;GAAQ,CAAC;GAC5D,KAAKJ,WAAW,KAAK,OAAO,CAAC,CAAC,OAAO,UAAmB;IACvD,KAAKS,QAAQ,IAAI,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,GAAG,IAAI;GACjF,CAAC;EACF,CAAC;CACF;CAMA,SAAS,SAA+B;EACvC,IAAI,kBAAkB,OAAO,KAAK,YAAY,QAAQ,EAAE;OACnD,KAAKL,SAAS,IAAI,QAAQ,EAAE,GAAG;IAClC,IAAI,QAAQ,UAAU,KAAA,GACrB,KAAKK,QACJ,QAAQ,IACR,IAAI,SAAS,QAAQ,MAAM,SAAS,QAAQ,MAAM,MAAM,QAAQ,MAAM,IAAI,GAC1E,IACD;SAEA,KAAKA,QAAQ,QAAQ,IAAI,QAAQ,QAAQ,KAAK;IAE/C;GACD;;EAGD,KAAKV,SAAS,KAAK,gBAAgB,OAAO;CAC3C;CAKA,MAAM,MAAc,YAA8D;EACjF,MAAM,cAAc,WAAW;EAC/B,MAAM,cAAc,WAAW;EAC/B,MAAM,UAKF;GACH;GACA,SAAS,KAAK,KAAK,KAAK,MAAM,IAAI;EACnC;EACA,KAAA,GAAA,oBAAA,SAAA,CAAa,WAAW,GAAG,QAAQ,cAAc;EACjD,KAAA,GAAA,oBAAA,SAAA,CAAa,WAAW,GAAG,QAAQ,aAAa;EAChD,OAAO,IAAI,iBAAA,KAAK,OAAO;CACxB;CAMA,MAAM,QAAyB;EAC9B,IAAI,EAAA,GAAA,oBAAA,SAAA,CAAU,MAAM,KAAK,EAAA,GAAA,oBAAA,QAAA,CAAS,OAAO,UAAU,GAAG,OAAO;EAC7D,MAAM,QAAkB,CAAC;EACzB,KAAK,MAAM,SAAS,OAAO,YAC1B,KAAA,GAAA,oBAAA,SAAA,CAAa,KAAK,MAAA,GAAA,oBAAA,SAAA,CAAc,MAAM,OAAO,GAAG,MAAM,KAAK,MAAM,OAAO;EAEzE,OAAO,MAAM,KAAK,IAAI;CACvB;CAEA,gBAAgB,IAAqB,QAAsB;EAC1D,KAAKU,QAAQ,oBAAI,IAAI,MAAM,gBAAgB,OAAO,oBAAoB,KAAKN,SAAS,GAAG,GAAG,IAAI;CAC/F;CAEA,QAAQ,IAAqB,OAAgB,QAAuB;EACnE,MAAM,UAAU,KAAKC,SAAS,IAAI,EAAE;EACpC,IAAI,YAAY,KAAA,GAAW;EAC3B,KAAKA,SAAS,OAAO,EAAE;EACvB,QAAQ,SAAS,oBAAoB,SAAS,QAAQ,OAAO;EAC7D,IAAI,QAAQ,QAAQ,OAAO,KAAK;OAC3B,QAAQ,QAAQ,KAAK;CAC3B;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC3OA,SAAgB,gBAAgB,SAA+C;CAC9E,OAAO,IAAI,UAAU,OAAO;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,gBAAgB,SAA+C;CAC9E,OAAO,IAAI,UAAU,OAAO;AAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,4BACf,WAC2B;CAE3B,OAAO;EACN,SAAA,IAFmB,mBAAA,QAEnB;EACA,SAAS,KAAA;EACT,MAAM,QAAuB,CAG7B;EACA,MAAM,KAAK,SAAwC;GAClD,MAAM,UAAU,KAAK,KAAK,UAAU,OAAO,CAAC;EAC7C;EACA,MAAM,QAAuB;GAC5B,MAAM,UAAU,MAAM;EACvB;CACD;AACD"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#emitter","#name","#version","#tools","#call","#emitter","#transport","#name","#version","#timeout","#pending","#receive","#connected","#protocol","#request","#settle","#tool","#text","#nextId","#timeoutRequest"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/validators.ts","../../../src/core/parsers.ts","../../../src/core/helpers.ts","../../../src/core/MCPServer.ts","../../../src/core/MCPClient.ts","../../../src/core/factories.ts"],"sourcesContent":["// MCP protocol revisions + the reserved JSON-RPC 2.0 error codes. The negotiated\n// protocol version is the current rev unless the client requests a supported\n// one (see `initializeResult` in ./helpers.js). Transport-level header names\n// (session / version headers) belong to the HTTP transport sub-chunk, NOT here.\n\n/** The MCP protocol revision this server implements (the default negotiated version). */\nexport const MCP_PROTOCOL_VERSION = '2025-06-18'\n\n/**\n * The MCP protocol revisions this server can negotiate.\n *\n * @remarks\n * `initialize` echoes the client's requested `protocolVersion` when it appears in\n * this list, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is\n * an immutable contract. The package does not advertise `2025-03-26` because that\n * revision mandates JSON-RPC batching, while this package accepts only individual\n * JSON-RPC messages.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze(['2025-06-18'])\n\n/** JSON-RPC 2.0 reserved error: invalid JSON was received (the message did not parse). */\nexport const JSONRPC_PARSE_ERROR = -32700\n\n/** JSON-RPC 2.0 reserved error: the payload was not a valid Request object. */\nexport const JSONRPC_INVALID_REQUEST = -32600\n\n/** JSON-RPC 2.0 reserved error: the requested method does not exist. */\nexport const JSONRPC_METHOD_NOT_FOUND = -32601\n\n/** JSON-RPC 2.0 reserved error: the method's parameters were invalid. */\nexport const JSONRPC_INVALID_PARAMS = -32602\n\n/** JSON-RPC 2.0 implementation-defined server error (the `-32000` to `-32099` range). */\nexport const JSONRPC_SERVER_ERROR = -32000\n\n// MCP CLIENT defaults — the identity an `MCPClient` reports in the `initialize`\n// handshake (`clientInfo`) and the per-request deadline, when the caller supplies\n// none. The egress mirror of the server's protocol-version constants above.\n\n/** The default client name reported in the MCP `initialize` handshake (`clientInfo.name`). */\nexport const DEFAULT_MCP_CLIENT_NAME = 'taverna'\n\n/** The default client version reported in the MCP `initialize` handshake (`clientInfo.version`). */\nexport const DEFAULT_MCP_CLIENT_VERSION = '1.0.0'\n\n/**\n * The default per-request deadline (ms) an `MCPClient` applies when `options.timeout`\n * is unset — a request the remote server does not answer within it rejects.\n */\nexport const DEFAULT_MCP_REQUEST_TIMEOUT = 30_000\n","/**\n * A remote Model Context Protocol JSON-RPC error, preserving its machine-readable\n * numeric code and optional structured context.\n *\n * @remarks\n * {@link MCPClient} throws this error only for a remote JSON-RPC `error` response.\n * Local lifecycle and transport conditions such as disconnects and request timeouts\n * remain plain `Error`s. `context` carries the response's optional `error.data`\n * unchanged and is `undefined` when the peer omitted it.\n *\n * @example\n * ```ts\n * const error = new MCPError('Method not found', -32601, { method: 'missing' })\n * error.code // -32601\n * error.context // { method: 'missing' }\n * ```\n */\nexport class MCPError extends Error {\n\toverride readonly name = 'MCPError'\n\treadonly code: number\n\treadonly context: unknown\n\n\t/**\n\t * Create a remote MCP protocol error.\n\t *\n\t * @param message - The human-readable JSON-RPC error message\n\t * @param code - The machine-readable numeric JSON-RPC error code\n\t * @param context - The optional JSON-RPC `error.data` payload\n\t */\n\tconstructor(message: string, code: number, context?: unknown) {\n\t\tsuper(message)\n\t\tthis.code = code\n\t\tthis.context = context\n\t}\n}\n\n/**\n * Determine whether an unknown value is an {@link MCPError}.\n *\n * @param value - The unknown value to inspect\n * @returns `true` only when the value is an `MCPError`\n *\n * @example\n * ```ts\n * isMCPError(new MCPError('Method not found', -32601)) // true\n * isMCPError(new Error('Method not found')) // false\n * ```\n */\nexport function isMCPError(value: unknown): value is MCPError {\n\ttry {\n\t\t// A revoked Proxy or a hostile prototype can make `instanceof` throw — this guard\n\t\t// must stay total, so the check is wrapped rather than left to escape.\n\t\treturn value instanceof MCPError\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type { JSONRPCMessage, JSONRPCRequest, JSONRPCResponse } from './types.js'\nimport { isNumber, isRecord, isString, isUndefined } from '@orkestrel/contract'\n\n// AGENTS §14: every guard here is a TOTAL function over the already-`JSON.parse`d\n// value — adversarial input returns `false`, never throws. The raw-string\n// `JSON.parse` (which CAN throw) happens in `MCPServer.handle` inside a try/catch;\n// these guards only ever see a parsed `unknown`. Each is a flat structural test on\n// `isRecord` + field checks (no user callbacks), so totality is immediate.\n\n/**\n * Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,\n * or absent.\n *\n * @remarks\n * A request id is a string, a number, or `undefined` (its ABSENCE marks a\n * NOTIFICATION). `null` is NOT a valid request id — it is valid only on a RESPONSE.\n * Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a string, a number, or `undefined`\n *\n * @example\n * ```ts\n * isRequestId(1) // true\n * isRequestId('abc') // true\n * isRequestId(undefined) // true — a notification\n * isRequestId(null) // false — valid only on a response\n * ```\n */\nexport function isRequestId(value: unknown): value is string | number | undefined {\n\treturn isUndefined(value) || isString(value) || isNumber(value)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCRequest}.\n *\n * @remarks\n * A request is a record with `jsonrpc === '2.0'` and a string `method`. `id`, when\n * present, must be a string or number; its ABSENCE is valid — that marks a\n * NOTIFICATION (a fire-and-forget request that yields no response). `params`, when\n * present, must be a record. Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request\n *\n * @example\n * ```ts\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // true\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'notifications/initialized' }) // true — a notification\n * isJSONRPCRequest({ jsonrpc: '1.0', method: 'ping' }) // false\n * ```\n */\nexport function isJSONRPCRequest(value: unknown): value is JSONRPCRequest {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0' || !isString(value['method'])) {\n\t\treturn false\n\t}\n\tif (!isRequestId(value['id'])) {\n\t\treturn false\n\t}\n\tconst params = value['params']\n\treturn isUndefined(params) || isRecord(params)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCResponse}.\n *\n * @remarks\n * A response is a record with `jsonrpc === '2.0'`, an `id` that is a string,\n * number, or `null`, and EXACTLY ONE of a `result` (any value, including\n * `undefined`'s absence) or an `error` (a record with a numeric `code` and string\n * `message`). Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC response\n */\nexport function isJSONRPCResponse(value: unknown): value is JSONRPCResponse {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0') {\n\t\treturn false\n\t}\n\tconst id = value['id']\n\tif (id !== null && !isString(id) && !isNumber(id)) {\n\t\treturn false\n\t}\n\tconst hasResult = Object.hasOwn(value, 'result')\n\tconst error = value['error']\n\tconst hasError = !isUndefined(error)\n\t// Exactly one of result / error — never both, never neither.\n\tif (hasResult === hasError) {\n\t\treturn false\n\t}\n\tif (hasError) {\n\t\treturn isRecord(error) && isNumber(error['code']) && isString(error['message'])\n\t}\n\treturn true\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCMessage} — a request or a\n * response.\n *\n * @remarks\n * The union of {@link isJSONRPCRequest} and {@link isJSONRPCResponse}. Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request or response\n */\nexport function isJSONRPCMessage(value: unknown): value is JSONRPCMessage {\n\treturn isJSONRPCRequest(value) || isJSONRPCResponse(value)\n}\n\n/**\n * Determine whether a parsed value is an MCP `initialize` request — a\n * {@link JSONRPCRequest} whose `method` is `'initialize'`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid `initialize` request\n *\n * @example\n * ```ts\n * isInitializeRequest({ jsonrpc: '2.0', method: 'initialize', id: 1 }) // true\n * isInitializeRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // false\n * ```\n */\nexport function isInitializeRequest(value: unknown): value is JSONRPCRequest {\n\treturn isJSONRPCRequest(value) && value.method === 'initialize'\n}\n","import type { JSONRPCMessage } from './types.js'\nimport { isJSONRPCMessage } from './validators.js'\n\n/**\n * Narrow an already-parsed value to a {@link JSONRPCMessage}, or `undefined` when\n * it is not one.\n *\n * @remarks\n * Total (§14) — a non-message returns `undefined`, never throws. The input must\n * ALREADY be `JSON.parse`d: the raw-string parse (which can throw on malformed\n * JSON) happens in `MCPServer.handle` inside a try/catch that maps a parse failure\n * to a `-32700` response. Sound with {@link isJSONRPCMessage}: a guard-valid input\n * is returned unchanged, and every non-`undefined` output satisfies the guard.\n *\n * @param value - The already-parsed value to narrow\n * @returns The value as a {@link JSONRPCMessage}, or `undefined`\n *\n * @example\n * ```ts\n * parseJSONRPCMessage({ jsonrpc: '2.0', method: 'ping', id: 1 }) // the request\n * parseJSONRPCMessage({ method: 'ping' }) // undefined — missing jsonrpc\n * ```\n */\nexport function parseJSONRPCMessage(value: unknown): JSONRPCMessage | undefined {\n\treturn isJSONRPCMessage(value) ? value : undefined\n}\n","import type { ToolManagerInterface, ToolResult } from '@orkestrel/tool'\nimport type {\n\tJSONRPCResponse,\n\tMCPClientInterface,\n\tMCPServerInterface,\n\tMCPToolDescriptor,\n\tMCPToolResult,\n\tMCPTransportInterface,\n} from './types.js'\nimport { MCP_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from './constants.js'\nimport { parseJSONRPCMessage } from './parsers.js'\n\n// Pure dispatch builders (AGENTS §5: the dispatch branches stay exported helpers,\n// not hidden privates). Each turns a piece of MCP state into the JSON-RPC `result`\n// payload (or a response envelope) the server returns — independently testable.\n\n/**\n * Build a JSON-RPC success {@link JSONRPCResponse} — the `id` echoed, the method's\n * value as `result`.\n *\n * @param id - The request's id (`null` only for a parse / invalid-request error)\n * @param result - The method's return value\n * @returns The success response envelope\n */\nexport function jsonRPCResult(id: string | number | null, result: unknown): JSONRPCResponse {\n\treturn { jsonrpc: '2.0', id, result }\n}\n\n/**\n * Build a JSON-RPC error {@link JSONRPCResponse} — the `id` echoed, the failure as\n * an `error` object.\n *\n * @param id - The request's id (`null` for a parse / invalid-request error)\n * @param code - One of the reserved JSON-RPC codes (see `./constants.js`)\n * @param message - A short human description of the failure\n * @param data - An OPTIONAL machine-readable payload (omitted from the envelope when absent)\n * @returns The error response envelope\n */\nexport function jsonRPCError(\n\tid: string | number | null,\n\tcode: number,\n\tmessage: string,\n\tdata?: unknown,\n): JSONRPCResponse {\n\treturn {\n\t\tjsonrpc: '2.0',\n\t\tid,\n\t\terror: data === undefined ? { code, message } : { code, message, data },\n\t}\n}\n\n/**\n * Map a {@link ToolManagerInterface}'s definitions to MCP `tools/list` descriptors\n * — renaming `parameters` to the wire's `inputSchema`.\n *\n * @remarks\n * Each {@link import('@orkestrel/tool').ToolDefinition} carries through its\n * `name` and (when present) `description`; its open JSON-Schema `parameters`\n * becomes `inputSchema`, defaulting to an empty object schema (`{ type: 'object' }`)\n * when a tool declares none (MCP requires an `inputSchema`).\n *\n * @param manager - The tool registry to describe\n * @returns One {@link MCPToolDescriptor} per registered tool, in registry order\n */\nexport function buildToolDescriptors(manager: ToolManagerInterface): readonly MCPToolDescriptor[] {\n\treturn manager.definitions().map((definition) => {\n\t\tconst descriptor: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tinputSchema: Readonly<Record<string, unknown>>\n\t\t} = {\n\t\t\tname: definition.name,\n\t\t\tinputSchema: definition.parameters ?? { type: 'object' },\n\t\t}\n\t\tif (definition.description !== undefined) descriptor.description = definition.description\n\t\treturn descriptor\n\t})\n}\n\n/**\n * Map an executed tool's {@link ToolResult} to an MCP {@link MCPToolResult} — the\n * value (or error) as a `text` content block.\n *\n * @remarks\n * The {@link ToolManagerInterface} already isolates a thrown tool into a\n * `success: false` result (so the server adds NO try/catch around `execute`):\n * that branch builds an `isError: true` result carrying `result.error`, so the\n * model sees the failure as a tool result it can react to rather than a protocol\n * error; the `success: true` branch serializes `result.value` (via\n * `JSON.stringify`) into one `text` block.\n *\n * @param result - The tool's execution outcome\n * @returns The MCP tool-call result\n */\nexport function buildToolResult(result: ToolResult): MCPToolResult {\n\tif (!result.success) {\n\t\treturn { content: [{ type: 'text', text: result.error }], isError: true }\n\t}\n\t// A content block must carry a string `text`; `JSON.stringify(undefined)` is the value\n\t// `undefined` (which serializes away), so a value-less result becomes an empty text block.\n\tconst text = result.value === undefined ? '' : JSON.stringify(result.value)\n\treturn { content: [{ type: 'text', text }] }\n}\n\n/**\n * Build the MCP `initialize` result — the negotiated protocol version, the\n * advertised capabilities, and the server identity.\n *\n * @remarks\n * Version negotiation echoes the client's `requested` version when it is one of the\n * {@link SUPPORTED_PROTOCOL_VERSIONS}, else falls back to {@link MCP_PROTOCOL_VERSION}.\n * `capabilities.tools` is an empty object — this server advertises the tools\n * capability with no sub-options (no list-changed notification yet).\n *\n * @param name - The server name (echoed in `serverInfo`)\n * @param version - The server version (echoed in `serverInfo`)\n * @param requested - The client's requested protocol version (negotiated when supported)\n * @returns The `initialize` result payload\n */\nexport function initializeResult(\n\tname: string,\n\tversion: string,\n\trequested?: string,\n): Readonly<Record<string, unknown>> {\n\tconst protocolVersion =\n\t\trequested !== undefined && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)\n\t\t\t? requested\n\t\t\t: MCP_PROTOCOL_VERSION\n\treturn {\n\t\tprotocolVersion,\n\t\tcapabilities: { tools: {} },\n\t\tserverInfo: { name, version },\n\t}\n}\n\n// The environment-agnostic PORT binders — the keystone that lets an\n// {@link MCPServerInterface} / {@link MCPClientInterface} run over ANY\n// {@link MCPTransportInterface} (a Node stdio pair, a browser MessagePort, a Web\n// Worker `self`) with no per-environment dispatch/correlation wiring duplicated at\n// each face. Both are TOTAL: a `send` throw or rejection is caught and never\n// escapes as an unhandled rejection.\n\n/**\n * Pipe an {@link MCPTransportInterface} into an {@link MCPServerInterface} — every\n * inbound message runs through `server.handle`, and a defined reply is written back\n * via `transport.send`.\n *\n * @remarks\n * `server.handle` already turns a malformed message into a serialized `-32700` /\n * `-32600` reply and a notification into `undefined` (no reply), so this binder adds\n * no parsing of its own. A `transport.send` throw or rejection is caught and routed\n * to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);\n * a listener on that event that itself throws is swallowed (the end of the line —\n * the caller's own bug, never this binder's). The returned unbind DETACHES this\n * binder (further inbound messages and the transport's `closed` signal are ignored)\n * WITHOUT closing the transport — closing is the caller's decision.\n *\n * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind\n * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent\n * `bindServer` call on the SAME transport is never double-dispatched by a stale\n * subscription left behind — an unbind→rebind cycle yields exactly one reply per\n * request.\n *\n * @param server - The transport-agnostic server to dispatch inbound messages over\n * @param transport - The duplex channel to pipe the server over\n * @returns Detach this binder from the transport (does not close it)\n *\n * @example\n * ```ts\n * const unbind = bindServer(server, transport)\n * // ... later, detach without closing:\n * unbind()\n * ```\n */\nexport function bindServer(\n\tserver: MCPServerInterface,\n\ttransport: MCPTransportInterface,\n): () => void {\n\tlet active = true\n\ttransport.listen(async (message) => {\n\t\tif (!active) return\n\t\ttry {\n\t\t\tconst response = await server.handle(message)\n\t\t\tif (response !== undefined) await transport.send(response)\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tserver.emitter.emit('error', error)\n\t\t\t} catch {\n\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t}\n\t\t}\n\t})\n\ttransport.closed(() => {\n\t\tactive = false\n\t})\n\treturn () => {\n\t\tactive = false\n\t\ttransport.listen(() => {})\n\t\ttransport.closed(() => {})\n\t}\n}\n\n/**\n * Pipe an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every\n * inbound message is decoded and delivered onto the client's OWN transport\n * (`client.transport.emitter`'s `message` / `close` events), resolving/rejecting the\n * client's correlated pending requests exactly as a direct reply would.\n *\n * @remarks\n * The client's outbound writes flow through `client.transport.send` — its existing,\n * unmodified request/response correlation — so `client` must have been constructed\n * with a {@link import('./types.js').ClientTransportInterface} that itself carries\n * the SAME `transport` (see {@link import('./factories.js').createDuplexClientTransport},\n * the additive factory that adapts an {@link MCPTransportInterface} into that shape);\n * this binder then completes the inbound half by decoding each message and pushing it\n * onto `client.transport.emitter` (an {@link import('@orkestrel/emitter').EmitterInterface}\n * exposes `emit`, so no client modification is needed). A malformed / non-JSON-RPC\n * inbound message is DROPPED (§14, total — never throws); a delivery fault is routed to\n * `client.transport.emitter`'s `error` event (never rethrown). The returned unbind\n * DETACHES this binder (further inbound messages and the transport's `closed` signal are\n * ignored) WITHOUT closing the transport.\n *\n * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind\n * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent\n * `bindClient` call on the SAME transport is never double-dispatched by a stale\n * subscription left behind — an unbind→rebind cycle delivers exactly one `message`\n * emit per inbound reply.\n *\n * @param client - The transport-agnostic client whose transport to deliver messages onto\n * @param transport - The duplex channel to pipe the client over\n * @returns Detach this binder from the transport (does not close it)\n *\n * @example\n * ```ts\n * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })\n * const unbind = bindClient(client, transport)\n * await client.connect()\n * // ... later, detach without closing:\n * unbind()\n * ```\n */\nexport function bindClient(\n\tclient: MCPClientInterface,\n\ttransport: MCPTransportInterface,\n): () => void {\n\tlet active = true\n\ttransport.listen((message) => {\n\t\tif (!active) return\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(message)\n\t\t} catch {\n\t\t\treturn\n\t\t}\n\t\tconst decoded = parseJSONRPCMessage(parsed)\n\t\tif (decoded === undefined) return\n\t\ttry {\n\t\t\tclient.transport.emitter.emit('message', decoded)\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tclient.transport.emitter.emit('error', error)\n\t\t\t} catch {\n\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t}\n\t\t}\n\t})\n\ttransport.closed(() => {\n\t\tif (!active) return\n\t\tactive = false\n\t\ttry {\n\t\t\tclient.transport.emitter.emit('close')\n\t\t} catch {\n\t\t\t// A throwing `close` listener is the caller's own bug — the end of the line.\n\t\t}\n\t})\n\treturn () => {\n\t\tactive = false\n\t\ttransport.listen(() => {})\n\t\ttransport.closed(() => {})\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolManagerInterface } from '@orkestrel/tool'\nimport type {\n\tJSONRPCRequest,\n\tJSONRPCResponse,\n\tMCPServerEventMap,\n\tMCPServerInterface,\n\tMCPServerOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport {\n\tJSONRPC_INVALID_PARAMS,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_METHOD_NOT_FOUND,\n\tJSONRPC_PARSE_ERROR,\n} from './constants.js'\nimport {\n\tbuildToolDescriptors,\n\tbuildToolResult,\n\tinitializeResult,\n\tjsonRPCError,\n\tjsonRPCResult,\n} from './helpers.js'\nimport { parseJSONRPCMessage } from './parsers.js'\n\n/**\n * A transport-agnostic Model Context Protocol server — dispatches JSON-RPC 2.0\n * requests over a live {@link ToolManagerInterface}, with NO transport coupling.\n *\n * @remarks\n * - **Two entry points.** `dispatch(request)` runs an already-parsed request and\n * resolves a {@link JSONRPCResponse} — or `undefined` for a NOTIFICATION (a\n * request with no `id`). `handle(message)` is the string boundary: it\n * `JSON.parse`s the raw message (a failure → a `-32700` response), narrows it to\n * a request (a non-request → a `-32600` response), dispatches, and serializes the\n * response back to a string (`undefined` for a notification).\n * - **The method switch.** `initialize` negotiates the protocol version + advertises\n * the tools capability; `notifications/initialized` is a notification (no\n * response); `ping` returns `{}`; `tools/list` lists the registry's tools (its\n * `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the\n * {@link ToolManagerInterface} isolates a tool throw into a `success: false`\n * result, which maps to an `isError: true` tool result — so the server adds NO\n * try/catch). An unknown method → `-32601`; a `tools/call` with a missing /\n * non-string `name` → `-32602`.\n * - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,\n * no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `request` at the top of every\n * dispatch; the emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a listener throw can never escape the dispatch.\n *\n * @example\n * ```ts\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n * const server = new MCPServer({ name: 'demo', version: '1.0.0', tools })\n * await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}') // '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}'\n * ```\n */\nexport class MCPServer implements MCPServerInterface {\n\treadonly #emitter: Emitter<MCPServerEventMap>\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #tools: ToolManagerInterface\n\n\tconstructor(options: MCPServerOptions) {\n\t\tthis.#emitter = new Emitter<MCPServerEventMap>({\n\t\t\t...(options.on !== undefined ? { on: options.on } : {}),\n\t\t\t...(options.error !== undefined ? { error: options.error } : {}),\n\t\t})\n\t\tthis.#name = options.name\n\t\tthis.#version = options.version\n\t\tthis.#tools = options.tools\n\t}\n\n\tget emitter(): EmitterInterface<MCPServerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget name(): string {\n\t\treturn this.#name\n\t}\n\n\tget version(): string {\n\t\treturn this.#version\n\t}\n\n\tasync dispatch(request: JSONRPCRequest): Promise<JSONRPCResponse | undefined> {\n\t\tconst id = request.id ?? null\n\t\tthis.#emitter.emit('request', request.method, id)\n\t\t// JSON-RPC: a request with NO `id` is a NOTIFICATION — it is handled (the\n\t\t// `request` event already fired) but NEVER produces a response, whatever its\n\t\t// method (`notifications/initialized`, a fire-and-forget `ping`, an unknown\n\t\t// method — all silent). So short-circuit here, and the switch below only ever\n\t\t// runs for an id-bearing request that expects a reply.\n\t\tif (request.id === undefined) {\n\t\t\treturn undefined\n\t\t}\n\t\tswitch (request.method) {\n\t\t\tcase 'initialize': {\n\t\t\t\tconst requested = request.params?.['protocolVersion']\n\t\t\t\treturn jsonRPCResult(\n\t\t\t\t\tid,\n\t\t\t\t\tinitializeResult(this.#name, this.#version, isString(requested) ? requested : undefined),\n\t\t\t\t)\n\t\t\t}\n\t\t\tcase 'ping':\n\t\t\t\treturn jsonRPCResult(id, {})\n\t\t\tcase 'tools/list':\n\t\t\t\treturn jsonRPCResult(id, { tools: buildToolDescriptors(this.#tools) })\n\t\t\tcase 'tools/call':\n\t\t\t\treturn this.#call(request, id)\n\t\t\tdefault:\n\t\t\t\treturn jsonRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`)\n\t\t}\n\t}\n\n\tasync handle(message: string): Promise<string | undefined> {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(message)\n\t\t} catch {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'))\n\t\t}\n\t\tconst decoded = parseJSONRPCMessage(parsed)\n\t\t// Only a REQUEST is dispatchable — a response (or any non-message) is invalid input.\n\t\tif (decoded === undefined || !('method' in decoded)) {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'))\n\t\t}\n\t\tconst response = await this.dispatch(decoded)\n\t\treturn response === undefined ? undefined : JSON.stringify(response)\n\t}\n\n\t// Run a `tools/call`: narrow `params.name` (string) + `params.arguments` (record,\n\t// default `{}`) with no `as`, execute the tool (the manager isolates a throw into\n\t// `success: false`), and map the result to an MCP tool-call result.\n\tasync #call(request: JSONRPCRequest, id: string | number | null): Promise<JSONRPCResponse> {\n\t\tconst params = request.params\n\t\tconst name = params?.['name']\n\t\tif (!isString(name)) {\n\t\t\treturn jsonRPCError(id, JSONRPC_INVALID_PARAMS, 'Invalid params: a string `name` is required')\n\t\t}\n\t\tconst rawArguments = params?.['arguments']\n\t\tconst args = isRecord(rawArguments) ? rawArguments : {}\n\t\tconst callId = request.id === undefined ? crypto.randomUUID() : String(request.id)\n\t\tconst result = await this.#tools.execute({ id: callId, name, arguments: args })\n\t\treturn jsonRPCResult(id, buildToolResult(result))\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolInterface } from '@orkestrel/tool'\nimport type {\n\tClientTransportInterface,\n\tJSONRPCMessage,\n\tJSONRPCRequest,\n\tMCPClientEventMap,\n\tMCPClientInterface,\n\tMCPClientOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Tool } from '@orkestrel/tool'\nimport { isArray, isRecord, isString } from '@orkestrel/contract'\nimport {\n\tDEFAULT_MCP_CLIENT_NAME,\n\tDEFAULT_MCP_CLIENT_VERSION,\n\tDEFAULT_MCP_REQUEST_TIMEOUT,\n\tMCP_PROTOCOL_VERSION,\n\tSUPPORTED_PROTOCOL_VERSIONS,\n} from './constants.js'\nimport { MCPError } from './errors.js'\nimport { isJSONRPCResponse, isRequestId } from './validators.js'\n\n/**\n * A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server\n * over an injected {@link ClientTransportInterface}, runs the `initialize` handshake,\n * and exposes the server's tools as local {@link ToolInterface}s an agent can run.\n *\n * @remarks\n * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;\n * this client ISSUES them over a transport. `connect` runs `initialize`, validates and\n * exposes the negotiated `protocol`, then sends `notifications/initialized`; `tools()`\n * lists the remote tools and wraps each as a\n * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a\n * remote `tools/call` and returns the tool's value (a remote `isError: true` throws\n * locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}\n * isolates it into a `success: false` result just like a local throw).\n * - **Request↔response correlation.** Each request is tagged with a monotonic numeric\n * `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects\n * the matching {@link #pending} entry by `id`. A message that is NOT a response to a\n * pending request is a server NOTIFICATION — re-surfaced on the `notification` event.\n * - **Per-request deadline.** `#request` races `AbortSignal.timeout(this.#timeout)` (the\n * taverna idiom — never a raw `setTimeout`): a server that never replies REJECTS the\n * pending request once the deadline fires, never hanging.\n * - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);\n * the concrete transport is injected. Wire fields are narrowed via the contracts\n * guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `connect` / `disconnect` /\n * `notification` / `error`; the emitter isolates a listener throw and routes it to its\n * `error` handler (the `error` option), so a listener throw can never escape.\n *\n * @example\n * ```ts\n * const client = new MCPClient({ transport, name: 'agent', version: '1.0.0' })\n * await client.connect()\n * const tools = await client.tools()\n * agent.context.tools.add(tools) // the remote tools are now the agent's\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport class MCPClient implements MCPClientInterface {\n\treadonly #emitter: Emitter<MCPClientEventMap>\n\treadonly #transport: ClientTransportInterface\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #timeout: number\n\t// The in-flight requests, keyed by JSON-RPC id, each holding its promise settlers —\n\t// resolved on the matching response, rejected on an error response, the deadline, or\n\t// `disconnect`. Genuinely private glue (§5): the settler shape lives inline here.\n\treadonly #pending = new Map<\n\t\tstring | number,\n\t\t{\n\t\t\treadonly resolve: (value: unknown) => void\n\t\t\treadonly reject: (reason?: unknown) => void\n\t\t\treadonly deadline: AbortSignal\n\t\t\treadonly timeout: () => void\n\t\t}\n\t>()\n\t#nextId = 0\n\t#connected = false\n\t#protocol: string | undefined = undefined\n\n\tconstructor(options: MCPClientOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientEventMap>({\n\t\t\t...(options.on !== undefined ? { on: options.on } : {}),\n\t\t\t...(options.error !== undefined ? { error: options.error } : {}),\n\t\t})\n\t\tthis.#transport = options.transport\n\t\tthis.#name = options.name ?? DEFAULT_MCP_CLIENT_NAME\n\t\tthis.#version = options.version ?? DEFAULT_MCP_CLIENT_VERSION\n\t\tthis.#timeout = options.timeout ?? DEFAULT_MCP_REQUEST_TIMEOUT\n\t\t// One message subscription for the client's whole life: a response settles its\n\t\t// pending request by id; anything else is a server notification.\n\t\tthis.#transport.emitter.on('message', (message) => this.#receive(message))\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget connected(): boolean {\n\t\treturn this.#connected\n\t}\n\n\tget protocol(): string | undefined {\n\t\treturn this.#protocol\n\t}\n\n\tget transport(): ClientTransportInterface {\n\t\treturn this.#transport\n\t}\n\n\ton<K extends keyof MCPClientEventMap>(\n\t\tevent: K,\n\t\thandler: (...args: MCPClientEventMap[K]) => void,\n\t): void {\n\t\tthis.#emitter.on(event, handler)\n\t}\n\n\tasync connect(): Promise<void> {\n\t\tif (this.#connected) return\n\t\tawait this.#transport.start()\n\t\t// The MCP handshake: negotiate the protocol version + advertise (empty) client\n\t\t// capabilities + identify ourselves, then mark connected and fire the no-args\n\t\t// `notifications/initialized` (a notification — no id, no response).\n\t\tconst result = await this.#request('initialize', {\n\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\tcapabilities: {},\n\t\t\tclientInfo: { name: this.#name, version: this.#version },\n\t\t})\n\t\tconst protocol = isRecord(result) ? result['protocolVersion'] : undefined\n\t\tif (!isString(protocol) || !SUPPORTED_PROTOCOL_VERSIONS.includes(protocol)) {\n\t\t\tawait this.#transport.close()\n\t\t\tif (isString(protocol)) {\n\t\t\t\tthrow new Error(`MCP server negotiated unsupported protocol version '${protocol}'`)\n\t\t\t}\n\t\t\tthrow new Error('MCP server returned a non-string protocol version')\n\t\t}\n\t\tthis.#protocol = protocol\n\t\tthis.#connected = true\n\t\tawait this.#transport.send({ jsonrpc: '2.0', method: 'notifications/initialized' })\n\t\tthis.#emitter.emit('connect')\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (!this.#connected) return\n\t\tthis.#connected = false\n\t\tthis.#protocol = undefined\n\t\t// Reject every still-pending request so no caller hangs past a disconnect, then\n\t\t// clear the map and tear the transport down.\n\t\tfor (const id of this.#pending.keys()) {\n\t\t\tthis.#settle(id, new Error('MCP client disconnected'), true)\n\t\t}\n\t\tawait this.#transport.close()\n\t\tthis.#emitter.emit('disconnect')\n\t}\n\n\tasync tools(): Promise<readonly ToolInterface[]> {\n\t\tconst result = await this.#request('tools/list')\n\t\t// The wire shape is `{ tools: MCPToolDescriptor[] }` — narrow it (§14): a\n\t\t// non-record / non-array `tools` yields no tools rather than throwing.\n\t\tif (!isRecord(result) || !isArray(result['tools'])) return []\n\t\tconst tools: ToolInterface[] = []\n\t\tfor (const descriptor of result['tools']) {\n\t\t\tif (!isRecord(descriptor) || !isString(descriptor['name'])) continue\n\t\t\tconst name = descriptor['name']\n\t\t\ttools.push(this.#tool(name, descriptor))\n\t\t}\n\t\treturn tools\n\t}\n\n\tasync call(name: string, args: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tconst result = await this.#request('tools/call', { name, arguments: args })\n\t\t// The inverse of the server's `buildToolResult`: concat the result's text blocks,\n\t\t// then either throw (a remote `isError`) or parse the JSON value.\n\t\tconst text = this.#text(result)\n\t\tif (isRecord(result) && result['isError'] === true) {\n\t\t\tthrow new Error(text.length > 0 ? text : `MCP tool '${name}' failed`)\n\t\t}\n\t\t// A success carries the value JSON-serialized into the text block(s); parse it,\n\t\t// falling back to the raw string when it is not JSON (the inverse of the server's\n\t\t// `JSON.stringify`, whose value-less result is an empty text block).\n\t\tif (text.length === 0) return undefined\n\t\ttry {\n\t\t\treturn JSON.parse(text)\n\t\t} catch {\n\t\t\treturn text\n\t\t}\n\t}\n\n\t// Issue a request and await its correlated response, bounded by the per-request\n\t// deadline. A monotonic numeric id keys the pending settlers; `AbortSignal.timeout`\n\t// (the taverna idiom — never a raw setTimeout) rejects the pending request if the\n\t// server never answers. The transport `send` is awaited so a write failure rejects\n\t// here rather than leaving a pending request to time out.\n\t#request(method: string, params?: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tthis.#nextId += 1\n\t\tconst id = this.#nextId\n\t\tconst request: JSONRPCRequest = {\n\t\t\tjsonrpc: '2.0',\n\t\t\tid,\n\t\t\tmethod,\n\t\t\t...(params === undefined ? {} : { params }),\n\t\t}\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst deadline = AbortSignal.timeout(this.#timeout)\n\t\t\tconst timeout = this.#timeoutRequest.bind(this, id, method)\n\t\t\tdeadline.addEventListener('abort', timeout, { once: true })\n\t\t\tthis.#pending.set(id, { resolve, reject, deadline, timeout })\n\t\t\tthis.#transport.send(request).catch((error: unknown) => {\n\t\t\t\tthis.#settle(id, error instanceof Error ? error : new Error(String(error)), true)\n\t\t\t})\n\t\t})\n\t}\n\n\t// Handle one inbound transport message: a response settles its pending request by\n\t// id (an error response rejects, a result resolves); anything else (a message with\n\t// no matching pending id) is a server-initiated notification, re-surfaced on the\n\t// `notification` event.\n\t#receive(message: JSONRPCMessage): void {\n\t\tif (isJSONRPCResponse(message) && isRequestId(message.id)) {\n\t\t\tif (this.#pending.has(message.id)) {\n\t\t\t\tif (message.error !== undefined) {\n\t\t\t\t\tthis.#settle(\n\t\t\t\t\t\tmessage.id,\n\t\t\t\t\t\tnew MCPError(message.error.message, message.error.code, message.error.data),\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tthis.#settle(message.id, message.result, false)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Not a correlated response — a server notification (or an unsolicited response).\n\t\tthis.#emitter.emit('notification', message)\n\t}\n\n\t// Wrap one remote tool descriptor as a local tool: map `inputSchema` → `parameters`\n\t// (the inverse of the server's rename, no `as`), carry `description` when present,\n\t// and bind `execute` to a remote `tools/call` via `call`.\n\t#tool(name: string, descriptor: Readonly<Record<string, unknown>>): ToolInterface {\n\t\tconst inputSchema = descriptor['inputSchema']\n\t\tconst description = descriptor['description']\n\t\tconst options: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tparameters?: Readonly<Record<string, unknown>>\n\t\t\texecute: (args: Readonly<Record<string, unknown>>) => Promise<unknown>\n\t\t} = {\n\t\t\tname,\n\t\t\texecute: this.call.bind(this, name),\n\t\t}\n\t\tif (isString(description)) options.description = description\n\t\tif (isRecord(inputSchema)) options.parameters = inputSchema\n\t\treturn new Tool(options)\n\t}\n\n\t// Concatenate an MCP tool-call result's text content blocks into one string — the\n\t// inverse of the server splitting a value into text block(s). Total (§14): a\n\t// non-record result, a non-array `content`, or a non-string `text` contributes\n\t// nothing rather than throwing.\n\t#text(result: unknown): string {\n\t\tif (!isRecord(result) || !isArray(result['content'])) return ''\n\t\tconst parts: string[] = []\n\t\tfor (const block of result['content']) {\n\t\t\tif (isRecord(block) && isString(block['text'])) parts.push(block['text'])\n\t\t}\n\t\treturn parts.join('\\n')\n\t}\n\n\t#timeoutRequest(id: string | number, method: string): void {\n\t\tthis.#settle(id, new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`), true)\n\t}\n\n\t#settle(id: string | number, value: unknown, failed: boolean): void {\n\t\tconst pending = this.#pending.get(id)\n\t\tif (pending === undefined) return\n\t\tthis.#pending.delete(id)\n\t\tpending.deadline.removeEventListener('abort', pending.timeout)\n\t\tif (failed) pending.reject(value)\n\t\telse pending.resolve(value)\n\t}\n}\n","import type {\n\tClientTransportEventMap,\n\tClientTransportInterface,\n\tJSONRPCMessage,\n\tMCPClientInterface,\n\tMCPClientOptions,\n\tMCPServerInterface,\n\tMCPServerOptions,\n\tMCPTransportInterface,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCPClient } from './MCPClient.js'\nimport { MCPServer } from './MCPServer.js'\n\n/**\n * Create a transport-agnostic Model Context Protocol server — exposes a live\n * {@link import('@orkestrel/tool').ToolManagerInterface} over JSON-RPC 2.0\n * (`initialize` / `ping` / `tools/list` / `tools/call`).\n *\n * @remarks\n * Pump raw message strings through `handle` (parse → dispatch → serialize) from a\n * transport, or call the typed `dispatch` directly with an already-parsed request.\n * The server is provider-agnostic — JSON-RPC plus the tool registry, with no HTTP\n * and no model. The {@link import('@orkestrel/tool').ToolManagerInterface} already\n * isolates a thrown tool into a `success: false` result (surfaced as an MCP\n * `isError: true` tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the\n * `request` event via `server.emitter.on('request', …)` for tracing.\n *\n * @param options - `name` / `version` (the server identity), `tools` (the live\n * registry to expose), an optional `description`, and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})\n * @returns A working {@link MCPServerInterface}\n *\n * @example\n * ```ts\n * import { createMCPServer, createTool, createToolManager } from '@src/core'\n *\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n *\n * const server = createMCPServer({ name: 'calculator', version: '1.0.0', tools })\n * server.emitter.on('request', (method, id) => log(method, id))\n *\n * // A transport pumps message strings through `handle`:\n * const reply = await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}')\n * // reply → '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"tools\":[{\"name\":\"add\",\"inputSchema\":{\"type\":\"object\"}}]}}'\n * ```\n */\nexport function createMCPServer(options: MCPServerOptions): MCPServerInterface {\n\treturn new MCPServer(options)\n}\n\n/**\n * Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE\n * MCP server over an injected {@link import('./types.js').ClientTransportInterface},\n * runs the `initialize` handshake, and exposes the server's tools as local\n * {@link import('@orkestrel/tool').ToolInterface}s an agent can run.\n *\n * @remarks\n * The egress mirror of {@link createMCPServer}: where the server exposes a local tool\n * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,\n * validates and exposes the negotiated protocol, `tools()` lists + wraps the remote\n * tools (each `execute` calls back over the wire),\n * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws\n * locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}\n * isolates it). The transport is injected — a concrete one (the HTTP transport over\n * `fetch`) lives in `@src/server`; the client itself is provider-agnostic. Subscribe\n * to `connect` / `disconnect` / `notification` via `client.on(...)` (or\n * `client.emitter.on(...)`).\n *\n * @param options - `transport` (the carrier; REQUIRED), `name` / `version` (the client\n * identity), `timeout` (the per-request deadline), and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})\n * @returns A working {@link MCPClientInterface}\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createHTTPClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * agent.context.tools.add(await client.tools()) // give the agent the remote tools\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport function createMCPClient(options: MCPClientOptions): MCPClientInterface {\n\treturn new MCPClient(options)\n}\n\n/**\n * Adapt an {@link MCPTransportInterface} (the environment-agnostic duplex message\n * channel) into a {@link ClientTransportInterface} — the additive bridge that lets\n * `createMCPClient` run over the new port without any change to `MCPClient`'s\n * existing shape.\n *\n * @remarks\n * Hand the RESULT to `createMCPClient({ transport })`, then pass the SAME\n * `transport` to {@link import('./helpers.js').bindClient} to complete the inbound\n * wiring: `send` serializes each outbound {@link JSONRPCMessage} and writes it via\n * `transport.send`; `close` closes the underlying\n * `transport`; `start` is a no-op (the duplex channel is already open by the time\n * it is handed in — there is no separate connect step at this layer); `session` is\n * always `undefined` (session correlation is a higher-level concern the duplex port\n * does not carry). Inbound delivery (`emitter`'s `message` / `close` events) is\n * `bindClient`'s job, not this factory's — the returned object exposes a `message`-\n * capable emitter for `bindClient` to push onto.\n *\n * @param transport - The duplex channel to adapt\n * @returns A {@link ClientTransportInterface} `createMCPClient` can drive\n *\n * @example\n * ```ts\n * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })\n * const unbind = bindClient(client, transport)\n * await client.connect()\n * ```\n */\nexport function createDuplexClientTransport(\n\ttransport: MCPTransportInterface,\n): ClientTransportInterface {\n\tconst emitter = new Emitter<ClientTransportEventMap>()\n\treturn {\n\t\temitter,\n\t\tsession: undefined,\n\t\tasync start(): Promise<void> {\n\t\t\t// The duplex channel is already open by the time it is handed in — no separate\n\t\t\t// connect step at this layer.\n\t\t},\n\t\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\t\tawait transport.send(JSON.stringify(message))\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait transport.close()\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
2
|
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
3
|
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
-
import { ToolInterface } from '@orkestrel/
|
|
5
|
-
import { ToolManagerInterface } from '@orkestrel/
|
|
6
|
-
import { ToolResult } from '@orkestrel/
|
|
4
|
+
import { ToolInterface } from '@orkestrel/tool';
|
|
5
|
+
import { ToolManagerInterface } from '@orkestrel/tool';
|
|
6
|
+
import { ToolResult } from '@orkestrel/tool';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Pipe an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every
|
|
@@ -85,7 +85,7 @@ export declare function bindServer(server: MCPServerInterface, transport: MCPTra
|
|
|
85
85
|
* — renaming `parameters` to the wire's `inputSchema`.
|
|
86
86
|
*
|
|
87
87
|
* @remarks
|
|
88
|
-
* Each {@link import('@orkestrel/
|
|
88
|
+
* Each {@link import('@orkestrel/tool').ToolDefinition} carries through its
|
|
89
89
|
* `name` and (when present) `description`; its open JSON-Schema `parameters`
|
|
90
90
|
* becomes `inputSchema`, defaulting to an empty object schema (`{ type: 'object' }`)
|
|
91
91
|
* when a tool declares none (MCP requires an `inputSchema`).
|
|
@@ -100,12 +100,12 @@ export declare function buildToolDescriptors(manager: ToolManagerInterface): rea
|
|
|
100
100
|
* value (or error) as a `text` content block.
|
|
101
101
|
*
|
|
102
102
|
* @remarks
|
|
103
|
-
* The {@link ToolManagerInterface} already isolates a thrown tool into
|
|
104
|
-
* `
|
|
105
|
-
*
|
|
103
|
+
* The {@link ToolManagerInterface} already isolates a thrown tool into a
|
|
104
|
+
* `success: false` result (so the server adds NO try/catch around `execute`):
|
|
105
|
+
* that branch builds an `isError: true` result carrying `result.error`, so the
|
|
106
106
|
* model sees the failure as a tool result it can react to rather than a protocol
|
|
107
|
-
* error;
|
|
108
|
-
* `text` block.
|
|
107
|
+
* error; the `success: true` branch serializes `result.value` (via
|
|
108
|
+
* `JSON.stringify`) into one `text` block.
|
|
109
109
|
*
|
|
110
110
|
* @param result - The tool's execution outcome
|
|
111
111
|
* @returns The MCP tool-call result
|
|
@@ -219,7 +219,7 @@ export declare function createDuplexClientTransport(transport: MCPTransportInter
|
|
|
219
219
|
* Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE
|
|
220
220
|
* MCP server over an injected {@link import('./types.js').ClientTransportInterface},
|
|
221
221
|
* runs the `initialize` handshake, and exposes the server's tools as local
|
|
222
|
-
* {@link import('@orkestrel/
|
|
222
|
+
* {@link import('@orkestrel/tool').ToolInterface}s an agent can run.
|
|
223
223
|
*
|
|
224
224
|
* @remarks
|
|
225
225
|
* The egress mirror of {@link createMCPServer}: where the server exposes a local tool
|
|
@@ -227,7 +227,7 @@ export declare function createDuplexClientTransport(transport: MCPTransportInter
|
|
|
227
227
|
* validates and exposes the negotiated protocol, `tools()` lists + wraps the remote
|
|
228
228
|
* tools (each `execute` calls back over the wire),
|
|
229
229
|
* and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws
|
|
230
|
-
* locally, so an agent's {@link import('@orkestrel/
|
|
230
|
+
* locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}
|
|
231
231
|
* isolates it). The transport is injected — a concrete one (the HTTP transport over
|
|
232
232
|
* `fetch`) lives in `@src/server`; the client itself is provider-agnostic. Subscribe
|
|
233
233
|
* to `connect` / `disconnect` / `notification` via `client.on(...)` (or
|
|
@@ -255,16 +255,16 @@ export declare function createMCPClient(options: MCPClientOptions): MCPClientInt
|
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
257
|
* Create a transport-agnostic Model Context Protocol server — exposes a live
|
|
258
|
-
* {@link import('@orkestrel/
|
|
258
|
+
* {@link import('@orkestrel/tool').ToolManagerInterface} over JSON-RPC 2.0
|
|
259
259
|
* (`initialize` / `ping` / `tools/list` / `tools/call`).
|
|
260
260
|
*
|
|
261
261
|
* @remarks
|
|
262
262
|
* Pump raw message strings through `handle` (parse → dispatch → serialize) from a
|
|
263
263
|
* transport, or call the typed `dispatch` directly with an already-parsed request.
|
|
264
264
|
* The server is provider-agnostic — JSON-RPC plus the tool registry, with no HTTP
|
|
265
|
-
* and no model. The {@link import('@orkestrel/
|
|
266
|
-
* isolates a thrown tool into a result
|
|
267
|
-
* tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the
|
|
265
|
+
* and no model. The {@link import('@orkestrel/tool').ToolManagerInterface} already
|
|
266
|
+
* isolates a thrown tool into a `success: false` result (surfaced as an MCP
|
|
267
|
+
* `isError: true` tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the
|
|
268
268
|
* `request` event via `server.emitter.on('request', …)` for tracing.
|
|
269
269
|
*
|
|
270
270
|
* @param options - `name` / `version` (the server identity), `tools` (the live
|
|
@@ -522,8 +522,8 @@ export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
|
522
522
|
* lists the remote tools and wraps each as a
|
|
523
523
|
* local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
|
|
524
524
|
* remote `tools/call` and returns the tool's value (a remote `isError: true` throws
|
|
525
|
-
* locally, so an agent's {@link import('@orkestrel/
|
|
526
|
-
* isolates it into a
|
|
525
|
+
* locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}
|
|
526
|
+
* isolates it into a `success: false` result just like a local throw).
|
|
527
527
|
* - **Request↔response correlation.** Each request is tagged with a monotonic numeric
|
|
528
528
|
* `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects
|
|
529
529
|
* the matching {@link #pending} entry by `id`. A message that is NOT a response to a
|
|
@@ -602,8 +602,8 @@ export declare type MCPClientEventMap = {
|
|
|
602
602
|
* the remote tools and wraps each as a local {@link ToolInterface} whose `execute`
|
|
603
603
|
* calls back through `call`; `call(name, args)` runs a remote `tools/call` and
|
|
604
604
|
* returns the tool's value (a remote tool FAILURE — `isError: true` — throws locally,
|
|
605
|
-
* so the agent's {@link ToolManagerInterface} isolates it into a
|
|
606
|
-
* like a local throw).
|
|
605
|
+
* so the agent's {@link ToolManagerInterface} isolates it into a `success: false`
|
|
606
|
+
* result just like a local throw).
|
|
607
607
|
* - **Request↔response correlation.** Every request is tagged with a monotonic numeric
|
|
608
608
|
* `id`; the client subscribes to the transport's `message` event and resolves /
|
|
609
609
|
* rejects the matching pending request by that `id`. A message that is NOT a response
|
|
@@ -683,8 +683,8 @@ export declare interface MCPClientInterface {
|
|
|
683
683
|
* The inverse of the server's `buildToolResult`: a SUCCESS parses the concatenated
|
|
684
684
|
* `text` as JSON (falling back to the raw string when it is not JSON); a remote tool
|
|
685
685
|
* FAILURE (`isError: true`) THROWS an `Error` carrying the error text — so an agent's
|
|
686
|
-
* {@link ToolManagerInterface} isolates the remote failure into a
|
|
687
|
-
* exactly as it would a local tool throw.
|
|
686
|
+
* {@link ToolManagerInterface} isolates the remote failure into a `success: false`
|
|
687
|
+
* result exactly as it would a local tool throw.
|
|
688
688
|
*
|
|
689
689
|
* @param name - The remote tool's name
|
|
690
690
|
* @param args - The arguments record forwarded as the call's `arguments`
|
|
@@ -775,10 +775,10 @@ export declare class MCPError extends Error {
|
|
|
775
775
|
* the tools capability; `notifications/initialized` is a notification (no
|
|
776
776
|
* response); `ping` returns `{}`; `tools/list` lists the registry's tools (its
|
|
777
777
|
* `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the
|
|
778
|
-
* {@link ToolManagerInterface} isolates a tool throw into
|
|
779
|
-
* maps to an `isError: true` tool result — so the server adds NO
|
|
780
|
-
* unknown method → `-32601`; a `tools/call` with a missing /
|
|
781
|
-
* `-32602`.
|
|
778
|
+
* {@link ToolManagerInterface} isolates a tool throw into a `success: false`
|
|
779
|
+
* result, which maps to an `isError: true` tool result — so the server adds NO
|
|
780
|
+
* try/catch). An unknown method → `-32601`; a `tools/call` with a missing /
|
|
781
|
+
* non-string `name` → `-32602`.
|
|
782
782
|
* - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,
|
|
783
783
|
* no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).
|
|
784
784
|
* - **Observable (§13).** The owned `emitter` fires `request` at the top of every
|
|
@@ -889,7 +889,7 @@ export declare interface MCPServerInterface {
|
|
|
889
889
|
* (`serverInfo`). `tools` is the live registry the server dispatches `tools/list`
|
|
890
890
|
* / `tools/call` over — its `definitions()` advertise the tools and its
|
|
891
891
|
* `execute()` runs a call (the manager already isolates a tool throw into a
|
|
892
|
-
*
|
|
892
|
+
* `success: false` result, so the server adds none). `description` is a human label for
|
|
893
893
|
* the server (reserved for a future `instructions` capability — unused by the
|
|
894
894
|
* current dispatch). `on` is the §8 reserved key: initial listeners for the
|
|
895
895
|
* server's {@link MCPServerEventMap}, wired at construction.
|
|
@@ -927,9 +927,9 @@ export declare interface MCPToolDescriptor {
|
|
|
927
927
|
*
|
|
928
928
|
* @remarks
|
|
929
929
|
* A success carries the tool's value serialized into one `text` content block; a
|
|
930
|
-
* tool FAILURE (the `
|
|
931
|
-
* text in `content` AND sets `isError: true`, so the model sees the
|
|
932
|
-
* tool result it can react to rather than a protocol error.
|
|
930
|
+
* tool FAILURE (the `success: false` branch the registry isolated) carries its
|
|
931
|
+
* `error` text in `content` AND sets `isError: true`, so the model sees the
|
|
932
|
+
* failure as a tool result it can react to rather than a protocol error.
|
|
933
933
|
*/
|
|
934
934
|
export declare interface MCPToolResult {
|
|
935
935
|
readonly content: readonly MCPContent[];
|