mcpspan 0.3.0 → 0.4.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 +4 -2
- package/dist/index.cjs +69 -6
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +69 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -209,8 +209,10 @@ mode, not in debug.
|
|
|
209
209
|
|
|
210
210
|
What is collected: the tool name, how long it took, whether it succeeded, the
|
|
211
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),
|
|
213
|
-
|
|
212
|
+
in bytes (its size only, never its content), whether it repeated the previous
|
|
213
|
+
call's arguments to the same tool in its session (compared in your process;
|
|
214
|
+
the arguments, or any digest of them, never leave it), which client called,
|
|
215
|
+
and the SDK version. For a resource or a prompt, the same, under the name it was
|
|
214
216
|
registered with: never the address a client read, only its template or, for
|
|
215
217
|
an address the server does not have, its scheme.
|
|
216
218
|
|
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.4.0";
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/transport.ts
|
|
152
152
|
/** Path the ingest API accepts batches on, appended to the configured endpoint. */
|
|
@@ -546,7 +546,7 @@ function definitionHash(tool) {
|
|
|
546
546
|
return;
|
|
547
547
|
}
|
|
548
548
|
}
|
|
549
|
-
/** Sorted keys, no whitespace, minimal escaping: the same text in every SDK. */
|
|
549
|
+
/** Sorted keys, no whitespace, minimal escaping: the same text in every SDK. Throws on what JSON cannot hold. */
|
|
550
550
|
function canonical(value) {
|
|
551
551
|
if (value === null) return "null";
|
|
552
552
|
if (typeof value === "boolean") return String(value);
|
|
@@ -856,6 +856,7 @@ function track(toolName, handler) {
|
|
|
856
856
|
sdkVersion: SDK_VERSION,
|
|
857
857
|
...session !== void 0 && { sessionId: session },
|
|
858
858
|
...definition(toolName),
|
|
859
|
+
...call?.repeated === true && { repeated: true },
|
|
859
860
|
...outcome
|
|
860
861
|
});
|
|
861
862
|
} catch {}
|
|
@@ -964,7 +965,8 @@ function recordRefusedCall(refused) {
|
|
|
964
965
|
timestamp: refused.timestamp,
|
|
965
966
|
sdkVersion: SDK_VERSION,
|
|
966
967
|
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
967
|
-
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName)
|
|
968
|
+
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName),
|
|
969
|
+
...refused.repeated === true && { repeated: true }
|
|
968
970
|
});
|
|
969
971
|
} catch {}
|
|
970
972
|
}
|
|
@@ -1369,6 +1371,40 @@ function watchPrimitive(handler, server) {
|
|
|
1369
1371
|
};
|
|
1370
1372
|
}
|
|
1371
1373
|
//#endregion
|
|
1374
|
+
//#region src/repeats.ts
|
|
1375
|
+
/**
|
|
1376
|
+
* Whether a call repeats the previous call to the same tool in the same
|
|
1377
|
+
* session (contract, 3.9): an agent stuck in a loop.
|
|
1378
|
+
*
|
|
1379
|
+
* Only the answer leaves the process. Kept here is a SHA-256 of the canonical
|
|
1380
|
+
* arguments of the latest call per session and tool, never sent: a digest of
|
|
1381
|
+
* a short identifier or an enumerated value is found by trying every one.
|
|
1382
|
+
*/
|
|
1383
|
+
/** Session and tool pairs kept, the oldest forgotten first. */
|
|
1384
|
+
const MAX_KEPT = 1e4;
|
|
1385
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1386
|
+
/**
|
|
1387
|
+
* Notes a call's arguments, as the client sent them, and says whether they are
|
|
1388
|
+
* the previous call's to the same tool in the same session. Never throws: an
|
|
1389
|
+
* argument object that cannot be written down is never a repeat.
|
|
1390
|
+
*/
|
|
1391
|
+
function noteArguments(sessionId, toolName, args) {
|
|
1392
|
+
try {
|
|
1393
|
+
const digest = (0, node_crypto.createHash)("sha256").update(canonical(args ?? {}), "utf8").digest("hex");
|
|
1394
|
+
const key = `${sessionId}\u0000${toolName}`;
|
|
1395
|
+
const previous = latest.get(key);
|
|
1396
|
+
latest.delete(key);
|
|
1397
|
+
latest.set(key, digest);
|
|
1398
|
+
if (latest.size > MAX_KEPT) {
|
|
1399
|
+
const oldest = latest.keys().next().value;
|
|
1400
|
+
if (oldest !== void 0) latest.delete(oldest);
|
|
1401
|
+
}
|
|
1402
|
+
return previous === digest;
|
|
1403
|
+
} catch {
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
//#endregion
|
|
1372
1408
|
//#region src/instrument.ts
|
|
1373
1409
|
/**
|
|
1374
1410
|
* Methods an MCP server registers tools through.
|
|
@@ -1397,6 +1433,28 @@ const registries = /* @__PURE__ */ new WeakMap();
|
|
|
1397
1433
|
* it on its own. A weak set, so contexts leave with their requests.
|
|
1398
1434
|
*/
|
|
1399
1435
|
const reachedHandler = /* @__PURE__ */ new WeakSet();
|
|
1436
|
+
/** Requests whose arguments repeat the previous call's to the same tool in the session (contract, 3.9). */
|
|
1437
|
+
const repeatedCalls = /* @__PURE__ */ new WeakSet();
|
|
1438
|
+
/**
|
|
1439
|
+
* Compares a call's arguments, as the client sent them, with the previous call's to the same tool in the same
|
|
1440
|
+
* session, as it arrives, before any validation; and marks the request so the handler side knows too. A call
|
|
1441
|
+
* without a session is never compared.
|
|
1442
|
+
*/
|
|
1443
|
+
function noteRepeat(server, request, context) {
|
|
1444
|
+
try {
|
|
1445
|
+
const name = request.params?.name;
|
|
1446
|
+
if (typeof name !== "string" || typeof context !== "object" || context === null) return false;
|
|
1447
|
+
const params = request.params;
|
|
1448
|
+
if (params["inputResponses"] !== void 0 || params["requestState"] !== void 0) return false;
|
|
1449
|
+
const sessionId = sessionFor(server, context);
|
|
1450
|
+
if (sessionId === void 0) return false;
|
|
1451
|
+
const repeated = noteArguments(sessionId, name, request.params?.arguments);
|
|
1452
|
+
if (repeated) repeatedCalls.add(context);
|
|
1453
|
+
return repeated;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1400
1458
|
/**
|
|
1401
1459
|
* Request contexts whose handler last answered with an interim
|
|
1402
1460
|
* `input_required` result (the 2026-07-28 protocol's way to ask the client for
|
|
@@ -1546,10 +1604,12 @@ function noteReached(handler, server) {
|
|
|
1546
1604
|
const sessionId = hasContext ? sessionFor(server, context) : void 0;
|
|
1547
1605
|
const client = clientFor(server, hasContext ? context : void 0);
|
|
1548
1606
|
const serverVersion = serverVersionOf(server);
|
|
1607
|
+
const repeated = hasContext && repeatedCalls.has(context);
|
|
1549
1608
|
const result = withCall({
|
|
1550
1609
|
...sessionId !== void 0 && { sessionId },
|
|
1551
1610
|
...client !== void 0 && { client },
|
|
1552
|
-
...serverVersion !== void 0 && { serverVersion }
|
|
1611
|
+
...serverVersion !== void 0 && { serverVersion },
|
|
1612
|
+
...repeated && { repeated }
|
|
1553
1613
|
}, run);
|
|
1554
1614
|
if (hasContext) noteInterim(context, result);
|
|
1555
1615
|
return result;
|
|
@@ -1622,6 +1682,7 @@ function watchToolCalls(handler, server, registry) {
|
|
|
1622
1682
|
if (!isRecording() || request?.method !== "tools/call") return call();
|
|
1623
1683
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1624
1684
|
const startedAt = performance.now();
|
|
1685
|
+
const repeated = noteRepeat(server, request, context);
|
|
1625
1686
|
const noteRefusal = (message) => {
|
|
1626
1687
|
try {
|
|
1627
1688
|
const reached = typeof context === "object" && context !== null && reachedHandler.has(context);
|
|
@@ -1637,7 +1698,8 @@ function watchToolCalls(handler, server, registry) {
|
|
|
1637
1698
|
durationMs: performance.now() - startedAt,
|
|
1638
1699
|
sessionId: sessionFor(server, context),
|
|
1639
1700
|
client: clientFor(server, context),
|
|
1640
|
-
serverVersion: serverVersionOf(server)
|
|
1701
|
+
serverVersion: serverVersionOf(server),
|
|
1702
|
+
repeated
|
|
1641
1703
|
});
|
|
1642
1704
|
return;
|
|
1643
1705
|
}
|
|
@@ -1651,7 +1713,8 @@ function watchToolCalls(handler, server, registry) {
|
|
|
1651
1713
|
durationMs: performance.now() - startedAt,
|
|
1652
1714
|
sessionId: sessionFor(server, context),
|
|
1653
1715
|
client: clientFor(server, context),
|
|
1654
|
-
serverVersion: serverVersionOf(server)
|
|
1716
|
+
serverVersion: serverVersionOf(server),
|
|
1717
|
+
repeated
|
|
1655
1718
|
});
|
|
1656
1719
|
} catch {}
|
|
1657
1720
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -201,6 +201,8 @@ interface ToolCallEvent {
|
|
|
201
201
|
responseBytes?: number;
|
|
202
202
|
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
203
|
definitionHash?: string;
|
|
204
|
+
/** The arguments are the previous call's to the same tool in this session (contract, 3.9). */
|
|
205
|
+
repeated?: boolean;
|
|
204
206
|
/** When the call started, as an ISO 8601 timestamp. */
|
|
205
207
|
timestamp: string;
|
|
206
208
|
/** Version of the mcpspan package that produced the event. */
|
package/dist/index.d.mts
CHANGED
|
@@ -201,6 +201,8 @@ interface ToolCallEvent {
|
|
|
201
201
|
responseBytes?: number;
|
|
202
202
|
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
203
|
definitionHash?: string;
|
|
204
|
+
/** The arguments are the previous call's to the same tool in this session (contract, 3.9). */
|
|
205
|
+
repeated?: boolean;
|
|
204
206
|
/** When the call started, as an ISO 8601 timestamp. */
|
|
205
207
|
timestamp: string;
|
|
206
208
|
/** Version of the mcpspan package that produced the event. */
|
package/dist/index.mjs
CHANGED
|
@@ -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.4.0";
|
|
149
149
|
//#endregion
|
|
150
150
|
//#region src/transport.ts
|
|
151
151
|
/** Path the ingest API accepts batches on, appended to the configured endpoint. */
|
|
@@ -545,7 +545,7 @@ function definitionHash(tool) {
|
|
|
545
545
|
return;
|
|
546
546
|
}
|
|
547
547
|
}
|
|
548
|
-
/** Sorted keys, no whitespace, minimal escaping: the same text in every SDK. */
|
|
548
|
+
/** Sorted keys, no whitespace, minimal escaping: the same text in every SDK. Throws on what JSON cannot hold. */
|
|
549
549
|
function canonical(value) {
|
|
550
550
|
if (value === null) return "null";
|
|
551
551
|
if (typeof value === "boolean") return String(value);
|
|
@@ -855,6 +855,7 @@ function track(toolName, handler) {
|
|
|
855
855
|
sdkVersion: SDK_VERSION,
|
|
856
856
|
...session !== void 0 && { sessionId: session },
|
|
857
857
|
...definition(toolName),
|
|
858
|
+
...call?.repeated === true && { repeated: true },
|
|
858
859
|
...outcome
|
|
859
860
|
});
|
|
860
861
|
} catch {}
|
|
@@ -963,7 +964,8 @@ function recordRefusedCall(refused) {
|
|
|
963
964
|
timestamp: refused.timestamp,
|
|
964
965
|
sdkVersion: SDK_VERSION,
|
|
965
966
|
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
966
|
-
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName)
|
|
967
|
+
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName),
|
|
968
|
+
...refused.repeated === true && { repeated: true }
|
|
967
969
|
});
|
|
968
970
|
} catch {}
|
|
969
971
|
}
|
|
@@ -1368,6 +1370,40 @@ function watchPrimitive(handler, server) {
|
|
|
1368
1370
|
};
|
|
1369
1371
|
}
|
|
1370
1372
|
//#endregion
|
|
1373
|
+
//#region src/repeats.ts
|
|
1374
|
+
/**
|
|
1375
|
+
* Whether a call repeats the previous call to the same tool in the same
|
|
1376
|
+
* session (contract, 3.9): an agent stuck in a loop.
|
|
1377
|
+
*
|
|
1378
|
+
* Only the answer leaves the process. Kept here is a SHA-256 of the canonical
|
|
1379
|
+
* arguments of the latest call per session and tool, never sent: a digest of
|
|
1380
|
+
* a short identifier or an enumerated value is found by trying every one.
|
|
1381
|
+
*/
|
|
1382
|
+
/** Session and tool pairs kept, the oldest forgotten first. */
|
|
1383
|
+
const MAX_KEPT = 1e4;
|
|
1384
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1385
|
+
/**
|
|
1386
|
+
* Notes a call's arguments, as the client sent them, and says whether they are
|
|
1387
|
+
* the previous call's to the same tool in the same session. Never throws: an
|
|
1388
|
+
* argument object that cannot be written down is never a repeat.
|
|
1389
|
+
*/
|
|
1390
|
+
function noteArguments(sessionId, toolName, args) {
|
|
1391
|
+
try {
|
|
1392
|
+
const digest = createHash("sha256").update(canonical(args ?? {}), "utf8").digest("hex");
|
|
1393
|
+
const key = `${sessionId}\u0000${toolName}`;
|
|
1394
|
+
const previous = latest.get(key);
|
|
1395
|
+
latest.delete(key);
|
|
1396
|
+
latest.set(key, digest);
|
|
1397
|
+
if (latest.size > MAX_KEPT) {
|
|
1398
|
+
const oldest = latest.keys().next().value;
|
|
1399
|
+
if (oldest !== void 0) latest.delete(oldest);
|
|
1400
|
+
}
|
|
1401
|
+
return previous === digest;
|
|
1402
|
+
} catch {
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
//#endregion
|
|
1371
1407
|
//#region src/instrument.ts
|
|
1372
1408
|
/**
|
|
1373
1409
|
* Methods an MCP server registers tools through.
|
|
@@ -1396,6 +1432,28 @@ const registries = /* @__PURE__ */ new WeakMap();
|
|
|
1396
1432
|
* it on its own. A weak set, so contexts leave with their requests.
|
|
1397
1433
|
*/
|
|
1398
1434
|
const reachedHandler = /* @__PURE__ */ new WeakSet();
|
|
1435
|
+
/** Requests whose arguments repeat the previous call's to the same tool in the session (contract, 3.9). */
|
|
1436
|
+
const repeatedCalls = /* @__PURE__ */ new WeakSet();
|
|
1437
|
+
/**
|
|
1438
|
+
* Compares a call's arguments, as the client sent them, with the previous call's to the same tool in the same
|
|
1439
|
+
* session, as it arrives, before any validation; and marks the request so the handler side knows too. A call
|
|
1440
|
+
* without a session is never compared.
|
|
1441
|
+
*/
|
|
1442
|
+
function noteRepeat(server, request, context) {
|
|
1443
|
+
try {
|
|
1444
|
+
const name = request.params?.name;
|
|
1445
|
+
if (typeof name !== "string" || typeof context !== "object" || context === null) return false;
|
|
1446
|
+
const params = request.params;
|
|
1447
|
+
if (params["inputResponses"] !== void 0 || params["requestState"] !== void 0) return false;
|
|
1448
|
+
const sessionId = sessionFor(server, context);
|
|
1449
|
+
if (sessionId === void 0) return false;
|
|
1450
|
+
const repeated = noteArguments(sessionId, name, request.params?.arguments);
|
|
1451
|
+
if (repeated) repeatedCalls.add(context);
|
|
1452
|
+
return repeated;
|
|
1453
|
+
} catch {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1399
1457
|
/**
|
|
1400
1458
|
* Request contexts whose handler last answered with an interim
|
|
1401
1459
|
* `input_required` result (the 2026-07-28 protocol's way to ask the client for
|
|
@@ -1545,10 +1603,12 @@ function noteReached(handler, server) {
|
|
|
1545
1603
|
const sessionId = hasContext ? sessionFor(server, context) : void 0;
|
|
1546
1604
|
const client = clientFor(server, hasContext ? context : void 0);
|
|
1547
1605
|
const serverVersion = serverVersionOf(server);
|
|
1606
|
+
const repeated = hasContext && repeatedCalls.has(context);
|
|
1548
1607
|
const result = withCall({
|
|
1549
1608
|
...sessionId !== void 0 && { sessionId },
|
|
1550
1609
|
...client !== void 0 && { client },
|
|
1551
|
-
...serverVersion !== void 0 && { serverVersion }
|
|
1610
|
+
...serverVersion !== void 0 && { serverVersion },
|
|
1611
|
+
...repeated && { repeated }
|
|
1552
1612
|
}, run);
|
|
1553
1613
|
if (hasContext) noteInterim(context, result);
|
|
1554
1614
|
return result;
|
|
@@ -1621,6 +1681,7 @@ function watchToolCalls(handler, server, registry) {
|
|
|
1621
1681
|
if (!isRecording() || request?.method !== "tools/call") return call();
|
|
1622
1682
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1623
1683
|
const startedAt = performance.now();
|
|
1684
|
+
const repeated = noteRepeat(server, request, context);
|
|
1624
1685
|
const noteRefusal = (message) => {
|
|
1625
1686
|
try {
|
|
1626
1687
|
const reached = typeof context === "object" && context !== null && reachedHandler.has(context);
|
|
@@ -1636,7 +1697,8 @@ function watchToolCalls(handler, server, registry) {
|
|
|
1636
1697
|
durationMs: performance.now() - startedAt,
|
|
1637
1698
|
sessionId: sessionFor(server, context),
|
|
1638
1699
|
client: clientFor(server, context),
|
|
1639
|
-
serverVersion: serverVersionOf(server)
|
|
1700
|
+
serverVersion: serverVersionOf(server),
|
|
1701
|
+
repeated
|
|
1640
1702
|
});
|
|
1641
1703
|
return;
|
|
1642
1704
|
}
|
|
@@ -1650,7 +1712,8 @@ function watchToolCalls(handler, server, registry) {
|
|
|
1650
1712
|
durationMs: performance.now() - startedAt,
|
|
1651
1713
|
sessionId: sessionFor(server, context),
|
|
1652
1714
|
client: clientFor(server, context),
|
|
1653
|
-
serverVersion: serverVersionOf(server)
|
|
1715
|
+
serverVersion: serverVersionOf(server),
|
|
1716
|
+
repeated
|
|
1654
1717
|
});
|
|
1655
1718
|
} catch {}
|
|
1656
1719
|
};
|
package/package.json
CHANGED