mcpspan 0.1.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/README.md +3 -2
- package/dist/index.cjs +136 -9
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +137 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,8 +208,9 @@ out of this as well.
|
|
|
208
208
|
mode, not in debug.
|
|
209
209
|
|
|
210
210
|
What is collected: the tool name, how long it took, whether it succeeded, the
|
|
211
|
-
error type and a truncated message when it did not,
|
|
212
|
-
|
|
211
|
+
error type and a truncated message when it did not, how large the answer was
|
|
212
|
+
in bytes (its size only, never its content), which client called, and the SDK
|
|
213
|
+
version. For a resource or a prompt, the same, under the name it was
|
|
213
214
|
registered with: never the address a client read, only its template or, for
|
|
214
215
|
an address the server does not have, its scheme.
|
|
215
216
|
|
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 {}
|
|
@@ -782,15 +865,21 @@ function track(toolName, handler) {
|
|
|
782
865
|
recorded = true;
|
|
783
866
|
return;
|
|
784
867
|
}
|
|
868
|
+
const size = responseBytes(value);
|
|
869
|
+
const measured = size === void 0 ? {} : { responseBytes: size };
|
|
785
870
|
if (!isErrorResult(value)) {
|
|
786
|
-
emit({
|
|
871
|
+
emit({
|
|
872
|
+
success: true,
|
|
873
|
+
...measured
|
|
874
|
+
});
|
|
787
875
|
return;
|
|
788
876
|
}
|
|
789
877
|
const errorMessage = describeErrorResult(value);
|
|
790
878
|
emit({
|
|
791
879
|
success: false,
|
|
792
880
|
errorSource: "result",
|
|
793
|
-
...errorMessage !== void 0 && { errorMessage }
|
|
881
|
+
...errorMessage !== void 0 && { errorMessage },
|
|
882
|
+
...measured
|
|
794
883
|
});
|
|
795
884
|
};
|
|
796
885
|
const fail = (error) => {
|
|
@@ -874,7 +963,8 @@ function recordRefusedCall(refused) {
|
|
|
874
963
|
...parameters !== void 0 && { parameters },
|
|
875
964
|
timestamp: refused.timestamp,
|
|
876
965
|
sdkVersion: SDK_VERSION,
|
|
877
|
-
...refused.sessionId !== void 0 && { sessionId: refused.sessionId }
|
|
966
|
+
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
967
|
+
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName)
|
|
878
968
|
});
|
|
879
969
|
} catch {}
|
|
880
970
|
}
|
|
@@ -903,7 +993,8 @@ function recordPrimitiveCall(call) {
|
|
|
903
993
|
...parameters !== void 0 && { parameters },
|
|
904
994
|
timestamp: call.timestamp,
|
|
905
995
|
sdkVersion: SDK_VERSION,
|
|
906
|
-
...call.sessionId !== void 0 && { sessionId: call.sessionId }
|
|
996
|
+
...call.sessionId !== void 0 && { sessionId: call.sessionId },
|
|
997
|
+
...call.responseBytes !== void 0 && { responseBytes: call.responseBytes }
|
|
907
998
|
});
|
|
908
999
|
} catch {}
|
|
909
1000
|
}
|
|
@@ -911,6 +1002,26 @@ function recordPrimitiveCall(call) {
|
|
|
911
1002
|
function isRecording() {
|
|
912
1003
|
return sink !== void 0;
|
|
913
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
|
+
}
|
|
1010
|
+
/** The largest size an event carries; anything larger is sent as this (contract, 3.7). */
|
|
1011
|
+
const MAX_RESPONSE_BYTES = 2147483647;
|
|
1012
|
+
/**
|
|
1013
|
+
* Size of an answer, in bytes of its compact JSON (contract, 3.7), or undefined
|
|
1014
|
+
* when it cannot be encoded. The JSON is counted and dropped: nothing of it is
|
|
1015
|
+
* kept or sent.
|
|
1016
|
+
*/
|
|
1017
|
+
function responseBytes(value) {
|
|
1018
|
+
try {
|
|
1019
|
+
const json = JSON.stringify(value);
|
|
1020
|
+
return json === void 0 ? void 0 : Math.min(Buffer.byteLength(json, "utf8"), MAX_RESPONSE_BYTES);
|
|
1021
|
+
} catch {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
914
1025
|
/** A result the 2026-07-28 protocol calls interim: the tool needs more input first. */
|
|
915
1026
|
function isInputRequired(value) {
|
|
916
1027
|
return typeof value === "object" && value !== null && value.resultType === "input_required";
|
|
@@ -1226,7 +1337,8 @@ function watchPrimitive(handler, server) {
|
|
|
1226
1337
|
durationMs: performance.now() - startedAt,
|
|
1227
1338
|
sessionId: typeof context === "object" && context !== null ? sessionFor(server, context) : void 0,
|
|
1228
1339
|
client: clientFor(server, typeof context === "object" && context !== null ? context : void 0),
|
|
1229
|
-
serverVersion: serverVersionOf(server)
|
|
1340
|
+
serverVersion: serverVersionOf(server),
|
|
1341
|
+
responseBytes: outcome.responseBytes
|
|
1230
1342
|
});
|
|
1231
1343
|
} catch {}
|
|
1232
1344
|
};
|
|
@@ -1249,7 +1361,10 @@ function watchPrimitive(handler, server) {
|
|
|
1249
1361
|
});
|
|
1250
1362
|
throw error;
|
|
1251
1363
|
}
|
|
1252
|
-
if (!(typeof result === "object" && result !== null && result.resultType === "input_required")) record({
|
|
1364
|
+
if (!(typeof result === "object" && result !== null && result.resultType === "input_required")) record({
|
|
1365
|
+
success: true,
|
|
1366
|
+
responseBytes: responseBytes(result)
|
|
1367
|
+
});
|
|
1253
1368
|
return result;
|
|
1254
1369
|
};
|
|
1255
1370
|
}
|
|
@@ -1469,7 +1584,11 @@ function interceptToolCalls(server, registry) {
|
|
|
1469
1584
|
if (intercepted.has(inner)) return;
|
|
1470
1585
|
intercepted.add(inner);
|
|
1471
1586
|
const handlers = inner._requestHandlers;
|
|
1472
|
-
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
|
+
]) {
|
|
1473
1592
|
const installed = handlers.get(method);
|
|
1474
1593
|
if (typeof installed === "function") handlers.set(method, watch(installed, server, registry));
|
|
1475
1594
|
}
|
|
@@ -1487,7 +1606,15 @@ function interceptToolCalls(server, registry) {
|
|
|
1487
1606
|
* so the handler for any other method runs exactly as it did.
|
|
1488
1607
|
*/
|
|
1489
1608
|
function watch(handler, server, registry) {
|
|
1490
|
-
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
|
+
};
|
|
1491
1618
|
}
|
|
1492
1619
|
function watchToolCalls(handler, server, registry) {
|
|
1493
1620
|
return async function watchedRequestHandler(request, context) {
|
package/dist/index.d.cts
CHANGED
|
@@ -194,6 +194,13 @@ interface ToolCallEvent {
|
|
|
194
194
|
* the version the MCP server gives itself.
|
|
195
195
|
*/
|
|
196
196
|
serverVersion?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Size of the answer, in bytes of compact JSON (contract, 3.7). Only when the
|
|
199
|
+
* call returned one; the content is counted, never kept.
|
|
200
|
+
*/
|
|
201
|
+
responseBytes?: number;
|
|
202
|
+
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
|
+
definitionHash?: string;
|
|
197
204
|
/** When the call started, as an ISO 8601 timestamp. */
|
|
198
205
|
timestamp: string;
|
|
199
206
|
/** Version of the mcpspan package that produced the event. */
|
package/dist/index.d.mts
CHANGED
|
@@ -194,6 +194,13 @@ interface ToolCallEvent {
|
|
|
194
194
|
* the version the MCP server gives itself.
|
|
195
195
|
*/
|
|
196
196
|
serverVersion?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Size of the answer, in bytes of compact JSON (contract, 3.7). Only when the
|
|
199
|
+
* call returned one; the content is counted, never kept.
|
|
200
|
+
*/
|
|
201
|
+
responseBytes?: number;
|
|
202
|
+
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
|
+
definitionHash?: string;
|
|
197
204
|
/** When the call started, as an ISO 8601 timestamp. */
|
|
198
205
|
timestamp: string;
|
|
199
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 {}
|
|
@@ -781,15 +864,21 @@ function track(toolName, handler) {
|
|
|
781
864
|
recorded = true;
|
|
782
865
|
return;
|
|
783
866
|
}
|
|
867
|
+
const size = responseBytes(value);
|
|
868
|
+
const measured = size === void 0 ? {} : { responseBytes: size };
|
|
784
869
|
if (!isErrorResult(value)) {
|
|
785
|
-
emit({
|
|
870
|
+
emit({
|
|
871
|
+
success: true,
|
|
872
|
+
...measured
|
|
873
|
+
});
|
|
786
874
|
return;
|
|
787
875
|
}
|
|
788
876
|
const errorMessage = describeErrorResult(value);
|
|
789
877
|
emit({
|
|
790
878
|
success: false,
|
|
791
879
|
errorSource: "result",
|
|
792
|
-
...errorMessage !== void 0 && { errorMessage }
|
|
880
|
+
...errorMessage !== void 0 && { errorMessage },
|
|
881
|
+
...measured
|
|
793
882
|
});
|
|
794
883
|
};
|
|
795
884
|
const fail = (error) => {
|
|
@@ -873,7 +962,8 @@ function recordRefusedCall(refused) {
|
|
|
873
962
|
...parameters !== void 0 && { parameters },
|
|
874
963
|
timestamp: refused.timestamp,
|
|
875
964
|
sdkVersion: SDK_VERSION,
|
|
876
|
-
...refused.sessionId !== void 0 && { sessionId: refused.sessionId }
|
|
965
|
+
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
966
|
+
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName)
|
|
877
967
|
});
|
|
878
968
|
} catch {}
|
|
879
969
|
}
|
|
@@ -902,7 +992,8 @@ function recordPrimitiveCall(call) {
|
|
|
902
992
|
...parameters !== void 0 && { parameters },
|
|
903
993
|
timestamp: call.timestamp,
|
|
904
994
|
sdkVersion: SDK_VERSION,
|
|
905
|
-
...call.sessionId !== void 0 && { sessionId: call.sessionId }
|
|
995
|
+
...call.sessionId !== void 0 && { sessionId: call.sessionId },
|
|
996
|
+
...call.responseBytes !== void 0 && { responseBytes: call.responseBytes }
|
|
906
997
|
});
|
|
907
998
|
} catch {}
|
|
908
999
|
}
|
|
@@ -910,6 +1001,26 @@ function recordPrimitiveCall(call) {
|
|
|
910
1001
|
function isRecording() {
|
|
911
1002
|
return sink !== void 0;
|
|
912
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
|
+
}
|
|
1009
|
+
/** The largest size an event carries; anything larger is sent as this (contract, 3.7). */
|
|
1010
|
+
const MAX_RESPONSE_BYTES = 2147483647;
|
|
1011
|
+
/**
|
|
1012
|
+
* Size of an answer, in bytes of its compact JSON (contract, 3.7), or undefined
|
|
1013
|
+
* when it cannot be encoded. The JSON is counted and dropped: nothing of it is
|
|
1014
|
+
* kept or sent.
|
|
1015
|
+
*/
|
|
1016
|
+
function responseBytes(value) {
|
|
1017
|
+
try {
|
|
1018
|
+
const json = JSON.stringify(value);
|
|
1019
|
+
return json === void 0 ? void 0 : Math.min(Buffer.byteLength(json, "utf8"), MAX_RESPONSE_BYTES);
|
|
1020
|
+
} catch {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
913
1024
|
/** A result the 2026-07-28 protocol calls interim: the tool needs more input first. */
|
|
914
1025
|
function isInputRequired(value) {
|
|
915
1026
|
return typeof value === "object" && value !== null && value.resultType === "input_required";
|
|
@@ -1225,7 +1336,8 @@ function watchPrimitive(handler, server) {
|
|
|
1225
1336
|
durationMs: performance.now() - startedAt,
|
|
1226
1337
|
sessionId: typeof context === "object" && context !== null ? sessionFor(server, context) : void 0,
|
|
1227
1338
|
client: clientFor(server, typeof context === "object" && context !== null ? context : void 0),
|
|
1228
|
-
serverVersion: serverVersionOf(server)
|
|
1339
|
+
serverVersion: serverVersionOf(server),
|
|
1340
|
+
responseBytes: outcome.responseBytes
|
|
1229
1341
|
});
|
|
1230
1342
|
} catch {}
|
|
1231
1343
|
};
|
|
@@ -1248,7 +1360,10 @@ function watchPrimitive(handler, server) {
|
|
|
1248
1360
|
});
|
|
1249
1361
|
throw error;
|
|
1250
1362
|
}
|
|
1251
|
-
if (!(typeof result === "object" && result !== null && result.resultType === "input_required")) record({
|
|
1363
|
+
if (!(typeof result === "object" && result !== null && result.resultType === "input_required")) record({
|
|
1364
|
+
success: true,
|
|
1365
|
+
responseBytes: responseBytes(result)
|
|
1366
|
+
});
|
|
1252
1367
|
return result;
|
|
1253
1368
|
};
|
|
1254
1369
|
}
|
|
@@ -1468,7 +1583,11 @@ function interceptToolCalls(server, registry) {
|
|
|
1468
1583
|
if (intercepted.has(inner)) return;
|
|
1469
1584
|
intercepted.add(inner);
|
|
1470
1585
|
const handlers = inner._requestHandlers;
|
|
1471
|
-
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
|
+
]) {
|
|
1472
1591
|
const installed = handlers.get(method);
|
|
1473
1592
|
if (typeof installed === "function") handlers.set(method, watch(installed, server, registry));
|
|
1474
1593
|
}
|
|
@@ -1486,7 +1605,15 @@ function interceptToolCalls(server, registry) {
|
|
|
1486
1605
|
* so the handler for any other method runs exactly as it did.
|
|
1487
1606
|
*/
|
|
1488
1607
|
function watch(handler, server, registry) {
|
|
1489
|
-
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
|
+
};
|
|
1490
1617
|
}
|
|
1491
1618
|
function watchToolCalls(handler, server, registry) {
|
|
1492
1619
|
return async function watchedRequestHandler(request, context) {
|
package/package.json
CHANGED