@warlock.js/ai-tools 5.32.0 → 5.33.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 +10 -0
- package/cjs/index.cjs +281 -118
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/mcp-serve.type.d.mts +39 -7
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/mcp/serve-http.mjs +61 -23
- package/esm/mcp/serve-http.mjs.map +1 -1
- package/esm/mcp/serve-requests.mjs +62 -0
- package/esm/mcp/serve-requests.mjs.map +1 -0
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +81 -12
- package/esm/mcp/serve.mjs.map +1 -1
- package/llms-full.txt +69 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/connect-mcp-server/SKILL.md +15 -0
- package/skills/expose-as-mcp-server/SKILL.md +54 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `@warlock.js/ai-tools` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.33.0 - 2026-10-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Keep HTTP and MCP fixtures valid under strict indexed access, accept string JSON-RPC request IDs in serving tests, and replace stdin through its descriptor without pretending a stream is a terminal.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- MCP serving now exposes per-call `requestId`, authenticated `principal` and cancellation-aware signals over HTTP and stdio. HTTP `auth.authenticate` resolves principals alongside bearer/authorize checks; stdio accepts static or provider principals. `beforeCall` validates policy after input validation and can return a native error refusal or throw a JSON-RPC error. `notifications/cancelled` aborts only the matching caller/request and suppresses its response. New hook/context/refusal and stdio transport types are exported.
|
|
16
|
+
|
|
7
17
|
## 5.32.0 - 2026-10-18
|
|
8
18
|
|
|
9
19
|
### Changed
|
package/cjs/index.cjs
CHANGED
|
@@ -1315,6 +1315,147 @@ const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
1315
1315
|
/** The newest MCP protocol version the server speaks. */
|
|
1316
1316
|
const LATEST_PROTOCOL_VERSION = "2025-11-25";
|
|
1317
1317
|
|
|
1318
|
+
//#endregion
|
|
1319
|
+
//#region ../../packages/ai-tools/src/mcp/result.ts
|
|
1320
|
+
const NATIVE_RESULT = Symbol("mcpResult");
|
|
1321
|
+
/**
|
|
1322
|
+
* Opt in to native MCP content, error status, structured output and metadata.
|
|
1323
|
+
* Validation happens after invoke(), so malformed payloads become JSON-RPC
|
|
1324
|
+
* errors rather than being caught as tool execution errors.
|
|
1325
|
+
*/
|
|
1326
|
+
function mcpResult(result) {
|
|
1327
|
+
return { [NATIVE_RESULT]: result };
|
|
1328
|
+
}
|
|
1329
|
+
/** Read a branded value without accepting lookalike application objects. */
|
|
1330
|
+
function isMcpResult(value) {
|
|
1331
|
+
return isRecord(value) && NATIVE_RESULT in value;
|
|
1332
|
+
}
|
|
1333
|
+
/** Get the original payload; no cloning, normalization or field stripping. */
|
|
1334
|
+
function unwrapMcpResult(value) {
|
|
1335
|
+
return value[NATIVE_RESULT];
|
|
1336
|
+
}
|
|
1337
|
+
/** Objects with result fields are candidates only when native mode is enabled. */
|
|
1338
|
+
function looksLikeMcpResult(value) {
|
|
1339
|
+
return isRecord(value) && [
|
|
1340
|
+
"content",
|
|
1341
|
+
"isError",
|
|
1342
|
+
"structuredContent",
|
|
1343
|
+
"_meta"
|
|
1344
|
+
].some((key) => key in value);
|
|
1345
|
+
}
|
|
1346
|
+
/** Non-array object used by the MCP JSON object fields. */
|
|
1347
|
+
function isRecord(value) {
|
|
1348
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1349
|
+
}
|
|
1350
|
+
function optionalString(value) {
|
|
1351
|
+
return value === void 0 || typeof value === "string";
|
|
1352
|
+
}
|
|
1353
|
+
function validAnnotations(value) {
|
|
1354
|
+
if (value === void 0) return true;
|
|
1355
|
+
if (!isRecord(value)) return false;
|
|
1356
|
+
return (value.audience === void 0 || Array.isArray(value.audience) && value.audience.every((role) => role === "user" || role === "assistant")) && (value.priority === void 0 || typeof value.priority === "number" && value.priority >= 0 && value.priority <= 1) && optionalString(value.lastModified);
|
|
1357
|
+
}
|
|
1358
|
+
function validMetadata(value) {
|
|
1359
|
+
return (value["_meta"] === void 0 || isRecord(value["_meta"])) && validAnnotations(value.annotations);
|
|
1360
|
+
}
|
|
1361
|
+
function validIcon(value) {
|
|
1362
|
+
return isRecord(value) && typeof value.src === "string" && optionalString(value.mimeType) && (value.sizes === void 0 || Array.isArray(value.sizes) && value.sizes.every((size) => typeof size === "string")) && (value.theme === void 0 || value.theme === "light" || value.theme === "dark");
|
|
1363
|
+
}
|
|
1364
|
+
function validContent(value) {
|
|
1365
|
+
if (!isRecord(value) || !validMetadata(value)) return false;
|
|
1366
|
+
switch (value.type) {
|
|
1367
|
+
case "text": return typeof value.text === "string";
|
|
1368
|
+
case "image":
|
|
1369
|
+
case "audio": return typeof value.data === "string" && typeof value.mimeType === "string";
|
|
1370
|
+
case "resource_link": return typeof value.uri === "string" && typeof value.name === "string" && optionalString(value.title) && optionalString(value.description) && optionalString(value.mimeType) && (value.size === void 0 || typeof value.size === "number") && (value.icons === void 0 || Array.isArray(value.icons) && value.icons.every(validIcon));
|
|
1371
|
+
case "resource": {
|
|
1372
|
+
const resource = value.resource;
|
|
1373
|
+
return isRecord(resource) && typeof resource.uri === "string" && optionalString(resource.mimeType) && validMetadata(resource) && (typeof resource.text === "string" || typeof resource.blob === "string");
|
|
1374
|
+
}
|
|
1375
|
+
default: return false;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/** Reject values JSON would drop, coerce or fail to encode (including cycles). */
|
|
1379
|
+
function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
1380
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1381
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1382
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
1383
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1384
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
|
|
1385
|
+
ancestors.add(value);
|
|
1386
|
+
const valid = Array.isArray(value) ? Array.from(value).every((entry) => isJsonValue(entry, ancestors)) : Object.values(value).every((entry) => entry === void 0 || isJsonValue(entry, ancestors));
|
|
1387
|
+
ancestors.delete(value);
|
|
1388
|
+
return valid;
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* MCP 2025-11-25 CallToolResult and ContentBlock fields:
|
|
1392
|
+
* https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result
|
|
1393
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
|
|
1394
|
+
* structuredContent is preserved even when tools/list declares outputSchema.
|
|
1395
|
+
* Tool authors remain responsible for conformity to that application schema.
|
|
1396
|
+
*/
|
|
1397
|
+
function assertMcpResult(value) {
|
|
1398
|
+
if (!isRecord(value) || !Array.isArray(value.content) || !Array.from(value.content).every(validContent) || value.isError !== void 0 && typeof value.isError !== "boolean" || value.structuredContent !== void 0 && !isRecord(value.structuredContent) || value["_meta"] !== void 0 && !isRecord(value["_meta"]) || !isJsonValue(value)) throw new TypeError("Invalid native MCP CallToolResult.");
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
//#endregion
|
|
1402
|
+
//#region ../../packages/ai-tools/src/mcp/serve-requests.ts
|
|
1403
|
+
/**
|
|
1404
|
+
* In-flight requests for one transport. HTTP partitions these by credentials;
|
|
1405
|
+
* stdio has a single connection. Cancellation never crosses those boundaries.
|
|
1406
|
+
* https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
|
|
1407
|
+
* (checked 2026-10-19): ignore unknown/completed ids and never answer cancelled work.
|
|
1408
|
+
*/
|
|
1409
|
+
var McpServeRequests = class {
|
|
1410
|
+
constructor() {
|
|
1411
|
+
this.scopes = /* @__PURE__ */ new Map();
|
|
1412
|
+
}
|
|
1413
|
+
cancel(message, scope = "") {
|
|
1414
|
+
if (!isRecord(message) || message.jsonrpc !== "2.0" || message.method !== "notifications/cancelled" || message.id !== void 0 || !isRecord(message.params)) return;
|
|
1415
|
+
const { requestId, reason } = message.params;
|
|
1416
|
+
if (typeof requestId !== "string" && typeof requestId !== "number" || reason !== void 0 && typeof reason !== "string") return;
|
|
1417
|
+
const pending = this.scopes.get(scope)?.get(requestId);
|
|
1418
|
+
if (pending?.size === 1) {
|
|
1419
|
+
for (const entry of pending) if (entry.cancellable) entry.controller.abort(new DOMException(typeof reason === "string" ? reason : "Request cancelled.", "AbortError"));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
async run(request, context, dispatch, scope = "") {
|
|
1423
|
+
let pending = this.scopes.get(scope);
|
|
1424
|
+
if (!pending) {
|
|
1425
|
+
pending = /* @__PURE__ */ new Map();
|
|
1426
|
+
this.scopes.set(scope, pending);
|
|
1427
|
+
}
|
|
1428
|
+
const controller = new AbortController();
|
|
1429
|
+
const entry = {
|
|
1430
|
+
controller,
|
|
1431
|
+
cancellable: request.method !== "initialize"
|
|
1432
|
+
};
|
|
1433
|
+
const calls = pending.get(request.id) ?? /* @__PURE__ */ new Set();
|
|
1434
|
+
calls.add(entry);
|
|
1435
|
+
pending.set(request.id, calls);
|
|
1436
|
+
const onAbort = () => controller.abort(context.signal?.reason);
|
|
1437
|
+
context.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1438
|
+
if (context.signal?.aborted) onAbort();
|
|
1439
|
+
try {
|
|
1440
|
+
if (controller.signal.aborted) return;
|
|
1441
|
+
const response = await dispatch({
|
|
1442
|
+
...context,
|
|
1443
|
+
requestId: request.id,
|
|
1444
|
+
signal: controller.signal
|
|
1445
|
+
});
|
|
1446
|
+
return controller.signal.aborted ? void 0 : response;
|
|
1447
|
+
} catch (cause) {
|
|
1448
|
+
if (controller.signal.aborted) return;
|
|
1449
|
+
throw cause;
|
|
1450
|
+
} finally {
|
|
1451
|
+
context.signal?.removeEventListener("abort", onAbort);
|
|
1452
|
+
calls.delete(entry);
|
|
1453
|
+
if (calls.size === 0) pending.delete(request.id);
|
|
1454
|
+
if (pending.size === 0) this.scopes.delete(scope);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1318
1459
|
//#endregion
|
|
1319
1460
|
//#region ../../packages/ai-tools/src/mcp/serve-http.ts
|
|
1320
1461
|
/**
|
|
@@ -1413,9 +1554,9 @@ function resolveSettings(transport) {
|
|
|
1413
1554
|
const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
|
|
1414
1555
|
if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
|
|
1415
1556
|
if (auth && auth.bearer !== void 0 && tokens.length === 0) throw new McpServeError("MCP http auth.bearer needs at least one token.", { type: "config" });
|
|
1416
|
-
if (auth && tokens.length === 0 && !auth.authorize) throw new McpServeError("MCP http auth needs `bearer` and/or `
|
|
1417
|
-
const authRequired = tokens.length > 0 || auth?.authorize !== void 0;
|
|
1418
|
-
if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer } or {
|
|
1557
|
+
if (auth && tokens.length === 0 && !auth.authorize && !auth.authenticate) throw new McpServeError("MCP http auth needs `bearer`, `authorize` and/or `authenticate`.", { type: "config" });
|
|
1558
|
+
const authRequired = tokens.length > 0 || auth?.authorize !== void 0 || auth?.authenticate !== void 0;
|
|
1559
|
+
if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer }, { authorize } or { authenticate }), or bind 127.0.0.1.`, {
|
|
1419
1560
|
type: "config",
|
|
1420
1561
|
context: { host }
|
|
1421
1562
|
});
|
|
@@ -1428,6 +1569,7 @@ function resolveSettings(transport) {
|
|
|
1428
1569
|
drainTimeoutMs,
|
|
1429
1570
|
tokenDigests: tokens.map(digest),
|
|
1430
1571
|
authorize: auth?.authorize,
|
|
1572
|
+
authenticate: auth?.authenticate,
|
|
1431
1573
|
authRequired,
|
|
1432
1574
|
allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
|
|
1433
1575
|
allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
|
|
@@ -1486,6 +1628,7 @@ function sendEmpty(response, status, headers = {}) {
|
|
|
1486
1628
|
async function startHttpServer(transport, dispatch) {
|
|
1487
1629
|
const settings = resolveSettings(transport);
|
|
1488
1630
|
const shutdown = new AbortController();
|
|
1631
|
+
const requests = new McpServeRequests();
|
|
1489
1632
|
const inFlight = /* @__PURE__ */ new Set();
|
|
1490
1633
|
let closing;
|
|
1491
1634
|
const server = (0, node_http.createServer)((request, response) => {
|
|
@@ -1493,7 +1636,7 @@ async function startHttpServer(transport, dispatch) {
|
|
|
1493
1636
|
sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
|
|
1494
1637
|
return;
|
|
1495
1638
|
}
|
|
1496
|
-
const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
|
|
1639
|
+
const work = handleRequest(request, response, settings, dispatch, shutdown.signal, requests).catch(() => {
|
|
1497
1640
|
sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
|
|
1498
1641
|
});
|
|
1499
1642
|
inFlight.add(work);
|
|
@@ -1555,7 +1698,7 @@ async function startHttpServer(transport, dispatch) {
|
|
|
1555
1698
|
* Validate and answer one HTTP request: path → Host / Origin → auth →
|
|
1556
1699
|
* method → headers → body → JSON-RPC dispatch.
|
|
1557
1700
|
*/
|
|
1558
|
-
async function handleRequest(request, response, settings, dispatch, signal) {
|
|
1701
|
+
async function handleRequest(request, response, settings, dispatch, signal, requests) {
|
|
1559
1702
|
const headers = flattenHeaders(request.headers);
|
|
1560
1703
|
const method = (request.method ?? "GET").toUpperCase();
|
|
1561
1704
|
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
@@ -1577,8 +1720,9 @@ async function handleRequest(request, response, settings, dispatch, signal) {
|
|
|
1577
1720
|
headers
|
|
1578
1721
|
};
|
|
1579
1722
|
if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
|
|
1580
|
-
|
|
1581
|
-
|
|
1723
|
+
const authentication = await authorized(info, settings);
|
|
1724
|
+
if (!authentication.allowed) {
|
|
1725
|
+
const challenge = settings.tokenDigests.length > 0 || settings.authenticate ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
|
|
1582
1726
|
sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
|
|
1583
1727
|
return;
|
|
1584
1728
|
}
|
|
@@ -1623,6 +1767,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
|
|
|
1623
1767
|
const id = record.id;
|
|
1624
1768
|
if (typeof record.method === "string") {
|
|
1625
1769
|
if (typeof id !== "string" && typeof id !== "number") {
|
|
1770
|
+
requests.cancel(record, cancellationScope(info));
|
|
1626
1771
|
sendEmpty(response, 202);
|
|
1627
1772
|
return;
|
|
1628
1773
|
}
|
|
@@ -1632,10 +1777,20 @@ async function handleRequest(request, response, settings, dispatch, signal) {
|
|
|
1632
1777
|
method: record.method
|
|
1633
1778
|
};
|
|
1634
1779
|
if (record.params !== void 0) rpcRequest.params = record.params;
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1780
|
+
const disconnected = new AbortController();
|
|
1781
|
+
const onClose = () => disconnected.abort();
|
|
1782
|
+
response.once("close", onClose);
|
|
1783
|
+
try {
|
|
1784
|
+
const rpcResponse = await requests.run(rpcRequest, {
|
|
1785
|
+
request: info,
|
|
1786
|
+
principal: authentication.principal,
|
|
1787
|
+
signal: AbortSignal.any([signal, disconnected.signal])
|
|
1788
|
+
}, (context) => dispatch(rpcRequest, context), cancellationScope(info));
|
|
1789
|
+
if (rpcResponse === void 0) sendEmpty(response, 202);
|
|
1790
|
+
else sendJson(response, 200, rpcResponse);
|
|
1791
|
+
} finally {
|
|
1792
|
+
response.off("close", onClose);
|
|
1793
|
+
}
|
|
1639
1794
|
return;
|
|
1640
1795
|
}
|
|
1641
1796
|
if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
|
|
@@ -1674,27 +1829,50 @@ function originAllowed(origin, settings) {
|
|
|
1674
1829
|
if (settings.allowedOrigins.has(parsed.origin)) return true;
|
|
1675
1830
|
return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
|
|
1676
1831
|
}
|
|
1677
|
-
/**
|
|
1678
|
-
* Run the configured auth. Bearer tokens are compared in constant time
|
|
1679
|
-
* over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
|
|
1680
|
-
* every configured token is compared so the match position does not leak.
|
|
1681
|
-
* An `authorize` hook that throws denies the request.
|
|
1682
|
-
*/
|
|
1683
1832
|
async function authorized(info, settings) {
|
|
1684
|
-
if (!settings.authRequired) return true;
|
|
1833
|
+
if (!settings.authRequired) return { allowed: true };
|
|
1685
1834
|
if (settings.tokenDigests.length > 0) {
|
|
1686
1835
|
const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
|
|
1687
1836
|
const presented = digest(match?.[1]?.trim() ?? "");
|
|
1688
1837
|
let matched = false;
|
|
1689
1838
|
for (const expected of settings.tokenDigests) matched = (0, node_crypto.timingSafeEqual)(presented, expected) || matched;
|
|
1690
|
-
if (!match || !matched) return false;
|
|
1839
|
+
if (!match || !matched) return { allowed: false };
|
|
1691
1840
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1841
|
+
try {
|
|
1842
|
+
if (settings.authorize && await settings.authorize(info) !== true) return { allowed: false };
|
|
1843
|
+
if (settings.authenticate) {
|
|
1844
|
+
const principal = await settings.authenticate(info);
|
|
1845
|
+
return principal === void 0 ? { allowed: false } : {
|
|
1846
|
+
allowed: true,
|
|
1847
|
+
principal
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
return { allowed: true };
|
|
1694
1851
|
} catch {
|
|
1695
|
-
return false;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1852
|
+
return { allowed: false };
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Stateless HTTP has no connection/session id. Repeat the caller's credential
|
|
1857
|
+
* and application headers on notifications; per-message transport headers do
|
|
1858
|
+
* not identify a caller. Hash the material rather than retaining secrets as keys.
|
|
1859
|
+
*/
|
|
1860
|
+
function cancellationScope(info) {
|
|
1861
|
+
const transportHeaders = /* @__PURE__ */ new Set([
|
|
1862
|
+
"host",
|
|
1863
|
+
"origin",
|
|
1864
|
+
"accept",
|
|
1865
|
+
"accept-encoding",
|
|
1866
|
+
"accept-language",
|
|
1867
|
+
"connection",
|
|
1868
|
+
"content-type",
|
|
1869
|
+
"content-length",
|
|
1870
|
+
"transfer-encoding",
|
|
1871
|
+
"mcp-protocol-version",
|
|
1872
|
+
"user-agent"
|
|
1873
|
+
]);
|
|
1874
|
+
const credentials = Object.entries(info.headers).filter(([name]) => !transportHeaders.has(name)).sort(([left], [right]) => left.localeCompare(right));
|
|
1875
|
+
return digest(JSON.stringify([info.remoteAddress, credentials])).toString("hex");
|
|
1698
1876
|
}
|
|
1699
1877
|
/** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
|
|
1700
1878
|
function isJsonContentType(contentType) {
|
|
@@ -1751,89 +1929,6 @@ function readBody$1(request, limit) {
|
|
|
1751
1929
|
});
|
|
1752
1930
|
}
|
|
1753
1931
|
|
|
1754
|
-
//#endregion
|
|
1755
|
-
//#region ../../packages/ai-tools/src/mcp/result.ts
|
|
1756
|
-
const NATIVE_RESULT = Symbol("mcpResult");
|
|
1757
|
-
/**
|
|
1758
|
-
* Opt in to native MCP content, error status, structured output and metadata.
|
|
1759
|
-
* Validation happens after invoke(), so malformed payloads become JSON-RPC
|
|
1760
|
-
* errors rather than being caught as tool execution errors.
|
|
1761
|
-
*/
|
|
1762
|
-
function mcpResult(result) {
|
|
1763
|
-
return { [NATIVE_RESULT]: result };
|
|
1764
|
-
}
|
|
1765
|
-
/** Read a branded value without accepting lookalike application objects. */
|
|
1766
|
-
function isMcpResult(value) {
|
|
1767
|
-
return isRecord(value) && NATIVE_RESULT in value;
|
|
1768
|
-
}
|
|
1769
|
-
/** Get the original payload; no cloning, normalization or field stripping. */
|
|
1770
|
-
function unwrapMcpResult(value) {
|
|
1771
|
-
return value[NATIVE_RESULT];
|
|
1772
|
-
}
|
|
1773
|
-
/** Objects with result fields are candidates only when native mode is enabled. */
|
|
1774
|
-
function looksLikeMcpResult(value) {
|
|
1775
|
-
return isRecord(value) && [
|
|
1776
|
-
"content",
|
|
1777
|
-
"isError",
|
|
1778
|
-
"structuredContent",
|
|
1779
|
-
"_meta"
|
|
1780
|
-
].some((key) => key in value);
|
|
1781
|
-
}
|
|
1782
|
-
/** Non-array object used by the MCP JSON object fields. */
|
|
1783
|
-
function isRecord(value) {
|
|
1784
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1785
|
-
}
|
|
1786
|
-
function optionalString(value) {
|
|
1787
|
-
return value === void 0 || typeof value === "string";
|
|
1788
|
-
}
|
|
1789
|
-
function validAnnotations(value) {
|
|
1790
|
-
if (value === void 0) return true;
|
|
1791
|
-
if (!isRecord(value)) return false;
|
|
1792
|
-
return (value.audience === void 0 || Array.isArray(value.audience) && value.audience.every((role) => role === "user" || role === "assistant")) && (value.priority === void 0 || typeof value.priority === "number" && value.priority >= 0 && value.priority <= 1) && optionalString(value.lastModified);
|
|
1793
|
-
}
|
|
1794
|
-
function validMetadata(value) {
|
|
1795
|
-
return (value["_meta"] === void 0 || isRecord(value["_meta"])) && validAnnotations(value.annotations);
|
|
1796
|
-
}
|
|
1797
|
-
function validIcon(value) {
|
|
1798
|
-
return isRecord(value) && typeof value.src === "string" && optionalString(value.mimeType) && (value.sizes === void 0 || Array.isArray(value.sizes) && value.sizes.every((size) => typeof size === "string")) && (value.theme === void 0 || value.theme === "light" || value.theme === "dark");
|
|
1799
|
-
}
|
|
1800
|
-
function validContent(value) {
|
|
1801
|
-
if (!isRecord(value) || !validMetadata(value)) return false;
|
|
1802
|
-
switch (value.type) {
|
|
1803
|
-
case "text": return typeof value.text === "string";
|
|
1804
|
-
case "image":
|
|
1805
|
-
case "audio": return typeof value.data === "string" && typeof value.mimeType === "string";
|
|
1806
|
-
case "resource_link": return typeof value.uri === "string" && typeof value.name === "string" && optionalString(value.title) && optionalString(value.description) && optionalString(value.mimeType) && (value.size === void 0 || typeof value.size === "number") && (value.icons === void 0 || Array.isArray(value.icons) && value.icons.every(validIcon));
|
|
1807
|
-
case "resource": {
|
|
1808
|
-
const resource = value.resource;
|
|
1809
|
-
return isRecord(resource) && typeof resource.uri === "string" && optionalString(resource.mimeType) && validMetadata(resource) && (typeof resource.text === "string" || typeof resource.blob === "string");
|
|
1810
|
-
}
|
|
1811
|
-
default: return false;
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
/** Reject values JSON would drop, coerce or fail to encode (including cycles). */
|
|
1815
|
-
function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
1816
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1817
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
1818
|
-
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
1819
|
-
const prototype = Object.getPrototypeOf(value);
|
|
1820
|
-
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
|
|
1821
|
-
ancestors.add(value);
|
|
1822
|
-
const valid = Array.isArray(value) ? Array.from(value).every((entry) => isJsonValue(entry, ancestors)) : Object.values(value).every((entry) => entry === void 0 || isJsonValue(entry, ancestors));
|
|
1823
|
-
ancestors.delete(value);
|
|
1824
|
-
return valid;
|
|
1825
|
-
}
|
|
1826
|
-
/**
|
|
1827
|
-
* MCP 2025-11-25 CallToolResult and ContentBlock fields:
|
|
1828
|
-
* https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result
|
|
1829
|
-
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts
|
|
1830
|
-
* structuredContent is preserved even when tools/list declares outputSchema.
|
|
1831
|
-
* Tool authors remain responsible for conformity to that application schema.
|
|
1832
|
-
*/
|
|
1833
|
-
function assertMcpResult(value) {
|
|
1834
|
-
if (!isRecord(value) || !Array.isArray(value.content) || !Array.from(value.content).every(validContent) || value.isError !== void 0 && typeof value.isError !== "boolean" || value.structuredContent !== void 0 && !isRecord(value.structuredContent) || value["_meta"] !== void 0 && !isRecord(value["_meta"]) || !isJsonValue(value)) throw new TypeError("Invalid native MCP CallToolResult.");
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
1932
|
//#endregion
|
|
1838
1933
|
//#region ../../packages/ai-tools/src/mcp/serve.ts
|
|
1839
1934
|
/** JSON-RPC version literal every outbound message carries. */
|
|
@@ -1952,7 +2047,10 @@ var McpServeHandler = class {
|
|
|
1952
2047
|
*/
|
|
1953
2048
|
async handle(request, context = {}) {
|
|
1954
2049
|
try {
|
|
1955
|
-
return this.ok(request.id, await this.dispatch(request,
|
|
2050
|
+
return this.ok(request.id, await this.dispatch(request, {
|
|
2051
|
+
...context,
|
|
2052
|
+
requestId: request.id
|
|
2053
|
+
}));
|
|
1956
2054
|
} catch (cause) {
|
|
1957
2055
|
if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
|
|
1958
2056
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -1965,7 +2063,10 @@ var McpServeHandler = class {
|
|
|
1965
2063
|
case "initialize": return this.initializeResult(request.params);
|
|
1966
2064
|
case "ping": return {};
|
|
1967
2065
|
case "tools/list": return { tools: this.listTools() };
|
|
1968
|
-
case "tools/call": return this.callTool(request.params,
|
|
2066
|
+
case "tools/call": return this.callTool(request.params, {
|
|
2067
|
+
...context,
|
|
2068
|
+
requestId: request.id
|
|
2069
|
+
});
|
|
1969
2070
|
}
|
|
1970
2071
|
if (this.resources) switch (request.method) {
|
|
1971
2072
|
case "resources/list": return { resources: this.listResources(this.resources) };
|
|
@@ -2036,7 +2137,11 @@ var McpServeHandler = class {
|
|
|
2036
2137
|
type: "protocol",
|
|
2037
2138
|
method: "tools/call"
|
|
2038
2139
|
});
|
|
2039
|
-
const toolContext = {
|
|
2140
|
+
const toolContext = {
|
|
2141
|
+
artifacts: {},
|
|
2142
|
+
requestId: context.requestId,
|
|
2143
|
+
principal: context.principal
|
|
2144
|
+
};
|
|
2040
2145
|
if (context.signal) toolContext.signal = context.signal;
|
|
2041
2146
|
if (context.request) {
|
|
2042
2147
|
const { headers, remoteAddress } = context.request;
|
|
@@ -2045,6 +2150,43 @@ var McpServeHandler = class {
|
|
|
2045
2150
|
remoteAddress
|
|
2046
2151
|
};
|
|
2047
2152
|
}
|
|
2153
|
+
if (this.options.beforeCall) {
|
|
2154
|
+
let input = args;
|
|
2155
|
+
if (contract.input) {
|
|
2156
|
+
let validation;
|
|
2157
|
+
try {
|
|
2158
|
+
validation = await contract.input["~standard"].validate(args);
|
|
2159
|
+
} catch (cause) {
|
|
2160
|
+
return {
|
|
2161
|
+
content: [{
|
|
2162
|
+
type: "text",
|
|
2163
|
+
text: `Schema validation threw for tool "${name}": ${cause instanceof Error ? cause.message : String(cause)}`
|
|
2164
|
+
}],
|
|
2165
|
+
isError: true
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
if (validation.issues) return {
|
|
2169
|
+
content: [{
|
|
2170
|
+
type: "text",
|
|
2171
|
+
text: `Validation failed: ${validation.issues.map((issue) => issue.message).join("; ")}`
|
|
2172
|
+
}],
|
|
2173
|
+
isError: true
|
|
2174
|
+
};
|
|
2175
|
+
input = validation.value;
|
|
2176
|
+
}
|
|
2177
|
+
context.signal?.throwIfAborted();
|
|
2178
|
+
const refusal = await this.options.beforeCall(context, contract, input);
|
|
2179
|
+
if (refusal !== void 0) {
|
|
2180
|
+
try {
|
|
2181
|
+
assertMcpResult(refusal);
|
|
2182
|
+
if (refusal.isError !== true) throw new Error("A refusal must be an error result.");
|
|
2183
|
+
} catch {
|
|
2184
|
+
throw new RpcFailure(JSON_RPC_INTERNAL_ERROR, "Invalid MCP beforeCall refusal.");
|
|
2185
|
+
}
|
|
2186
|
+
return refusal;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
context.signal?.throwIfAborted();
|
|
2048
2190
|
const result = await contract.invoke(args, toolContext);
|
|
2049
2191
|
if (result.error) return {
|
|
2050
2192
|
content: [{
|
|
@@ -2175,7 +2317,7 @@ const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
|
2175
2317
|
* answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
|
|
2176
2318
|
* Never rejects: a dispatch failure is answered as an internal error.
|
|
2177
2319
|
*/
|
|
2178
|
-
async function answerStdioLine(line, dispatch) {
|
|
2320
|
+
async function answerStdioLine(line, dispatch, notify) {
|
|
2179
2321
|
const trimmed = line.trim();
|
|
2180
2322
|
if (!trimmed) return;
|
|
2181
2323
|
let message;
|
|
@@ -2194,10 +2336,21 @@ async function answerStdioLine(line, dispatch) {
|
|
|
2194
2336
|
})}\n`;
|
|
2195
2337
|
const request = message;
|
|
2196
2338
|
const id = request.id;
|
|
2197
|
-
if (id === void 0 || id === null)
|
|
2339
|
+
if (id === void 0 || id === null) {
|
|
2340
|
+
notify?.(message);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
if (typeof id !== "string" && typeof id !== "number") return `${JSON.stringify({
|
|
2344
|
+
jsonrpc: JSONRPC_VERSION,
|
|
2345
|
+
id: null,
|
|
2346
|
+
error: {
|
|
2347
|
+
code: JSON_RPC_INVALID_REQUEST,
|
|
2348
|
+
message: "Invalid request: id must be a string or number."
|
|
2349
|
+
}
|
|
2350
|
+
})}\n`;
|
|
2198
2351
|
try {
|
|
2199
2352
|
const response = await dispatch(request);
|
|
2200
|
-
return `${JSON.stringify(response)}\n`;
|
|
2353
|
+
return response === void 0 ? void 0 : `${JSON.stringify(response)}\n`;
|
|
2201
2354
|
} catch {
|
|
2202
2355
|
return `${JSON.stringify({
|
|
2203
2356
|
jsonrpc: JSONRPC_VERSION,
|
|
@@ -2281,7 +2434,7 @@ var McpServerImpl = class {
|
|
|
2281
2434
|
start() {
|
|
2282
2435
|
if (!this.running) {
|
|
2283
2436
|
const transport = this.options.transport ?? { type: "stdio" };
|
|
2284
|
-
const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
|
|
2437
|
+
const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio(transport));
|
|
2285
2438
|
this.running = starting.catch((error) => {
|
|
2286
2439
|
this.running = void 0;
|
|
2287
2440
|
throw error;
|
|
@@ -2301,19 +2454,29 @@ var McpServerImpl = class {
|
|
|
2301
2454
|
};
|
|
2302
2455
|
}
|
|
2303
2456
|
/** Wire the stdin line reader and answer each request on stdout. */
|
|
2304
|
-
startStdio() {
|
|
2457
|
+
startStdio(transport) {
|
|
2458
|
+
const shutdown = new AbortController();
|
|
2459
|
+
const requests = new McpServeRequests();
|
|
2305
2460
|
const reader = (0, node_readline.createInterface)({ input: process.stdin });
|
|
2461
|
+
reader.once("close", () => shutdown.abort());
|
|
2306
2462
|
reader.on("line", (line) => {
|
|
2307
|
-
this.onLine(line).catch(() => {});
|
|
2463
|
+
this.onLine(line, transport, requests, shutdown.signal).catch(() => {});
|
|
2308
2464
|
});
|
|
2309
2465
|
return { close: async () => {
|
|
2466
|
+
shutdown.abort();
|
|
2310
2467
|
reader.close();
|
|
2311
2468
|
this.running = void 0;
|
|
2312
2469
|
} };
|
|
2313
2470
|
}
|
|
2314
2471
|
/** Answer one stdin line on stdout (see {@link answerStdioLine}). */
|
|
2315
|
-
async onLine(line) {
|
|
2316
|
-
const reply = await answerStdioLine(line, (request) =>
|
|
2472
|
+
async onLine(line, transport, requests, signal) {
|
|
2473
|
+
const reply = await answerStdioLine(line, (request) => requests.run(request, { signal }, async (context) => {
|
|
2474
|
+
const principal = transport.principalProvider ? await transport.principalProvider(context) : transport.principal;
|
|
2475
|
+
return this.handler.handle(request, {
|
|
2476
|
+
...context,
|
|
2477
|
+
principal
|
|
2478
|
+
});
|
|
2479
|
+
}), (message) => requests.cancel(message));
|
|
2317
2480
|
if (reply !== void 0) process.stdout.write(reply);
|
|
2318
2481
|
}
|
|
2319
2482
|
async stop() {
|