mcpspan 0.2.0 → 0.3.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/dist/index.cjs +105 -4
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +106 -5
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -146,7 +146,7 @@ var EventQueue = class {
|
|
|
146
146
|
* Single source of truth: `package.json` follows this constant, not the other
|
|
147
147
|
* way round, and a unit test fails if the two ever drift apart.
|
|
148
148
|
*/
|
|
149
|
-
const SDK_VERSION = "0.
|
|
149
|
+
const SDK_VERSION = "0.3.0";
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/transport.ts
|
|
152
152
|
/** Path the ingest API accepts batches on, appended to the configured endpoint. */
|
|
@@ -498,6 +498,88 @@ function currentCall() {
|
|
|
498
498
|
return current;
|
|
499
499
|
}
|
|
500
500
|
//#endregion
|
|
501
|
+
//#region src/definition.ts
|
|
502
|
+
/**
|
|
503
|
+
* Tool definitions as the server lists them, fingerprinted (contract, 3.8).
|
|
504
|
+
*
|
|
505
|
+
* Rewording a description can change how agents use a tool more than a change
|
|
506
|
+
* to its code. The fingerprint is taken from the answer to `tools/list`, what
|
|
507
|
+
* an agent actually read, and sent with every call to the tool, so the
|
|
508
|
+
* dashboard can mark when a definition changed. Kept for the process: one
|
|
509
|
+
* process reports to one server, and a listing on one connection describes
|
|
510
|
+
* the same tools as on any other.
|
|
511
|
+
*/
|
|
512
|
+
const listed = /* @__PURE__ */ new Map();
|
|
513
|
+
/** The latest fingerprint listed for a tool, if any listing in this process named it. */
|
|
514
|
+
function definitionOf(toolName) {
|
|
515
|
+
return listed.get(toolName);
|
|
516
|
+
}
|
|
517
|
+
/** Notes every tool in an answer to `tools/list`. Never throws. */
|
|
518
|
+
function noteListing(result) {
|
|
519
|
+
try {
|
|
520
|
+
const tools = result?.tools;
|
|
521
|
+
if (!Array.isArray(tools)) return;
|
|
522
|
+
for (const tool of tools) {
|
|
523
|
+
const name = tool?.name;
|
|
524
|
+
if (typeof name !== "string") continue;
|
|
525
|
+
const hash = definitionHash(tool);
|
|
526
|
+
if (hash !== void 0) listed.set(name, hash);
|
|
527
|
+
}
|
|
528
|
+
} catch {}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* The first 16 hex characters of the SHA-256 of the tool's name, title,
|
|
532
|
+
* description and input schema, as canonical JSON. Undefined for a definition
|
|
533
|
+
* that cannot be written so, which is then sent without one.
|
|
534
|
+
*/
|
|
535
|
+
function definitionHash(tool) {
|
|
536
|
+
const hashed = {};
|
|
537
|
+
for (const field of [
|
|
538
|
+
"name",
|
|
539
|
+
"title",
|
|
540
|
+
"description",
|
|
541
|
+
"inputSchema"
|
|
542
|
+
]) if (tool[field] !== void 0) hashed[field] = tool[field];
|
|
543
|
+
try {
|
|
544
|
+
return (0, node_crypto.createHash)("sha256").update(canonical(hashed), "utf8").digest("hex").slice(0, 16);
|
|
545
|
+
} catch {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/** Sorted keys, no whitespace, minimal escaping: the same text in every SDK. */
|
|
550
|
+
function canonical(value) {
|
|
551
|
+
if (value === null) return "null";
|
|
552
|
+
if (typeof value === "boolean") return String(value);
|
|
553
|
+
if (typeof value === "number") {
|
|
554
|
+
if (!Number.isFinite(value)) throw new TypeError("not a JSON number");
|
|
555
|
+
return String(value);
|
|
556
|
+
}
|
|
557
|
+
if (typeof value === "string") return text(value);
|
|
558
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
559
|
+
if (typeof value === "object") {
|
|
560
|
+
const object = value;
|
|
561
|
+
return `{${Object.keys(object).filter((key) => object[key] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((key) => `${text(key)}:${canonical(object[key])}`).join(",")}}`;
|
|
562
|
+
}
|
|
563
|
+
throw new TypeError(`cannot fingerprint ${typeof value}`);
|
|
564
|
+
}
|
|
565
|
+
const ESCAPES = {
|
|
566
|
+
"\"": "\\\"",
|
|
567
|
+
"\\": "\\\\",
|
|
568
|
+
"\b": "\\b",
|
|
569
|
+
"\f": "\\f",
|
|
570
|
+
"\n": "\\n",
|
|
571
|
+
"\r": "\\r",
|
|
572
|
+
" ": "\\t"
|
|
573
|
+
};
|
|
574
|
+
function text(value) {
|
|
575
|
+
let out = "\"";
|
|
576
|
+
for (const character of value) {
|
|
577
|
+
const code = character.codePointAt(0) ?? 0;
|
|
578
|
+
out += ESCAPES[character] ?? (code < 32 ? `\\u${code.toString(16).padStart(4, "0")}` : character);
|
|
579
|
+
}
|
|
580
|
+
return `${out}"`;
|
|
581
|
+
}
|
|
582
|
+
//#endregion
|
|
501
583
|
//#region src/client.ts
|
|
502
584
|
/**
|
|
503
585
|
* Names we recognise, matched as substrings of what a client reports.
|
|
@@ -773,6 +855,7 @@ function track(toolName, handler) {
|
|
|
773
855
|
timestamp,
|
|
774
856
|
sdkVersion: SDK_VERSION,
|
|
775
857
|
...session !== void 0 && { sessionId: session },
|
|
858
|
+
...definition(toolName),
|
|
776
859
|
...outcome
|
|
777
860
|
});
|
|
778
861
|
} catch {}
|
|
@@ -880,7 +963,8 @@ function recordRefusedCall(refused) {
|
|
|
880
963
|
...parameters !== void 0 && { parameters },
|
|
881
964
|
timestamp: refused.timestamp,
|
|
882
965
|
sdkVersion: SDK_VERSION,
|
|
883
|
-
...refused.sessionId !== void 0 && { sessionId: refused.sessionId }
|
|
966
|
+
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
967
|
+
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName)
|
|
884
968
|
});
|
|
885
969
|
} catch {}
|
|
886
970
|
}
|
|
@@ -918,6 +1002,11 @@ function recordPrimitiveCall(call) {
|
|
|
918
1002
|
function isRecording() {
|
|
919
1003
|
return sink !== void 0;
|
|
920
1004
|
}
|
|
1005
|
+
/** The fingerprint of a tool as last listed (contract, 3.8), as event fields. */
|
|
1006
|
+
function definition(toolName) {
|
|
1007
|
+
const hash = definitionOf(toolName);
|
|
1008
|
+
return hash === void 0 ? {} : { definitionHash: hash };
|
|
1009
|
+
}
|
|
921
1010
|
/** The largest size an event carries; anything larger is sent as this (contract, 3.7). */
|
|
922
1011
|
const MAX_RESPONSE_BYTES = 2147483647;
|
|
923
1012
|
/**
|
|
@@ -1495,7 +1584,11 @@ function interceptToolCalls(server, registry) {
|
|
|
1495
1584
|
if (intercepted.has(inner)) return;
|
|
1496
1585
|
intercepted.add(inner);
|
|
1497
1586
|
const handlers = inner._requestHandlers;
|
|
1498
|
-
if (handlers instanceof Map) for (const method of [
|
|
1587
|
+
if (handlers instanceof Map) for (const method of [
|
|
1588
|
+
"tools/call",
|
|
1589
|
+
"tools/list",
|
|
1590
|
+
...PRIMITIVE_METHODS
|
|
1591
|
+
]) {
|
|
1499
1592
|
const installed = handlers.get(method);
|
|
1500
1593
|
if (typeof installed === "function") handlers.set(method, watch(installed, server, registry));
|
|
1501
1594
|
}
|
|
@@ -1513,7 +1606,15 @@ function interceptToolCalls(server, registry) {
|
|
|
1513
1606
|
* so the handler for any other method runs exactly as it did.
|
|
1514
1607
|
*/
|
|
1515
1608
|
function watch(handler, server, registry) {
|
|
1516
|
-
return watchPrimitive(watchToolCalls(handler, server, registry), server);
|
|
1609
|
+
return watchListing(watchPrimitive(watchToolCalls(handler, server, registry), server));
|
|
1610
|
+
}
|
|
1611
|
+
/** Notes the tools a `tools/list` answer describes, for the fingerprint each call carries (contract, 3.8). */
|
|
1612
|
+
function watchListing(handler) {
|
|
1613
|
+
return async function watchedListing(request, ...rest) {
|
|
1614
|
+
const result = await handler.call(this, request, ...rest);
|
|
1615
|
+
if (request?.method === "tools/list" && isRecording()) noteListing(result);
|
|
1616
|
+
return result;
|
|
1617
|
+
};
|
|
1517
1618
|
}
|
|
1518
1619
|
function watchToolCalls(handler, server, registry) {
|
|
1519
1620
|
return async function watchedRequestHandler(request, context) {
|
package/dist/index.d.cts
CHANGED
|
@@ -199,6 +199,8 @@ interface ToolCallEvent {
|
|
|
199
199
|
* call returned one; the content is counted, never kept.
|
|
200
200
|
*/
|
|
201
201
|
responseBytes?: number;
|
|
202
|
+
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
|
+
definitionHash?: string;
|
|
202
204
|
/** When the call started, as an ISO 8601 timestamp. */
|
|
203
205
|
timestamp: string;
|
|
204
206
|
/** Version of the mcpspan package that produced the event. */
|
package/dist/index.d.mts
CHANGED
|
@@ -199,6 +199,8 @@ interface ToolCallEvent {
|
|
|
199
199
|
* call returned one; the content is counted, never kept.
|
|
200
200
|
*/
|
|
201
201
|
responseBytes?: number;
|
|
202
|
+
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
|
+
definitionHash?: string;
|
|
202
204
|
/** When the call started, as an ISO 8601 timestamp. */
|
|
203
205
|
timestamp: string;
|
|
204
206
|
/** Version of the mcpspan package that produced the event. */
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
/** Renders any thrown value as one readable line, for diagnostics. */
|
|
3
3
|
function formatError(error) {
|
|
4
4
|
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
@@ -145,7 +145,7 @@ var EventQueue = class {
|
|
|
145
145
|
* Single source of truth: `package.json` follows this constant, not the other
|
|
146
146
|
* way round, and a unit test fails if the two ever drift apart.
|
|
147
147
|
*/
|
|
148
|
-
const SDK_VERSION = "0.
|
|
148
|
+
const SDK_VERSION = "0.3.0";
|
|
149
149
|
//#endregion
|
|
150
150
|
//#region src/transport.ts
|
|
151
151
|
/** Path the ingest API accepts batches on, appended to the configured endpoint. */
|
|
@@ -497,6 +497,88 @@ function currentCall() {
|
|
|
497
497
|
return current;
|
|
498
498
|
}
|
|
499
499
|
//#endregion
|
|
500
|
+
//#region src/definition.ts
|
|
501
|
+
/**
|
|
502
|
+
* Tool definitions as the server lists them, fingerprinted (contract, 3.8).
|
|
503
|
+
*
|
|
504
|
+
* Rewording a description can change how agents use a tool more than a change
|
|
505
|
+
* to its code. The fingerprint is taken from the answer to `tools/list`, what
|
|
506
|
+
* an agent actually read, and sent with every call to the tool, so the
|
|
507
|
+
* dashboard can mark when a definition changed. Kept for the process: one
|
|
508
|
+
* process reports to one server, and a listing on one connection describes
|
|
509
|
+
* the same tools as on any other.
|
|
510
|
+
*/
|
|
511
|
+
const listed = /* @__PURE__ */ new Map();
|
|
512
|
+
/** The latest fingerprint listed for a tool, if any listing in this process named it. */
|
|
513
|
+
function definitionOf(toolName) {
|
|
514
|
+
return listed.get(toolName);
|
|
515
|
+
}
|
|
516
|
+
/** Notes every tool in an answer to `tools/list`. Never throws. */
|
|
517
|
+
function noteListing(result) {
|
|
518
|
+
try {
|
|
519
|
+
const tools = result?.tools;
|
|
520
|
+
if (!Array.isArray(tools)) return;
|
|
521
|
+
for (const tool of tools) {
|
|
522
|
+
const name = tool?.name;
|
|
523
|
+
if (typeof name !== "string") continue;
|
|
524
|
+
const hash = definitionHash(tool);
|
|
525
|
+
if (hash !== void 0) listed.set(name, hash);
|
|
526
|
+
}
|
|
527
|
+
} catch {}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* The first 16 hex characters of the SHA-256 of the tool's name, title,
|
|
531
|
+
* description and input schema, as canonical JSON. Undefined for a definition
|
|
532
|
+
* that cannot be written so, which is then sent without one.
|
|
533
|
+
*/
|
|
534
|
+
function definitionHash(tool) {
|
|
535
|
+
const hashed = {};
|
|
536
|
+
for (const field of [
|
|
537
|
+
"name",
|
|
538
|
+
"title",
|
|
539
|
+
"description",
|
|
540
|
+
"inputSchema"
|
|
541
|
+
]) if (tool[field] !== void 0) hashed[field] = tool[field];
|
|
542
|
+
try {
|
|
543
|
+
return createHash("sha256").update(canonical(hashed), "utf8").digest("hex").slice(0, 16);
|
|
544
|
+
} catch {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/** Sorted keys, no whitespace, minimal escaping: the same text in every SDK. */
|
|
549
|
+
function canonical(value) {
|
|
550
|
+
if (value === null) return "null";
|
|
551
|
+
if (typeof value === "boolean") return String(value);
|
|
552
|
+
if (typeof value === "number") {
|
|
553
|
+
if (!Number.isFinite(value)) throw new TypeError("not a JSON number");
|
|
554
|
+
return String(value);
|
|
555
|
+
}
|
|
556
|
+
if (typeof value === "string") return text(value);
|
|
557
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
558
|
+
if (typeof value === "object") {
|
|
559
|
+
const object = value;
|
|
560
|
+
return `{${Object.keys(object).filter((key) => object[key] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((key) => `${text(key)}:${canonical(object[key])}`).join(",")}}`;
|
|
561
|
+
}
|
|
562
|
+
throw new TypeError(`cannot fingerprint ${typeof value}`);
|
|
563
|
+
}
|
|
564
|
+
const ESCAPES = {
|
|
565
|
+
"\"": "\\\"",
|
|
566
|
+
"\\": "\\\\",
|
|
567
|
+
"\b": "\\b",
|
|
568
|
+
"\f": "\\f",
|
|
569
|
+
"\n": "\\n",
|
|
570
|
+
"\r": "\\r",
|
|
571
|
+
" ": "\\t"
|
|
572
|
+
};
|
|
573
|
+
function text(value) {
|
|
574
|
+
let out = "\"";
|
|
575
|
+
for (const character of value) {
|
|
576
|
+
const code = character.codePointAt(0) ?? 0;
|
|
577
|
+
out += ESCAPES[character] ?? (code < 32 ? `\\u${code.toString(16).padStart(4, "0")}` : character);
|
|
578
|
+
}
|
|
579
|
+
return `${out}"`;
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
500
582
|
//#region src/client.ts
|
|
501
583
|
/**
|
|
502
584
|
* Names we recognise, matched as substrings of what a client reports.
|
|
@@ -772,6 +854,7 @@ function track(toolName, handler) {
|
|
|
772
854
|
timestamp,
|
|
773
855
|
sdkVersion: SDK_VERSION,
|
|
774
856
|
...session !== void 0 && { sessionId: session },
|
|
857
|
+
...definition(toolName),
|
|
775
858
|
...outcome
|
|
776
859
|
});
|
|
777
860
|
} catch {}
|
|
@@ -879,7 +962,8 @@ function recordRefusedCall(refused) {
|
|
|
879
962
|
...parameters !== void 0 && { parameters },
|
|
880
963
|
timestamp: refused.timestamp,
|
|
881
964
|
sdkVersion: SDK_VERSION,
|
|
882
|
-
...refused.sessionId !== void 0 && { sessionId: refused.sessionId }
|
|
965
|
+
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
966
|
+
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName)
|
|
883
967
|
});
|
|
884
968
|
} catch {}
|
|
885
969
|
}
|
|
@@ -917,6 +1001,11 @@ function recordPrimitiveCall(call) {
|
|
|
917
1001
|
function isRecording() {
|
|
918
1002
|
return sink !== void 0;
|
|
919
1003
|
}
|
|
1004
|
+
/** The fingerprint of a tool as last listed (contract, 3.8), as event fields. */
|
|
1005
|
+
function definition(toolName) {
|
|
1006
|
+
const hash = definitionOf(toolName);
|
|
1007
|
+
return hash === void 0 ? {} : { definitionHash: hash };
|
|
1008
|
+
}
|
|
920
1009
|
/** The largest size an event carries; anything larger is sent as this (contract, 3.7). */
|
|
921
1010
|
const MAX_RESPONSE_BYTES = 2147483647;
|
|
922
1011
|
/**
|
|
@@ -1494,7 +1583,11 @@ function interceptToolCalls(server, registry) {
|
|
|
1494
1583
|
if (intercepted.has(inner)) return;
|
|
1495
1584
|
intercepted.add(inner);
|
|
1496
1585
|
const handlers = inner._requestHandlers;
|
|
1497
|
-
if (handlers instanceof Map) for (const method of [
|
|
1586
|
+
if (handlers instanceof Map) for (const method of [
|
|
1587
|
+
"tools/call",
|
|
1588
|
+
"tools/list",
|
|
1589
|
+
...PRIMITIVE_METHODS
|
|
1590
|
+
]) {
|
|
1498
1591
|
const installed = handlers.get(method);
|
|
1499
1592
|
if (typeof installed === "function") handlers.set(method, watch(installed, server, registry));
|
|
1500
1593
|
}
|
|
@@ -1512,7 +1605,15 @@ function interceptToolCalls(server, registry) {
|
|
|
1512
1605
|
* so the handler for any other method runs exactly as it did.
|
|
1513
1606
|
*/
|
|
1514
1607
|
function watch(handler, server, registry) {
|
|
1515
|
-
return watchPrimitive(watchToolCalls(handler, server, registry), server);
|
|
1608
|
+
return watchListing(watchPrimitive(watchToolCalls(handler, server, registry), server));
|
|
1609
|
+
}
|
|
1610
|
+
/** Notes the tools a `tools/list` answer describes, for the fingerprint each call carries (contract, 3.8). */
|
|
1611
|
+
function watchListing(handler) {
|
|
1612
|
+
return async function watchedListing(request, ...rest) {
|
|
1613
|
+
const result = await handler.call(this, request, ...rest);
|
|
1614
|
+
if (request?.method === "tools/list" && isRecording()) noteListing(result);
|
|
1615
|
+
return result;
|
|
1616
|
+
};
|
|
1516
1617
|
}
|
|
1517
1618
|
function watchToolCalls(handler, server, registry) {
|
|
1518
1619
|
return async function watchedRequestHandler(request, context) {
|
package/package.json
CHANGED