@warlock.js/ai-tools 5.30.0 → 5.31.0
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/CHANGELOG.md +37 -1
- package/README.md +25 -13
- package/cjs/index.cjs +843 -101
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/mcp-serve.type.d.mts +257 -0
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
- package/esm/contracts/mcp.type.d.mts +16 -59
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/errors.d.mts +36 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +20 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/index.d.mts +6 -4
- package/esm/index.mjs +3 -2
- package/esm/mcp/client.mjs +35 -5
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +12 -5
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +1 -0
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/protocol.d.mts +16 -0
- package/esm/mcp/protocol.d.mts.map +1 -0
- package/esm/mcp/protocol.mjs +23 -0
- package/esm/mcp/protocol.mjs.map +1 -0
- package/esm/mcp/serve-http.mjs +443 -0
- package/esm/mcp/serve-http.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +45 -26
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +325 -92
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/register.d.mts +12 -1
- package/esm/register.d.mts.map +1 -1
- package/esm/register.mjs +3 -2
- package/esm/register.mjs.map +1 -1
- package/esm/schema.mjs.map +1 -1
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/calculator.mjs.map +1 -1
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/schema.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/schema.mjs.map +1 -1
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +194 -57
- package/llms.txt +3 -3
- package/package.json +2 -6
- package/skills/connect-mcp-server/SKILL.md +28 -6
- package/skills/expose-as-mcp-server/SKILL.md +117 -36
- package/skills/index.md +5 -2
- package/skills/use-web-and-http-tools/SKILL.md +49 -15
package/cjs/index.cjs
CHANGED
|
@@ -2,6 +2,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
let _warlock_js_ai = require("@warlock.js/ai");
|
|
3
3
|
let node_child_process = require("node:child_process");
|
|
4
4
|
let node_readline = require("node:readline");
|
|
5
|
+
let node_crypto = require("node:crypto");
|
|
6
|
+
let node_http = require("node:http");
|
|
7
|
+
let node_net = require("node:net");
|
|
5
8
|
|
|
6
9
|
//#region ../../packages/ai-tools/src/errors.ts
|
|
7
10
|
/**
|
|
@@ -80,6 +83,25 @@ var McpTransportError = class extends _warlock_js_ai.AIError {
|
|
|
80
83
|
}
|
|
81
84
|
};
|
|
82
85
|
/**
|
|
86
|
+
* `ai.mcp.serve` could not build or start the server. Thrown by `serve()`
|
|
87
|
+
* itself for an invalid source or a name collision, and by `start()` for
|
|
88
|
+
* transport configuration and bind failures — never for a single bad request, which gets an HTTP / JSON-RPC
|
|
89
|
+
* error response instead. Extends {@link AIError} (code `TOOL_EXEC_FAILED`);
|
|
90
|
+
* branch on `error.type`.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* if (error instanceof McpServeError && error.type === "collision") {
|
|
94
|
+
* // two tools share a name — rename one or use a grouped source + prefixToolNames
|
|
95
|
+
* }
|
|
96
|
+
*/
|
|
97
|
+
var McpServeError = class extends _warlock_js_ai.AIError {
|
|
98
|
+
constructor(message, options) {
|
|
99
|
+
super("TOOL_EXEC_FAILED", message, options);
|
|
100
|
+
this.name = "McpServeError";
|
|
101
|
+
this.type = options.type;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
83
105
|
* A web tool failed — a missing optional peer, an absent API key, a host
|
|
84
106
|
* rejected by the `allowHosts` guardrail, an unparseable URL, or a failed
|
|
85
107
|
* network call.
|
|
@@ -355,7 +377,7 @@ async function readCappedBody(response, maxBytes) {
|
|
|
355
377
|
* `application/json` and the `+json` structured-suffix convention
|
|
356
378
|
* (e.g. `application/vnd.api+json`), case-insensitively.
|
|
357
379
|
*/
|
|
358
|
-
function isJsonContentType(contentType) {
|
|
380
|
+
function isJsonContentType$1(contentType) {
|
|
359
381
|
if (!contentType) return false;
|
|
360
382
|
const value = contentType.toLowerCase();
|
|
361
383
|
return value.includes("application/json") || value.includes("+json");
|
|
@@ -471,7 +493,7 @@ function httpRequestTool(options = {}) {
|
|
|
471
493
|
});
|
|
472
494
|
const { text, truncated } = await readCappedBody(response, maxBytes);
|
|
473
495
|
let parsedBody = text;
|
|
474
|
-
if (!truncated && isJsonContentType(responseHeaders["content-type"]) && text.length > 0) try {
|
|
496
|
+
if (!truncated && isJsonContentType$1(responseHeaders["content-type"]) && text.length > 0) try {
|
|
475
497
|
parsedBody = JSON.parse(text);
|
|
476
498
|
} catch {
|
|
477
499
|
parsedBody = text;
|
|
@@ -1046,7 +1068,7 @@ function withIdAllocator(client) {
|
|
|
1046
1068
|
/** Default per-call timeout for `tools/call`. */
|
|
1047
1069
|
const DEFAULT_CALL_TIMEOUT_MS = 3e4;
|
|
1048
1070
|
/** The MCP protocol version this client advertises in `initialize`. */
|
|
1049
|
-
const PROTOCOL_VERSION
|
|
1071
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
1050
1072
|
/**
|
|
1051
1073
|
* The internal {@link McpClient} implementation. Owns one JSON-RPC client
|
|
1052
1074
|
* over a transport, runs the `initialize` handshake on first use, lists
|
|
@@ -1059,8 +1081,24 @@ const PROTOCOL_VERSION$1 = "2025-06-18";
|
|
|
1059
1081
|
var McpClientImpl = class {
|
|
1060
1082
|
constructor(source, options = {}) {
|
|
1061
1083
|
this.initialized = false;
|
|
1084
|
+
this.onAbort = () => {
|
|
1085
|
+
this.close().catch(() => void 0);
|
|
1086
|
+
};
|
|
1062
1087
|
this.rpc = createJsonRpcClient(source);
|
|
1063
1088
|
this.options = options;
|
|
1089
|
+
const signal = options.signal;
|
|
1090
|
+
if (signal?.aborted) this.onAbort();
|
|
1091
|
+
else signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Reject with a typed `"closed"` error once the client was closed or its
|
|
1095
|
+
* `signal` aborted, so no request is sent on a dead transport.
|
|
1096
|
+
*/
|
|
1097
|
+
assertOpen(method) {
|
|
1098
|
+
if (this.closing || this.options.signal?.aborted) throw new McpTransportError(`MCP client is closed; "${method}" was not sent.`, {
|
|
1099
|
+
type: "closed",
|
|
1100
|
+
method
|
|
1101
|
+
});
|
|
1064
1102
|
}
|
|
1065
1103
|
/**
|
|
1066
1104
|
* Run the MCP `initialize` handshake exactly once, then send the
|
|
@@ -1070,15 +1108,16 @@ var McpClientImpl = class {
|
|
|
1070
1108
|
*/
|
|
1071
1109
|
async handshake() {
|
|
1072
1110
|
if (this.initialized) return;
|
|
1111
|
+
this.assertOpen("initialize");
|
|
1073
1112
|
try {
|
|
1074
1113
|
await this.rpc.call("initialize", {
|
|
1075
|
-
protocolVersion: PROTOCOL_VERSION
|
|
1114
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1076
1115
|
capabilities: {},
|
|
1077
1116
|
clientInfo: {
|
|
1078
1117
|
name: "@warlock.js/ai-tools",
|
|
1079
1118
|
version: "4.4.0"
|
|
1080
1119
|
}
|
|
1081
|
-
});
|
|
1120
|
+
}, { signal: this.options.signal });
|
|
1082
1121
|
await this.rpc.notify("notifications/initialized");
|
|
1083
1122
|
} catch (cause) {
|
|
1084
1123
|
if (cause instanceof McpTransportError) throw cause;
|
|
@@ -1107,7 +1146,8 @@ var McpClientImpl = class {
|
|
|
1107
1146
|
*/
|
|
1108
1147
|
async listAndAdapt() {
|
|
1109
1148
|
await this.handshake();
|
|
1110
|
-
|
|
1149
|
+
this.assertOpen("tools/list");
|
|
1150
|
+
const descriptors = (await this.rpc.call("tools/list", void 0, { signal: this.options.signal })).tools ?? [];
|
|
1111
1151
|
const filter = this.options.filter;
|
|
1112
1152
|
return (filter ? descriptors.filter((d) => filter(d.name)) : descriptors).map((descriptor) => this.adapt(descriptor));
|
|
1113
1153
|
}
|
|
@@ -1128,11 +1168,12 @@ var McpClientImpl = class {
|
|
|
1128
1168
|
description: descriptor.description ?? `Invoke the remote MCP tool "${remoteName}".`,
|
|
1129
1169
|
input,
|
|
1130
1170
|
execute: async (args, ctx) => {
|
|
1171
|
+
this.assertOpen("tools/call");
|
|
1131
1172
|
const result = await this.rpc.call("tools/call", {
|
|
1132
1173
|
name: remoteName,
|
|
1133
1174
|
arguments: args ?? {}
|
|
1134
1175
|
}, {
|
|
1135
|
-
signal: ctx?.signal,
|
|
1176
|
+
signal: combineSignals(ctx?.signal, this.options.signal),
|
|
1136
1177
|
timeoutMs
|
|
1137
1178
|
});
|
|
1138
1179
|
if (result.isError) throw new McpTransportError(`MCP tool "${remoteName}" returned an error: ${unwrapContent(result.content)}`, {
|
|
@@ -1144,10 +1185,20 @@ var McpClientImpl = class {
|
|
|
1144
1185
|
});
|
|
1145
1186
|
}
|
|
1146
1187
|
close() {
|
|
1147
|
-
|
|
1188
|
+
this.options.signal?.removeEventListener("abort", this.onAbort);
|
|
1189
|
+
this.closing ??= this.rpc.close();
|
|
1190
|
+
return this.closing;
|
|
1148
1191
|
}
|
|
1149
1192
|
};
|
|
1150
1193
|
/**
|
|
1194
|
+
* The signal a `tools/call` honors: the per-call `ctx.signal`, the
|
|
1195
|
+
* client-level `options.signal`, both (whichever aborts first), or none.
|
|
1196
|
+
*/
|
|
1197
|
+
function combineSignals(callSignal, clientSignal) {
|
|
1198
|
+
if (!callSignal || !clientSignal) return callSignal ?? clientSignal;
|
|
1199
|
+
return AbortSignal.any([callSignal, clientSignal]);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1151
1202
|
* Flatten an MCP `tools/call` result's content blocks into the value a
|
|
1152
1203
|
* tool returns. Text blocks are concatenated; a single block whose text is
|
|
1153
1204
|
* valid JSON is parsed so structured tool output flows back as an object
|
|
@@ -1219,7 +1270,8 @@ function tryParseJson(text) {
|
|
|
1219
1270
|
* @param server - The transport config (`{ type: "stdio" }` /
|
|
1220
1271
|
* `{ type: "http" }`). A pre-built transport client may be injected for
|
|
1221
1272
|
* testing.
|
|
1222
|
-
* @param options - Prefix / filter / per-call timeout
|
|
1273
|
+
* @param options - Prefix / filter / per-call timeout, and a `signal` that
|
|
1274
|
+
* closes the client when it aborts.
|
|
1223
1275
|
* @returns An {@link McpClient} handle.
|
|
1224
1276
|
*
|
|
1225
1277
|
* @example
|
|
@@ -1233,6 +1285,462 @@ function mcp$1(server, options) {
|
|
|
1233
1285
|
return new McpClientImpl(server, options);
|
|
1234
1286
|
}
|
|
1235
1287
|
|
|
1288
|
+
//#endregion
|
|
1289
|
+
//#region ../../packages/ai-tools/src/mcp/protocol.ts
|
|
1290
|
+
/**
|
|
1291
|
+
* MCP protocol versions the `ai.mcp.serve` server speaks, newest first.
|
|
1292
|
+
* `initialize` echoes the client's requested version when it is listed
|
|
1293
|
+
* here and otherwise answers with the newest one (lifecycle, version
|
|
1294
|
+
* negotiation). The HTTP transport rejects any other value in the
|
|
1295
|
+
* `MCP-Protocol-Version` header with 400.
|
|
1296
|
+
*
|
|
1297
|
+
* Source: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle
|
|
1298
|
+
* and https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
|
|
1299
|
+
* (checked 2026-10-17).
|
|
1300
|
+
*/
|
|
1301
|
+
const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1302
|
+
"2025-11-25",
|
|
1303
|
+
"2025-06-18",
|
|
1304
|
+
"2025-03-26"
|
|
1305
|
+
];
|
|
1306
|
+
/** The newest MCP protocol version the server speaks. */
|
|
1307
|
+
const LATEST_PROTOCOL_VERSION = "2025-11-25";
|
|
1308
|
+
|
|
1309
|
+
//#endregion
|
|
1310
|
+
//#region ../../packages/ai-tools/src/mcp/serve-http.ts
|
|
1311
|
+
/**
|
|
1312
|
+
* MCP Streamable HTTP server transport for `ai.mcp.serve`.
|
|
1313
|
+
*
|
|
1314
|
+
* Implements the server side of the Streamable HTTP transport from the MCP
|
|
1315
|
+
* specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
|
|
1316
|
+
* checked 2026-10-17) on `node:http`, with no SDK dependency:
|
|
1317
|
+
*
|
|
1318
|
+
* - one endpoint path; each POST carries exactly one JSON-RPC message
|
|
1319
|
+
* (batches were removed from the protocol in 2025-06-18 and get 400);
|
|
1320
|
+
* - a request is answered with `Content-Type: application/json` (the spec
|
|
1321
|
+
* lets a server pick JSON or SSE per request); a notification or a
|
|
1322
|
+
* response is acknowledged with `202 Accepted` and no body;
|
|
1323
|
+
* - stateless: no `MCP-Session-Id` is issued (optional in the spec), so
|
|
1324
|
+
* GET (server-initiated SSE stream) and DELETE (session end) answer
|
|
1325
|
+
* `405 Method Not Allowed`, as the spec allows;
|
|
1326
|
+
* - an unsupported `MCP-Protocol-Version` header gets 400;
|
|
1327
|
+
* - security warning items: `Origin` is validated on every request (an
|
|
1328
|
+
* invalid one gets 403), the default bind is `127.0.0.1`, and
|
|
1329
|
+
* authentication is required for any non-loopback bind.
|
|
1330
|
+
*/
|
|
1331
|
+
/** Default bind interface — loopback only. */
|
|
1332
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
1333
|
+
/** Default endpoint path. */
|
|
1334
|
+
const DEFAULT_PATH = "/mcp";
|
|
1335
|
+
/** Default request body cap: 1 MiB. */
|
|
1336
|
+
const DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
1337
|
+
/** Default time `close()` waits for in-flight requests. */
|
|
1338
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 5e3;
|
|
1339
|
+
/** JSON-RPC codes used for transport-level error bodies. */
|
|
1340
|
+
const JSON_RPC_PARSE_ERROR = -32700;
|
|
1341
|
+
const JSON_RPC_INVALID_REQUEST$1 = -32600;
|
|
1342
|
+
/** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */
|
|
1343
|
+
const JSON_RPC_SERVER_ERROR = -32e3;
|
|
1344
|
+
/** Host names that always mean "this machine". */
|
|
1345
|
+
const LOOPBACK_NAMES = new Set([
|
|
1346
|
+
"localhost",
|
|
1347
|
+
"127.0.0.1",
|
|
1348
|
+
"::1",
|
|
1349
|
+
"[::1]"
|
|
1350
|
+
]);
|
|
1351
|
+
/**
|
|
1352
|
+
* True when `host` names a loopback interface (`localhost`, `127.0.0.0/8`,
|
|
1353
|
+
* `::1`, or an IPv4-mapped `::ffff:127.x.x.x`).
|
|
1354
|
+
*/
|
|
1355
|
+
function isLoopbackHost(host) {
|
|
1356
|
+
const normalized = host.trim().toLowerCase();
|
|
1357
|
+
if (LOOPBACK_NAMES.has(normalized)) return true;
|
|
1358
|
+
const unbracketed = normalized.replace(/^\[|\]$/g, "");
|
|
1359
|
+
const mapped = unbracketed.startsWith("::ffff:") ? unbracketed.slice(7) : unbracketed;
|
|
1360
|
+
return (0, node_net.isIPv4)(mapped) && mapped.startsWith("127.");
|
|
1361
|
+
}
|
|
1362
|
+
/** Strip the port from a `Host` header value (`[::1]:3000` → `[::1]`, `a.b:80` → `a.b`). */
|
|
1363
|
+
function hostnameOf(hostHeader) {
|
|
1364
|
+
const value = hostHeader.trim().toLowerCase();
|
|
1365
|
+
if (value.startsWith("[")) {
|
|
1366
|
+
const end = value.indexOf("]");
|
|
1367
|
+
return end === -1 ? value : value.slice(0, end + 1);
|
|
1368
|
+
}
|
|
1369
|
+
const colon = value.lastIndexOf(":");
|
|
1370
|
+
return colon === -1 ? value : value.slice(0, colon);
|
|
1371
|
+
}
|
|
1372
|
+
/** SHA-256 digest — equal-length input for `timingSafeEqual`. */
|
|
1373
|
+
function digest(value) {
|
|
1374
|
+
return (0, node_crypto.createHash)("sha256").update(value, "utf8").digest();
|
|
1375
|
+
}
|
|
1376
|
+
/** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
|
|
1377
|
+
function normalizeOrigin(origin) {
|
|
1378
|
+
try {
|
|
1379
|
+
return new URL(origin).origin;
|
|
1380
|
+
} catch (cause) {
|
|
1381
|
+
throw new McpServeError(`allowedOrigins entry "${origin}" is not a valid origin URL.`, {
|
|
1382
|
+
type: "config",
|
|
1383
|
+
cause
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Validate and default the transport options. Throws {@link McpServeError}
|
|
1389
|
+
* (`"config"`) for a non-loopback bind without auth, an empty bearer
|
|
1390
|
+
* token, an `auth` with neither check, a bad path / port / limit.
|
|
1391
|
+
*/
|
|
1392
|
+
function resolveSettings(transport) {
|
|
1393
|
+
const host = transport.host ?? DEFAULT_HOST;
|
|
1394
|
+
const port = transport.port ?? 0;
|
|
1395
|
+
const path = transport.path ?? DEFAULT_PATH;
|
|
1396
|
+
const loopback = isLoopbackHost(host);
|
|
1397
|
+
const auth = transport.auth;
|
|
1398
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new McpServeError(`MCP http port must be an integer in 0–65535 (got ${port}).`, { type: "config" });
|
|
1399
|
+
if (!path.startsWith("/")) throw new McpServeError(`MCP http path must start with "/" (got "${path}").`, { type: "config" });
|
|
1400
|
+
const maxBodyBytes = transport.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
1401
|
+
if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) throw new McpServeError("MCP http maxBodyBytes must be a positive integer.", { type: "config" });
|
|
1402
|
+
const drainTimeoutMs = transport.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
1403
|
+
if (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0) throw new McpServeError("MCP http drainTimeoutMs must be zero or a positive number.", { type: "config" });
|
|
1404
|
+
const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
|
|
1405
|
+
if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
|
|
1406
|
+
if (auth && auth.bearer !== void 0 && tokens.length === 0) throw new McpServeError("MCP http auth.bearer needs at least one token.", { type: "config" });
|
|
1407
|
+
if (auth && tokens.length === 0 && !auth.authorize) throw new McpServeError("MCP http auth needs `bearer` and/or `authorize`.", { type: "config" });
|
|
1408
|
+
const authRequired = tokens.length > 0 || auth?.authorize !== void 0;
|
|
1409
|
+
if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer } or { authorize }), or bind 127.0.0.1.`, {
|
|
1410
|
+
type: "config",
|
|
1411
|
+
context: { host }
|
|
1412
|
+
});
|
|
1413
|
+
return {
|
|
1414
|
+
host,
|
|
1415
|
+
port,
|
|
1416
|
+
path,
|
|
1417
|
+
loopback,
|
|
1418
|
+
maxBodyBytes,
|
|
1419
|
+
drainTimeoutMs,
|
|
1420
|
+
tokenDigests: tokens.map(digest),
|
|
1421
|
+
authorize: auth?.authorize,
|
|
1422
|
+
authRequired,
|
|
1423
|
+
allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
|
|
1424
|
+
allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
/** Flatten Node's header bag into lower-cased `name → value` strings. */
|
|
1428
|
+
function flattenHeaders(headers) {
|
|
1429
|
+
const flat = {};
|
|
1430
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1431
|
+
if (value === void 0) continue;
|
|
1432
|
+
flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
1433
|
+
}
|
|
1434
|
+
return flat;
|
|
1435
|
+
}
|
|
1436
|
+
/** Write a JSON body with a status code. */
|
|
1437
|
+
function sendJson(response, status, body, headers = {}) {
|
|
1438
|
+
if (response.headersSent || response.destroyed) return;
|
|
1439
|
+
const payload = JSON.stringify(body);
|
|
1440
|
+
response.writeHead(status, {
|
|
1441
|
+
...headers,
|
|
1442
|
+
"content-type": "application/json",
|
|
1443
|
+
"content-length": Buffer.byteLength(payload).toString()
|
|
1444
|
+
});
|
|
1445
|
+
response.end(payload);
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Write an HTTP-level refusal: the status plus a JSON-RPC error body with
|
|
1449
|
+
* no request id (the spec allows such a body on 4xx transport errors).
|
|
1450
|
+
*/
|
|
1451
|
+
function sendRefusal(response, status, code, message, headers = {}) {
|
|
1452
|
+
sendJson(response, status, {
|
|
1453
|
+
jsonrpc: "2.0",
|
|
1454
|
+
id: null,
|
|
1455
|
+
error: {
|
|
1456
|
+
code,
|
|
1457
|
+
message
|
|
1458
|
+
}
|
|
1459
|
+
}, headers);
|
|
1460
|
+
}
|
|
1461
|
+
/** Write an empty-bodied status (202 / 405). */
|
|
1462
|
+
function sendEmpty(response, status, headers = {}) {
|
|
1463
|
+
if (response.headersSent || response.destroyed) return;
|
|
1464
|
+
response.writeHead(status, headers);
|
|
1465
|
+
response.end();
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Bind an MCP Streamable HTTP endpoint and pump each JSON-RPC request
|
|
1469
|
+
* through `dispatch`. Resolves once the port is bound.
|
|
1470
|
+
*
|
|
1471
|
+
* Throws {@link McpServeError}: `"config"` for invalid / unsafe options
|
|
1472
|
+
* (checked before anything binds), `"listen"` when binding fails.
|
|
1473
|
+
*
|
|
1474
|
+
* @param transport - The http transport options.
|
|
1475
|
+
* @param dispatch - Maps one JSON-RPC request (plus call context) to its response.
|
|
1476
|
+
*/
|
|
1477
|
+
async function startHttpServer(transport, dispatch) {
|
|
1478
|
+
const settings = resolveSettings(transport);
|
|
1479
|
+
const shutdown = new AbortController();
|
|
1480
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1481
|
+
let closing;
|
|
1482
|
+
const server = (0, node_http.createServer)((request, response) => {
|
|
1483
|
+
if (closing) {
|
|
1484
|
+
sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
|
|
1488
|
+
sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
|
|
1489
|
+
});
|
|
1490
|
+
inFlight.add(work);
|
|
1491
|
+
work.finally(() => {
|
|
1492
|
+
inFlight.delete(work);
|
|
1493
|
+
if (closing) server.closeIdleConnections();
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
await new Promise((resolve, reject) => {
|
|
1497
|
+
const onError = (cause) => {
|
|
1498
|
+
reject(new McpServeError(`MCP http server could not listen on ${settings.host}:${settings.port}: ${cause.message}`, {
|
|
1499
|
+
type: "listen",
|
|
1500
|
+
cause,
|
|
1501
|
+
context: {
|
|
1502
|
+
host: settings.host,
|
|
1503
|
+
port: settings.port
|
|
1504
|
+
}
|
|
1505
|
+
}));
|
|
1506
|
+
};
|
|
1507
|
+
server.once("error", onError);
|
|
1508
|
+
server.listen(settings.port, settings.host, () => {
|
|
1509
|
+
server.off("error", onError);
|
|
1510
|
+
resolve();
|
|
1511
|
+
});
|
|
1512
|
+
});
|
|
1513
|
+
const address = server.address();
|
|
1514
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : settings.port;
|
|
1515
|
+
const url = `http://${(0, node_net.isIPv6)(settings.host) ? `[${settings.host}]` : settings.host}:${boundPort}${settings.path}`;
|
|
1516
|
+
const close = () => {
|
|
1517
|
+
if (closing) return closing;
|
|
1518
|
+
closing = (async () => {
|
|
1519
|
+
const closed = new Promise((resolve) => {
|
|
1520
|
+
server.close(() => resolve());
|
|
1521
|
+
});
|
|
1522
|
+
server.closeIdleConnections();
|
|
1523
|
+
if (inFlight.size > 0) {
|
|
1524
|
+
let timer;
|
|
1525
|
+
const timedOut = new Promise((resolve) => {
|
|
1526
|
+
timer = setTimeout(() => resolve("timeout"), settings.drainTimeoutMs);
|
|
1527
|
+
});
|
|
1528
|
+
const outcome = await Promise.race([Promise.allSettled([...inFlight]).then(() => "drained"), timedOut]);
|
|
1529
|
+
clearTimeout(timer);
|
|
1530
|
+
if (outcome === "timeout") {
|
|
1531
|
+
shutdown.abort();
|
|
1532
|
+
server.closeAllConnections();
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
server.closeIdleConnections();
|
|
1536
|
+
await closed;
|
|
1537
|
+
})();
|
|
1538
|
+
return closing;
|
|
1539
|
+
};
|
|
1540
|
+
return {
|
|
1541
|
+
url,
|
|
1542
|
+
close
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Validate and answer one HTTP request: path → Host / Origin → auth →
|
|
1547
|
+
* method → headers → body → JSON-RPC dispatch.
|
|
1548
|
+
*/
|
|
1549
|
+
async function handleRequest(request, response, settings, dispatch, signal) {
|
|
1550
|
+
const headers = flattenHeaders(request.headers);
|
|
1551
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
1552
|
+
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
1553
|
+
if (!hostAllowed(headers.host, settings)) {
|
|
1554
|
+
sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (!originAllowed(headers.origin, settings)) {
|
|
1558
|
+
sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (path !== settings.path) {
|
|
1562
|
+
sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const info = {
|
|
1566
|
+
method,
|
|
1567
|
+
path,
|
|
1568
|
+
headers
|
|
1569
|
+
};
|
|
1570
|
+
if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
|
|
1571
|
+
if (!await authorized(info, settings)) {
|
|
1572
|
+
sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", settings.tokenDigests.length > 0 ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {});
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
if (method !== "POST") {
|
|
1576
|
+
sendEmpty(response, 405, { allow: "POST" });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
const protocolVersion = headers["mcp-protocol-version"];
|
|
1580
|
+
if (protocolVersion !== void 0 && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
|
|
1581
|
+
sendRefusal(response, 400, JSON_RPC_SERVER_ERROR, `Unsupported MCP-Protocol-Version "${protocolVersion}" (supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")}).`);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (!isJsonContentType(headers["content-type"])) {
|
|
1585
|
+
sendRefusal(response, 415, JSON_RPC_SERVER_ERROR, "Content-Type must be application/json.");
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (!acceptsJson(headers.accept)) {
|
|
1589
|
+
sendRefusal(response, 406, JSON_RPC_SERVER_ERROR, "Accept must allow application/json.");
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const body = await readBody$1(request, settings.maxBodyBytes);
|
|
1593
|
+
if (body.type === "too-large") {
|
|
1594
|
+
sendRefusal(response, 413, JSON_RPC_SERVER_ERROR, `Request body exceeds ${settings.maxBodyBytes} bytes.`, { connection: "close" });
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
let message;
|
|
1598
|
+
try {
|
|
1599
|
+
message = JSON.parse(body.text);
|
|
1600
|
+
} catch {
|
|
1601
|
+
sendRefusal(response, 400, JSON_RPC_PARSE_ERROR, "Parse error: body is not valid JSON.");
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
1605
|
+
sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: the body must be one JSON-RPC message (batches are not supported).");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const record = message;
|
|
1609
|
+
if (record.jsonrpc !== "2.0") {
|
|
1610
|
+
sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: jsonrpc must be \"2.0\".");
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const id = record.id;
|
|
1614
|
+
if (typeof record.method === "string") {
|
|
1615
|
+
if (typeof id !== "string" && typeof id !== "number") {
|
|
1616
|
+
sendEmpty(response, 202);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const rpcRequest = {
|
|
1620
|
+
jsonrpc: "2.0",
|
|
1621
|
+
id,
|
|
1622
|
+
method: record.method
|
|
1623
|
+
};
|
|
1624
|
+
if (record.params !== void 0) rpcRequest.params = record.params;
|
|
1625
|
+
sendJson(response, 200, await dispatch(rpcRequest, {
|
|
1626
|
+
request: info,
|
|
1627
|
+
signal
|
|
1628
|
+
}));
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
|
|
1632
|
+
sendEmpty(response, 202);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST$1, "Invalid request: not a JSON-RPC message.");
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Host-header check. While bound to loopback only loopback names (plus
|
|
1639
|
+
* `allowedHosts`) are accepted, so a DNS-rebound name pointing at
|
|
1640
|
+
* 127.0.0.1 is refused. Off loopback the check applies only when
|
|
1641
|
+
* `allowedHosts` is set.
|
|
1642
|
+
*/
|
|
1643
|
+
function hostAllowed(hostHeader, settings) {
|
|
1644
|
+
if (!settings.loopback && settings.allowedHosts.size === 0) return true;
|
|
1645
|
+
if (hostHeader === void 0) return false;
|
|
1646
|
+
const name = hostnameOf(hostHeader);
|
|
1647
|
+
if (settings.allowedHosts.has(name)) return true;
|
|
1648
|
+
return settings.loopback && isLoopbackHost(name);
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Origin check (MCP Streamable HTTP security warning). No `Origin` header —
|
|
1652
|
+
* a non-browser client — passes; otherwise the origin must be a loopback
|
|
1653
|
+
* `http(s)` origin or listed in `allowedOrigins`. `null` and unparsable
|
|
1654
|
+
* origins are refused.
|
|
1655
|
+
*/
|
|
1656
|
+
function originAllowed(origin, settings) {
|
|
1657
|
+
if (origin === void 0) return true;
|
|
1658
|
+
let parsed;
|
|
1659
|
+
try {
|
|
1660
|
+
parsed = new URL(origin);
|
|
1661
|
+
} catch {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
if (settings.allowedOrigins.has(parsed.origin)) return true;
|
|
1665
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Run the configured auth. Bearer tokens are compared in constant time
|
|
1669
|
+
* over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
|
|
1670
|
+
* every configured token is compared so the match position does not leak.
|
|
1671
|
+
* An `authorize` hook that throws denies the request.
|
|
1672
|
+
*/
|
|
1673
|
+
async function authorized(info, settings) {
|
|
1674
|
+
if (!settings.authRequired) return true;
|
|
1675
|
+
if (settings.tokenDigests.length > 0) {
|
|
1676
|
+
const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
|
|
1677
|
+
const presented = digest(match?.[1]?.trim() ?? "");
|
|
1678
|
+
let matched = false;
|
|
1679
|
+
for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
|
|
1680
|
+
if (!match || !matched) return false;
|
|
1681
|
+
}
|
|
1682
|
+
if (settings.authorize) try {
|
|
1683
|
+
return await settings.authorize(info) === true;
|
|
1684
|
+
} catch {
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
/** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
|
|
1690
|
+
function isJsonContentType(contentType) {
|
|
1691
|
+
return contentType?.split(";")[0]?.trim().toLowerCase() === "application/json";
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* True when the `Accept` header allows a JSON reply. Clients are required
|
|
1695
|
+
* to list both `application/json` and `text/event-stream`; this server
|
|
1696
|
+
* always answers JSON, so it only insists on that half. A missing header
|
|
1697
|
+
* is accepted.
|
|
1698
|
+
*/
|
|
1699
|
+
function acceptsJson(accept) {
|
|
1700
|
+
if (accept === void 0 || accept.trim() === "") return true;
|
|
1701
|
+
return accept.split(",").map((part) => part.split(";")[0]?.trim().toLowerCase()).some((type) => type === "application/json" || type === "application/*" || type === "*/*");
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Read the request body as UTF-8, refusing anything over `limit` bytes —
|
|
1705
|
+
* up front from `Content-Length`, and while streaming for chunked bodies.
|
|
1706
|
+
* A refused body is left unread; the 413 response closes the connection.
|
|
1707
|
+
*/
|
|
1708
|
+
function readBody$1(request, limit) {
|
|
1709
|
+
const declared = Number(request.headers["content-length"]);
|
|
1710
|
+
if (Number.isFinite(declared) && declared > limit) return Promise.resolve({ type: "too-large" });
|
|
1711
|
+
return new Promise((resolve, reject) => {
|
|
1712
|
+
const chunks = [];
|
|
1713
|
+
let size = 0;
|
|
1714
|
+
let done = false;
|
|
1715
|
+
const finish = (result) => {
|
|
1716
|
+
if (!done) {
|
|
1717
|
+
done = true;
|
|
1718
|
+
resolve(result);
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
request.on("data", (chunk) => {
|
|
1722
|
+
if (done) return;
|
|
1723
|
+
size += chunk.length;
|
|
1724
|
+
if (size > limit) {
|
|
1725
|
+
request.pause();
|
|
1726
|
+
finish({ type: "too-large" });
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
chunks.push(chunk);
|
|
1730
|
+
});
|
|
1731
|
+
request.on("end", () => finish({
|
|
1732
|
+
type: "ok",
|
|
1733
|
+
text: Buffer.concat(chunks).toString("utf8")
|
|
1734
|
+
}));
|
|
1735
|
+
request.on("error", (error) => {
|
|
1736
|
+
if (!done) {
|
|
1737
|
+
done = true;
|
|
1738
|
+
reject(error);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1236
1744
|
//#endregion
|
|
1237
1745
|
//#region ../../packages/ai-tools/src/mcp/serve.ts
|
|
1238
1746
|
/** JSON-RPC version literal every outbound message carries. */
|
|
@@ -1241,59 +1749,159 @@ const JSONRPC_VERSION = "2.0";
|
|
|
1241
1749
|
const DEFAULT_SCHEMA_TARGET = "draft-2020-12";
|
|
1242
1750
|
/** Default advertised server version when the caller omits one. */
|
|
1243
1751
|
const DEFAULT_VERSION = "4.4.0";
|
|
1244
|
-
/** The MCP protocol version this server advertises in `initialize`. */
|
|
1245
|
-
const PROTOCOL_VERSION = "2025-06-18";
|
|
1246
1752
|
/** JSON-RPC standard error codes we emit. */
|
|
1247
1753
|
const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
1248
1754
|
const JSON_RPC_INVALID_PARAMS = -32602;
|
|
1755
|
+
/** MCP's "resource not found" error code (server/resources, error handling). */
|
|
1756
|
+
const MCP_RESOURCE_NOT_FOUND = -32002;
|
|
1249
1757
|
/**
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1758
|
+
* A JSON-RPC failure raised inside a method handler, carrying the exact
|
|
1759
|
+
* code to answer with (unknown resource, missing prompt argument, …).
|
|
1252
1760
|
*/
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1761
|
+
var RpcFailure = class extends Error {
|
|
1762
|
+
constructor(code, message, data) {
|
|
1763
|
+
super(message);
|
|
1764
|
+
this.code = code;
|
|
1765
|
+
this.data = data;
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
/** Enumerate one tool set into a flat contract array. */
|
|
1769
|
+
function toolsOf(source) {
|
|
1770
|
+
return Array.isArray(source) ? source : source.tools();
|
|
1771
|
+
}
|
|
1772
|
+
/** Narrow a serve source to its grouped shape. */
|
|
1773
|
+
function isGroupedSource(source) {
|
|
1774
|
+
return !Array.isArray(source) && "groups" in source;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Resolve the serve source into the exposed tool list, applying
|
|
1778
|
+
* `prefixToolNames` and refusing duplicate names. Throws
|
|
1779
|
+
* {@link McpServeError} — `"config"` for `prefixToolNames` on an ungrouped
|
|
1780
|
+
* source, `"collision"` when two tools would share a name.
|
|
1781
|
+
*/
|
|
1782
|
+
function resolveTools(source, options) {
|
|
1783
|
+
const prefix = options.prefixToolNames ?? false;
|
|
1784
|
+
const exposed = [];
|
|
1785
|
+
if (isGroupedSource(source)) for (const [group, groupSource] of Object.entries(source.groups)) for (const contract of toolsOf(groupSource)) {
|
|
1786
|
+
let name = contract.name;
|
|
1787
|
+
if (prefix === true) name = `${group}_${contract.name}`;
|
|
1788
|
+
else if (typeof prefix === "function") name = prefix(group, contract.name);
|
|
1789
|
+
exposed.push({
|
|
1790
|
+
name,
|
|
1791
|
+
contract,
|
|
1792
|
+
origin: `group "${group}"`
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
if (prefix !== false) throw new McpServeError("prefixToolNames needs a grouped source: serve({ groups: { name: tools } }, …).", { type: "config" });
|
|
1797
|
+
for (const contract of toolsOf(source)) exposed.push({
|
|
1798
|
+
name: contract.name,
|
|
1799
|
+
contract,
|
|
1800
|
+
origin: "the source"
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1804
|
+
for (const tool of exposed) {
|
|
1805
|
+
const previous = seen.get(tool.name);
|
|
1806
|
+
if (previous !== void 0) throw new McpServeError(`MCP tool name "${tool.name}" is exposed twice (from ${previous} and ${tool.origin}). Rename one tool, or serve a grouped source with prefixToolNames.`, {
|
|
1807
|
+
type: "collision",
|
|
1808
|
+
context: { name: tool.name }
|
|
1809
|
+
});
|
|
1810
|
+
seen.set(tool.name, tool.origin);
|
|
1811
|
+
}
|
|
1812
|
+
return exposed.map(({ name, contract }) => ({
|
|
1813
|
+
name,
|
|
1814
|
+
contract
|
|
1815
|
+
}));
|
|
1816
|
+
}
|
|
1817
|
+
/** Throw a collision error when two entries share the same key. */
|
|
1818
|
+
function assertUnique(entries, key, label) {
|
|
1819
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1820
|
+
for (const entry of entries ?? []) {
|
|
1821
|
+
const value = key(entry);
|
|
1822
|
+
if (seen.has(value)) throw new McpServeError(`MCP ${label} "${value}" is declared twice.`, {
|
|
1823
|
+
type: "collision",
|
|
1824
|
+
context: { [label]: value }
|
|
1825
|
+
});
|
|
1826
|
+
seen.add(value);
|
|
1827
|
+
}
|
|
1256
1828
|
}
|
|
1257
1829
|
/**
|
|
1258
1830
|
* The pure protocol core of `serve` — maps one JSON-RPC request to its
|
|
1259
|
-
* response, with no I/O. Both the stdio and
|
|
1831
|
+
* response, with no I/O. Both the stdio and HTTP serve-transports pump
|
|
1260
1832
|
* their inbound requests through this, and specs can drive it directly.
|
|
1261
1833
|
*
|
|
1262
|
-
* Handles
|
|
1263
|
-
* `
|
|
1264
|
-
*
|
|
1834
|
+
* Handles `initialize`, `ping`, `tools/list`, `tools/call`, and — when
|
|
1835
|
+
* configured — `resources/list`, `resources/templates/list`,
|
|
1836
|
+
* `resources/read`, `prompts/list` and `prompts/get`. Any other method
|
|
1837
|
+
* answers with a JSON-RPC `method not found` error.
|
|
1265
1838
|
*
|
|
1266
1839
|
* Constructed via {@link createServeHandler}.
|
|
1267
1840
|
*/
|
|
1268
1841
|
var McpServeHandler = class {
|
|
1269
1842
|
constructor(source, options) {
|
|
1270
|
-
this.tools = resolveTools(source);
|
|
1271
|
-
this.byName = new Map(this.tools.map((
|
|
1843
|
+
this.tools = resolveTools(source, options);
|
|
1844
|
+
this.byName = new Map(this.tools.map((tool) => [tool.name, tool.contract]));
|
|
1845
|
+
assertUnique(options.resources, (resource) => resource.uri, "resource");
|
|
1846
|
+
assertUnique(options.prompts, (prompt) => prompt.name, "prompt");
|
|
1847
|
+
this.resources = options.resources ? new Map(options.resources.map((resource) => [resource.uri, resource])) : void 0;
|
|
1848
|
+
this.prompts = options.prompts ? new Map(options.prompts.map((prompt) => [prompt.name, prompt])) : void 0;
|
|
1272
1849
|
this.options = options;
|
|
1273
1850
|
}
|
|
1274
1851
|
/**
|
|
1275
1852
|
* Dispatch one inbound JSON-RPC request to its handler and produce the
|
|
1276
1853
|
* response. A handler that throws is mapped to a JSON-RPC error response
|
|
1277
1854
|
* — the serve loop never crashes on a bad request.
|
|
1855
|
+
*
|
|
1856
|
+
* @param request - The JSON-RPC request.
|
|
1857
|
+
* @param context - Per-call context: the inbound HTTP request (headers
|
|
1858
|
+
* reach tools as `ctx.request`) and an abort signal.
|
|
1278
1859
|
*/
|
|
1279
|
-
async handle(request) {
|
|
1860
|
+
async handle(request, context = {}) {
|
|
1280
1861
|
try {
|
|
1281
|
-
|
|
1282
|
-
case "initialize": return this.ok(request.id, this.initializeResult());
|
|
1283
|
-
case "tools/list": return this.ok(request.id, { tools: this.listTools() });
|
|
1284
|
-
case "tools/call": return this.ok(request.id, await this.callTool(request.params));
|
|
1285
|
-
default: return this.error(request.id, JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
|
|
1286
|
-
}
|
|
1862
|
+
return this.ok(request.id, await this.dispatch(request, context));
|
|
1287
1863
|
} catch (cause) {
|
|
1864
|
+
if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
|
|
1288
1865
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1289
1866
|
return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);
|
|
1290
1867
|
}
|
|
1291
1868
|
}
|
|
1292
|
-
/**
|
|
1293
|
-
|
|
1869
|
+
/** Route a method to its result builder. */
|
|
1870
|
+
async dispatch(request, context) {
|
|
1871
|
+
switch (request.method) {
|
|
1872
|
+
case "initialize": return this.initializeResult(request.params);
|
|
1873
|
+
case "ping": return {};
|
|
1874
|
+
case "tools/list": return { tools: this.listTools() };
|
|
1875
|
+
case "tools/call": return this.callTool(request.params, context);
|
|
1876
|
+
default: break;
|
|
1877
|
+
}
|
|
1878
|
+
if (this.resources) switch (request.method) {
|
|
1879
|
+
case "resources/list": return { resources: this.listResources(this.resources) };
|
|
1880
|
+
case "resources/templates/list": return { resourceTemplates: [] };
|
|
1881
|
+
case "resources/read": return this.readResource(this.resources, request.params, context);
|
|
1882
|
+
default: break;
|
|
1883
|
+
}
|
|
1884
|
+
if (this.prompts) switch (request.method) {
|
|
1885
|
+
case "prompts/list": return { prompts: this.listPrompts(this.prompts) };
|
|
1886
|
+
case "prompts/get": return this.getPrompt(this.prompts, request.params, context);
|
|
1887
|
+
default: break;
|
|
1888
|
+
}
|
|
1889
|
+
throw new RpcFailure(JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Build the `initialize` result advertising name / version /
|
|
1893
|
+
* capabilities, negotiating the protocol version: the client's requested
|
|
1894
|
+
* version when supported, otherwise the newest this server speaks.
|
|
1895
|
+
*/
|
|
1896
|
+
initializeResult(params) {
|
|
1897
|
+
const requested = readRecord(params).protocolVersion;
|
|
1898
|
+
const protocolVersion = typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
|
|
1899
|
+
const capabilities = { tools: {} };
|
|
1900
|
+
if (this.resources) capabilities.resources = {};
|
|
1901
|
+
if (this.prompts) capabilities.prompts = {};
|
|
1294
1902
|
return {
|
|
1295
|
-
protocolVersion
|
|
1296
|
-
capabilities
|
|
1903
|
+
protocolVersion,
|
|
1904
|
+
capabilities,
|
|
1297
1905
|
serverInfo: {
|
|
1298
1906
|
name: this.options.name,
|
|
1299
1907
|
version: this.options.version ?? DEFAULT_VERSION
|
|
@@ -1302,19 +1910,19 @@ var McpServeHandler = class {
|
|
|
1302
1910
|
}
|
|
1303
1911
|
/**
|
|
1304
1912
|
* Build the `tools/list` payload: one {@link McpToolDescriptor} per
|
|
1305
|
-
*
|
|
1306
|
-
* configured dialect (default `draft-2020-12` — overriding
|
|
1913
|
+
* exposed tool, its `inputSchema` extracted via `extractJsonSchema` at
|
|
1914
|
+
* the configured dialect (default `draft-2020-12` — overriding
|
|
1307
1915
|
* `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
|
|
1308
1916
|
*/
|
|
1309
1917
|
listTools() {
|
|
1310
1918
|
const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
|
|
1311
|
-
return this.tools.map((contract) => {
|
|
1919
|
+
return this.tools.map(({ name, contract }) => {
|
|
1312
1920
|
const inputSchema = (0, _warlock_js_ai.extractJsonSchema)(contract.input, { target }) ?? {
|
|
1313
1921
|
type: "object",
|
|
1314
1922
|
properties: {}
|
|
1315
1923
|
};
|
|
1316
1924
|
return {
|
|
1317
|
-
name
|
|
1925
|
+
name,
|
|
1318
1926
|
description: contract.description,
|
|
1319
1927
|
inputSchema
|
|
1320
1928
|
};
|
|
@@ -1325,15 +1933,27 @@ var McpServeHandler = class {
|
|
|
1325
1933
|
* never-throwing {@link import("@warlock.js/ai").ToolInvokeResult}: `data`
|
|
1326
1934
|
* → a text content block, `error` → an `isError: true` result. An unknown
|
|
1327
1935
|
* tool name throws (mapped to a JSON-RPC error by {@link handle}).
|
|
1936
|
+
*
|
|
1937
|
+
* The contract receives a `ToolContext` carrying the abort signal and,
|
|
1938
|
+
* over HTTP, the inbound request's headers on `ctx.request`.
|
|
1328
1939
|
*/
|
|
1329
|
-
async callTool(params) {
|
|
1940
|
+
async callTool(params, context) {
|
|
1330
1941
|
const { name, args } = readCallParams(params);
|
|
1331
1942
|
const contract = this.byName.get(name);
|
|
1332
1943
|
if (!contract) throw new McpTransportError(`Unknown tool "${name}".`, {
|
|
1333
1944
|
type: "protocol",
|
|
1334
1945
|
method: "tools/call"
|
|
1335
1946
|
});
|
|
1336
|
-
const
|
|
1947
|
+
const toolContext = { artifacts: {} };
|
|
1948
|
+
if (context.signal) toolContext.signal = context.signal;
|
|
1949
|
+
if (context.request) {
|
|
1950
|
+
const { headers, remoteAddress } = context.request;
|
|
1951
|
+
toolContext.request = remoteAddress === void 0 ? { headers } : {
|
|
1952
|
+
headers,
|
|
1953
|
+
remoteAddress
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
const result = await contract.invoke(args, toolContext);
|
|
1337
1957
|
if (result.error) return {
|
|
1338
1958
|
content: [{
|
|
1339
1959
|
type: "text",
|
|
@@ -1349,6 +1969,75 @@ var McpServeHandler = class {
|
|
|
1349
1969
|
isError: false
|
|
1350
1970
|
};
|
|
1351
1971
|
}
|
|
1972
|
+
/** Build the `resources/list` payload (metadata only, no contents). */
|
|
1973
|
+
listResources(resources) {
|
|
1974
|
+
return [...resources.values()].map((resource) => compact({
|
|
1975
|
+
uri: resource.uri,
|
|
1976
|
+
name: resource.name,
|
|
1977
|
+
title: resource.title,
|
|
1978
|
+
description: resource.description,
|
|
1979
|
+
mimeType: resource.mimeType
|
|
1980
|
+
}));
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Answer `resources/read`: run the resource's `read()` and wrap the
|
|
1984
|
+
* result as one text or base64 blob content entry.
|
|
1985
|
+
*/
|
|
1986
|
+
async readResource(resources, params, context) {
|
|
1987
|
+
const uri = readRecord(params).uri;
|
|
1988
|
+
if (typeof uri !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "resources/read requires a string `uri`.");
|
|
1989
|
+
const resource = resources.get(uri);
|
|
1990
|
+
if (!resource) throw new RpcFailure(MCP_RESOURCE_NOT_FOUND, "Resource not found", { uri });
|
|
1991
|
+
const body = await resource.read(context);
|
|
1992
|
+
return { contents: [compact(typeof body === "string" ? {
|
|
1993
|
+
uri,
|
|
1994
|
+
mimeType: resource.mimeType,
|
|
1995
|
+
text: body
|
|
1996
|
+
} : {
|
|
1997
|
+
uri,
|
|
1998
|
+
mimeType: resource.mimeType,
|
|
1999
|
+
blob: Buffer.from(body).toString("base64")
|
|
2000
|
+
})] };
|
|
2001
|
+
}
|
|
2002
|
+
/** Build the `prompts/list` payload. */
|
|
2003
|
+
listPrompts(prompts) {
|
|
2004
|
+
return [...prompts.values()].map((prompt) => compact({
|
|
2005
|
+
name: prompt.name,
|
|
2006
|
+
title: prompt.title,
|
|
2007
|
+
description: prompt.description,
|
|
2008
|
+
arguments: prompt.arguments
|
|
2009
|
+
}));
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Answer `prompts/get`: validate the arguments (strings only; required
|
|
2013
|
+
* ones present) and build the messages. An unknown prompt or a missing
|
|
2014
|
+
* required argument answers invalid-params, per the MCP prompts spec.
|
|
2015
|
+
*/
|
|
2016
|
+
async getPrompt(prompts, params, context) {
|
|
2017
|
+
const record = readRecord(params);
|
|
2018
|
+
const name = record.name;
|
|
2019
|
+
if (typeof name !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "prompts/get requires a string `name`.");
|
|
2020
|
+
const prompt = prompts.get(name);
|
|
2021
|
+
if (!prompt) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Unknown prompt "${name}".`);
|
|
2022
|
+
const args = {};
|
|
2023
|
+
for (const [key, value] of Object.entries(readRecord(record.arguments))) {
|
|
2024
|
+
if (typeof value !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt argument "${key}" must be a string.`);
|
|
2025
|
+
args[key] = value;
|
|
2026
|
+
}
|
|
2027
|
+
for (const argument of prompt.arguments ?? []) if (argument.required && args[argument.name] === void 0) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt "${name}" requires the "${argument.name}" argument.`);
|
|
2028
|
+
const built = await prompt.get(args, context);
|
|
2029
|
+
const messages = typeof built === "string" ? [{
|
|
2030
|
+
role: "user",
|
|
2031
|
+
content: {
|
|
2032
|
+
type: "text",
|
|
2033
|
+
text: built
|
|
2034
|
+
}
|
|
2035
|
+
}] : built;
|
|
2036
|
+
return compact({
|
|
2037
|
+
description: prompt.description,
|
|
2038
|
+
messages
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
1352
2041
|
/** Build a JSON-RPC success response. */
|
|
1353
2042
|
ok(id, result) {
|
|
1354
2043
|
return {
|
|
@@ -1358,17 +2047,77 @@ var McpServeHandler = class {
|
|
|
1358
2047
|
};
|
|
1359
2048
|
}
|
|
1360
2049
|
/** Build a JSON-RPC error response. */
|
|
1361
|
-
error(id, code, message) {
|
|
2050
|
+
error(id, code, message, data) {
|
|
1362
2051
|
return {
|
|
1363
2052
|
jsonrpc: JSONRPC_VERSION,
|
|
1364
2053
|
id,
|
|
1365
|
-
error: {
|
|
2054
|
+
error: data === void 0 ? {
|
|
1366
2055
|
code,
|
|
1367
2056
|
message
|
|
2057
|
+
} : {
|
|
2058
|
+
code,
|
|
2059
|
+
message,
|
|
2060
|
+
data
|
|
1368
2061
|
}
|
|
1369
2062
|
};
|
|
1370
2063
|
}
|
|
1371
2064
|
};
|
|
2065
|
+
/** JSON-RPC "Invalid Request" — the line is JSON but not a request object. */
|
|
2066
|
+
const JSON_RPC_INVALID_REQUEST = -32600;
|
|
2067
|
+
/** JSON-RPC "Internal error" — answering the request failed unexpectedly. */
|
|
2068
|
+
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
2069
|
+
/**
|
|
2070
|
+
* Turn one stdio line into the line to write back, or `undefined` when
|
|
2071
|
+
* nothing is answered: blank or non-JSON lines and notifications (no `id`).
|
|
2072
|
+
* A JSON value that is not a request object (`null`, a number, an array) is
|
|
2073
|
+
* answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
|
|
2074
|
+
* Never rejects: a dispatch failure is answered as an internal error.
|
|
2075
|
+
*/
|
|
2076
|
+
async function answerStdioLine(line, dispatch) {
|
|
2077
|
+
const trimmed = line.trim();
|
|
2078
|
+
if (!trimmed) return;
|
|
2079
|
+
let message;
|
|
2080
|
+
try {
|
|
2081
|
+
message = JSON.parse(trimmed);
|
|
2082
|
+
} catch {
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return `${JSON.stringify({
|
|
2086
|
+
jsonrpc: JSONRPC_VERSION,
|
|
2087
|
+
id: null,
|
|
2088
|
+
error: {
|
|
2089
|
+
code: JSON_RPC_INVALID_REQUEST,
|
|
2090
|
+
message: "Invalid request: not a JSON-RPC object."
|
|
2091
|
+
}
|
|
2092
|
+
})}\n`;
|
|
2093
|
+
const request = message;
|
|
2094
|
+
const id = request.id;
|
|
2095
|
+
if (id === void 0 || id === null) return;
|
|
2096
|
+
try {
|
|
2097
|
+
const response = await dispatch(request);
|
|
2098
|
+
return `${JSON.stringify(response)}\n`;
|
|
2099
|
+
} catch {
|
|
2100
|
+
return `${JSON.stringify({
|
|
2101
|
+
jsonrpc: JSONRPC_VERSION,
|
|
2102
|
+
id,
|
|
2103
|
+
error: {
|
|
2104
|
+
code: JSON_RPC_INTERNAL_ERROR,
|
|
2105
|
+
message: "Internal error."
|
|
2106
|
+
}
|
|
2107
|
+
})}\n`;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
/** Read an unknown value as a plain record (non-objects read as empty). */
|
|
2111
|
+
function readRecord(value) {
|
|
2112
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
2113
|
+
return value;
|
|
2114
|
+
}
|
|
2115
|
+
/** Drop `undefined` members so optional wire fields are omitted, not `null`. */
|
|
2116
|
+
function compact(value) {
|
|
2117
|
+
const result = {};
|
|
2118
|
+
for (const [key, member] of Object.entries(value)) if (member !== void 0) result[key] = member;
|
|
2119
|
+
return result;
|
|
2120
|
+
}
|
|
1372
2121
|
/**
|
|
1373
2122
|
* Read and validate the `tools/call` params into `{ name, args }`. Throws
|
|
1374
2123
|
* a typed {@link McpTransportError} when `name` is missing — mapped to a
|
|
@@ -1404,9 +2153,12 @@ function serializeData(data) {
|
|
|
1404
2153
|
* {@link serve}) so callers and tests can drive the MCP protocol without an
|
|
1405
2154
|
* actual transport — feed it a JSON-RPC request, get the response.
|
|
1406
2155
|
*
|
|
1407
|
-
* @
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
2156
|
+
* Throws {@link McpServeError} on duplicate tool names / resource URIs /
|
|
2157
|
+
* prompt names, or `prefixToolNames` on an ungrouped source.
|
|
2158
|
+
*
|
|
2159
|
+
* @param source - The tools to expose (one tool set or `{ groups }`).
|
|
2160
|
+
* @param options - Serve options (name / version / schema target / resources / prompts).
|
|
2161
|
+
* @returns An object whose `handle(request, context?)` maps a request to a response.
|
|
1410
2162
|
*/
|
|
1411
2163
|
function createServeHandler(source, options) {
|
|
1412
2164
|
return new McpServeHandler(source, options);
|
|
@@ -1414,79 +2166,66 @@ function createServeHandler(source, options) {
|
|
|
1414
2166
|
/**
|
|
1415
2167
|
* The internal {@link McpServer} — owns a {@link McpServeHandler} and a
|
|
1416
2168
|
* transport pump. For `stdio` it reads newline-delimited JSON-RPC requests
|
|
1417
|
-
* from `process.stdin` and writes responses to `process.stdout`;
|
|
1418
|
-
* `http`
|
|
1419
|
-
* (a serve-over-HTTP needs a server the caller owns).
|
|
2169
|
+
* from `process.stdin` and writes responses to `process.stdout`; for
|
|
2170
|
+
* `http` it binds a Streamable HTTP endpoint (see `./serve-http`).
|
|
1420
2171
|
*
|
|
1421
2172
|
* Constructed via {@link serve}; the class itself is internal.
|
|
1422
2173
|
*/
|
|
1423
2174
|
var McpServerImpl = class {
|
|
1424
2175
|
constructor(source, options) {
|
|
1425
|
-
this.running = false;
|
|
1426
2176
|
this.handler = new McpServeHandler(source, options);
|
|
1427
2177
|
this.options = options;
|
|
1428
2178
|
}
|
|
1429
|
-
|
|
1430
|
-
if (this.running)
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
2179
|
+
start() {
|
|
2180
|
+
if (!this.running) {
|
|
2181
|
+
const transport = this.options.transport ?? { type: "stdio" };
|
|
2182
|
+
const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
|
|
2183
|
+
this.running = starting.catch((error) => {
|
|
2184
|
+
this.running = void 0;
|
|
2185
|
+
throw error;
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
return this.running;
|
|
2189
|
+
}
|
|
2190
|
+
/** Bind the HTTP endpoint; its `close()` also makes this server startable again. */
|
|
2191
|
+
async startHttp(transport) {
|
|
2192
|
+
const http = await startHttpServer(transport, (request, context) => this.handler.handle(request, context));
|
|
2193
|
+
return {
|
|
2194
|
+
url: http.url,
|
|
2195
|
+
close: async () => {
|
|
2196
|
+
await http.close();
|
|
2197
|
+
this.running = void 0;
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
/** Wire the stdin line reader and answer each request on stdout. */
|
|
2202
|
+
startStdio() {
|
|
2203
|
+
const reader = (0, node_readline.createInterface)({ input: process.stdin });
|
|
2204
|
+
reader.on("line", (line) => {
|
|
2205
|
+
this.onLine(line).catch(() => {});
|
|
1436
2206
|
});
|
|
2207
|
+
return { close: async () => {
|
|
2208
|
+
reader.close();
|
|
2209
|
+
this.running = void 0;
|
|
2210
|
+
} };
|
|
1437
2211
|
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Parse one stdin line as a JSON-RPC request, dispatch it through the
|
|
1440
|
-
* handler, and write the response as a single line to stdout. Non-JSON
|
|
1441
|
-
* lines and notifications (no `id`) are ignored.
|
|
1442
|
-
*/
|
|
2212
|
+
/** Answer one stdin line on stdout (see {@link answerStdioLine}). */
|
|
1443
2213
|
async onLine(line) {
|
|
1444
|
-
const
|
|
1445
|
-
if (
|
|
1446
|
-
|
|
2214
|
+
const reply = await answerStdioLine(line, (request) => this.handler.handle(request));
|
|
2215
|
+
if (reply !== void 0) process.stdout.write(reply);
|
|
2216
|
+
}
|
|
2217
|
+
async stop() {
|
|
2218
|
+
const running = this.running;
|
|
2219
|
+
if (!running) return;
|
|
2220
|
+
let handle;
|
|
1447
2221
|
try {
|
|
1448
|
-
|
|
2222
|
+
handle = await running;
|
|
1449
2223
|
} catch {
|
|
1450
2224
|
return;
|
|
1451
2225
|
}
|
|
1452
|
-
|
|
1453
|
-
const response = await this.handler.handle(request);
|
|
1454
|
-
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1455
|
-
}
|
|
1456
|
-
async stop() {
|
|
1457
|
-
this.running = false;
|
|
1458
|
-
this.reader?.close();
|
|
1459
|
-
this.reader = void 0;
|
|
2226
|
+
await handle.close();
|
|
1460
2227
|
}
|
|
1461
2228
|
};
|
|
1462
|
-
/**
|
|
1463
|
-
* Expose a built agent / supervisor / orchestrator (or a raw
|
|
1464
|
-
* `ToolContract[]`) AS an MCP server (Direction B: local primitive → MCP
|
|
1465
|
-
* server other clients consume).
|
|
1466
|
-
*
|
|
1467
|
-
* Enumerates `source.tools()` (or the literal array) once at construction.
|
|
1468
|
-
* `tools/list` answers with each tool's `inputSchema` extracted via
|
|
1469
|
-
* `extractJsonSchema` at the configured `schemaTarget` (default
|
|
1470
|
-
* `draft-2020-12`). `tools/call` routes to the named contract's
|
|
1471
|
-
* `invoke()` and maps the never-throwing result — `data` becomes a text
|
|
1472
|
-
* content block, `error` becomes an `isError: true` result — so a failing
|
|
1473
|
-
* tool surfaces as a normal MCP tool error rather than crashing the server.
|
|
1474
|
-
*
|
|
1475
|
-
* The default transport is `stdio`, pumped over `process.stdin` /
|
|
1476
|
-
* `process.stdout`. Serving over HTTP is left to a host-owned server;
|
|
1477
|
-
* `start()` rejects an `http` transport (the protocol core is available
|
|
1478
|
-
* via {@link createServeHandler} for a caller's own HTTP wiring).
|
|
1479
|
-
*
|
|
1480
|
-
* @param source - The tools to expose.
|
|
1481
|
-
* @param options - Server name, version, transport, and schema dialect.
|
|
1482
|
-
* @returns An {@link McpServer} with `start()` / `stop()`.
|
|
1483
|
-
*
|
|
1484
|
-
* @example
|
|
1485
|
-
* serve(
|
|
1486
|
-
* { tools: () => ws.allTools() },
|
|
1487
|
-
* { name: "warlock-workspace", transport: { type: "stdio" } },
|
|
1488
|
-
* ).start();
|
|
1489
|
-
*/
|
|
1490
2229
|
function serve(source, options) {
|
|
1491
2230
|
return new McpServerImpl(source, options);
|
|
1492
2231
|
}
|
|
@@ -2560,7 +3299,8 @@ _warlock_js_ai.ai.tools = {
|
|
|
2560
3299
|
fetchUrl: fetchUrlTool,
|
|
2561
3300
|
http: httpRequestTool,
|
|
2562
3301
|
calculator: calculatorTool,
|
|
2563
|
-
dateTime: (options) => dateTimeTool(options)
|
|
3302
|
+
dateTime: (options) => dateTimeTool(options),
|
|
3303
|
+
measure: _warlock_js_ai.measureTools
|
|
2564
3304
|
};
|
|
2565
3305
|
_warlock_js_ai.ai.mcp = mcp;
|
|
2566
3306
|
|
|
@@ -2568,7 +3308,9 @@ _warlock_js_ai.ai.mcp = mcp;
|
|
|
2568
3308
|
exports.CalculatorError = CalculatorError;
|
|
2569
3309
|
exports.DateTimeError = DateTimeError;
|
|
2570
3310
|
exports.HttpPolicyError = HttpPolicyError;
|
|
3311
|
+
exports.McpServeError = McpServeError;
|
|
2571
3312
|
exports.McpTransportError = McpTransportError;
|
|
3313
|
+
exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
|
|
2572
3314
|
exports.WebToolError = WebToolError;
|
|
2573
3315
|
exports.calculatorTool = calculatorTool;
|
|
2574
3316
|
exports.createJsonRpcClient = createJsonRpcClient;
|