mcpspan 0.4.0 → 0.5.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 +30 -2
- package/dist/index.cjs +117 -3
- package/dist/index.d.cts +13 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +117 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,6 +202,14 @@ names and types of the arguments instead, which is what shows the agent wrote
|
|
|
202
202
|
`dest` where the schema says `destination`. Tools passed through `exclude` stay
|
|
203
203
|
out of this as well.
|
|
204
204
|
|
|
205
|
+
For a refusal of arguments, the SDK also says which ones. It checks what the
|
|
206
|
+
agent sent against the input schema your server listed, in your process, and
|
|
207
|
+
records the top-level arguments that did not match, by the names the schema
|
|
208
|
+
declares, so the tool's page can show that 29 refusals were all `passengers`.
|
|
209
|
+
Values are never sent, and neither is a name the agent made up. It works from
|
|
210
|
+
the latest `tools/list` your server answered in the same process; a refusal
|
|
211
|
+
before any listing names nothing.
|
|
212
|
+
|
|
205
213
|
## Privacy
|
|
206
214
|
|
|
207
215
|
**Parameter values never leave your process.** Not by default, not in any
|
|
@@ -211,8 +219,12 @@ What is collected: the tool name, how long it took, whether it succeeded, the
|
|
|
211
219
|
error type and a truncated message when it did not, how large the answer was
|
|
212
220
|
in bytes (its size only, never its content), whether it repeated the previous
|
|
213
221
|
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),
|
|
215
|
-
|
|
222
|
+
the arguments, or any digest of them, never leave it), a fingerprint of the
|
|
223
|
+
tool's definition as your server lists it (its name, title, description and
|
|
224
|
+
input schema, hashed, so the dashboard can mark when you changed it), for a call your
|
|
225
|
+
server refused for its arguments the names of those that did not match the
|
|
226
|
+
tool's schema (never what was sent), which
|
|
227
|
+
client called, and the SDK version. For a resource or a prompt, the same, under the name it was
|
|
216
228
|
registered with: never the address a client read, only its template or, for
|
|
217
229
|
an address the server does not have, its scheme.
|
|
218
230
|
|
|
@@ -231,6 +243,21 @@ That records `{ destination: 'string', passengers: 'number' }`. Knowing
|
|
|
231
243
|
destination tells you nothing you needed, and puts your users' data somewhere
|
|
232
244
|
it does not belong.
|
|
233
245
|
|
|
246
|
+
Error messages are sent, cut short, because they are usually what says why a
|
|
247
|
+
call failed. If your tools can fail with text you would not send anywhere, as
|
|
248
|
+
a tool that runs commands or reads files might quote a path or a token, turn
|
|
249
|
+
them off:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
instrument(server, {
|
|
253
|
+
apiKey: process.env.MCPSPAN_API_KEY,
|
|
254
|
+
captureErrorMessages: false,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Every failure is still recorded, with where it came from and the exception's
|
|
259
|
+
type. Only the text is left out.
|
|
260
|
+
|
|
234
261
|
Types stay coarse and carry no length, because the distance between "a 34
|
|
235
262
|
character string" and "a credit card number" is shorter than it looks. Nested
|
|
236
263
|
objects are named but not opened.
|
|
@@ -282,6 +309,7 @@ That is the first rule, and everything below follows from it.
|
|
|
282
309
|
| `apiKey` | `MCPSPAN_API_KEY` | Identifies your server. Without it, nothing is collected. |
|
|
283
310
|
| `endpoint` | `MCPSPAN_ENDPOINT`; none | Your mcpspan installation. Nothing is collected without it. |
|
|
284
311
|
| `captureParameterNames` | `false` | Records parameter names and types, never values. |
|
|
312
|
+
| `captureErrorMessages` | `true` | Sends the text of a failure, cut short. Off sends that it failed and how, without the text. |
|
|
285
313
|
| `serverVersion` | `MCPSPAN_SERVER_VERSION`, then the server's own | The version to record calls under: a release, a tag, a commit. |
|
|
286
314
|
| `debug` | `false` | Writes delivery diagnostics to stderr. |
|
|
287
315
|
| `onDiagnostic` | - | Receives diagnostics instead of stderr. Implies `debug`. |
|
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.5.0";
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/transport.ts
|
|
152
152
|
/** Path the ingest API accepts batches on, appended to the configured endpoint. */
|
|
@@ -510,10 +510,16 @@ function currentCall() {
|
|
|
510
510
|
* the same tools as on any other.
|
|
511
511
|
*/
|
|
512
512
|
const listed = /* @__PURE__ */ new Map();
|
|
513
|
+
/** Each tool's input schema as last listed, to tell which arguments a refusal was over (contract, 3.10). */
|
|
514
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
513
515
|
/** The latest fingerprint listed for a tool, if any listing in this process named it. */
|
|
514
516
|
function definitionOf(toolName) {
|
|
515
517
|
return listed.get(toolName);
|
|
516
518
|
}
|
|
519
|
+
/** The latest input schema listed for a tool, if any listing in this process named it. */
|
|
520
|
+
function schemaOf(toolName) {
|
|
521
|
+
return schemas.get(toolName);
|
|
522
|
+
}
|
|
517
523
|
/** Notes every tool in an answer to `tools/list`. Never throws. */
|
|
518
524
|
function noteListing(result) {
|
|
519
525
|
try {
|
|
@@ -524,6 +530,7 @@ function noteListing(result) {
|
|
|
524
530
|
if (typeof name !== "string") continue;
|
|
525
531
|
const hash = definitionHash(tool);
|
|
526
532
|
if (hash !== void 0) listed.set(name, hash);
|
|
533
|
+
schemas.set(name, tool.inputSchema);
|
|
527
534
|
}
|
|
528
535
|
} catch {}
|
|
529
536
|
}
|
|
@@ -580,6 +587,99 @@ function text(value) {
|
|
|
580
587
|
return `${out}"`;
|
|
581
588
|
}
|
|
582
589
|
//#endregion
|
|
590
|
+
//#region src/arguments.ts
|
|
591
|
+
/**
|
|
592
|
+
* Which top-level arguments of a refused call did not match the tool's input
|
|
593
|
+
* schema (contract, 3.10).
|
|
594
|
+
*
|
|
595
|
+
* The server's own refusal is not read: each validation library words it
|
|
596
|
+
* differently, and some quote the value the agent sent. The arguments are
|
|
597
|
+
* checked here instead, against the schema the server listed, by a small set
|
|
598
|
+
* of rules that never fail what they do not understand. Only names the schema
|
|
599
|
+
* declares come out, so nothing the client made up, and no value, is sent.
|
|
600
|
+
*/
|
|
601
|
+
/** Names sent at most, per call. */
|
|
602
|
+
const MAX_NAMES = 20;
|
|
603
|
+
/** The declared names whose arguments fail the schema, sorted, at most twenty. Never throws. */
|
|
604
|
+
function invalidArguments(schema, args) {
|
|
605
|
+
try {
|
|
606
|
+
if (!isObject(schema)) return [];
|
|
607
|
+
const values = args ?? {};
|
|
608
|
+
if (!isObject(values)) return [];
|
|
609
|
+
const names = /* @__PURE__ */ new Set();
|
|
610
|
+
if (Array.isArray(schema["required"])) {
|
|
611
|
+
for (const name of schema["required"]) if (typeof name === "string" && !Object.hasOwn(values, name)) names.add(name);
|
|
612
|
+
}
|
|
613
|
+
const properties = schema["properties"];
|
|
614
|
+
if (isObject(properties)) {
|
|
615
|
+
for (const [name, property] of Object.entries(properties)) if (Object.hasOwn(values, name) && !matches(property, values[name])) names.add(name);
|
|
616
|
+
}
|
|
617
|
+
return [...names].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, MAX_NAMES);
|
|
618
|
+
} catch {
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/** Whether a value passes a schema under the checks the contract lists, and only those. */
|
|
623
|
+
function matches(schema, value) {
|
|
624
|
+
if (schema === false) return false;
|
|
625
|
+
if (!isObject(schema)) return true;
|
|
626
|
+
const type = schema["type"];
|
|
627
|
+
if (typeof type === "string" && !isType(type, value)) return false;
|
|
628
|
+
if (Array.isArray(type) && type.every((name) => typeof name === "string") && !type.some((name) => isType(name, value))) return false;
|
|
629
|
+
if (Array.isArray(schema["enum"])) {
|
|
630
|
+
const sent = canonical(value);
|
|
631
|
+
if (!schema["enum"].some((allowed) => canonical(allowed) === sent)) return false;
|
|
632
|
+
}
|
|
633
|
+
if (Object.hasOwn(schema, "const") && canonical(schema["const"]) !== canonical(value)) return false;
|
|
634
|
+
if (typeof value === "number") {
|
|
635
|
+
if (isNumber(schema["minimum"]) && value < schema["minimum"]) return false;
|
|
636
|
+
if (isNumber(schema["maximum"]) && value > schema["maximum"]) return false;
|
|
637
|
+
if (isNumber(schema["exclusiveMinimum"]) && value <= schema["exclusiveMinimum"]) return false;
|
|
638
|
+
if (isNumber(schema["exclusiveMaximum"]) && value >= schema["exclusiveMaximum"]) return false;
|
|
639
|
+
}
|
|
640
|
+
if (typeof value === "string") {
|
|
641
|
+
const length = [...value].length;
|
|
642
|
+
if (isNumber(schema["minLength"]) && length < schema["minLength"]) return false;
|
|
643
|
+
if (isNumber(schema["maxLength"]) && length > schema["maxLength"]) return false;
|
|
644
|
+
}
|
|
645
|
+
if (Array.isArray(value)) {
|
|
646
|
+
if (isNumber(schema["minItems"]) && value.length < schema["minItems"]) return false;
|
|
647
|
+
if (isNumber(schema["maxItems"]) && value.length > schema["maxItems"]) return false;
|
|
648
|
+
const items = schema["items"];
|
|
649
|
+
if (isObject(items) || typeof items === "boolean") {
|
|
650
|
+
if (!value.every((item) => matches(items, item))) return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (isObject(value)) {
|
|
654
|
+
if (Array.isArray(schema["required"])) {
|
|
655
|
+
for (const name of schema["required"]) if (typeof name === "string" && !Object.hasOwn(value, name)) return false;
|
|
656
|
+
}
|
|
657
|
+
const properties = schema["properties"];
|
|
658
|
+
if (isObject(properties)) {
|
|
659
|
+
for (const [name, property] of Object.entries(properties)) if (Object.hasOwn(value, name) && !matches(property, value[name])) return false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
function isType(type, value) {
|
|
665
|
+
switch (type) {
|
|
666
|
+
case "string": return typeof value === "string";
|
|
667
|
+
case "number": return typeof value === "number";
|
|
668
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
669
|
+
case "boolean": return typeof value === "boolean";
|
|
670
|
+
case "object": return isObject(value);
|
|
671
|
+
case "array": return Array.isArray(value);
|
|
672
|
+
case "null": return value === null;
|
|
673
|
+
default: return true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function isObject(value) {
|
|
677
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
678
|
+
}
|
|
679
|
+
function isNumber(value) {
|
|
680
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
583
683
|
//#region src/client.ts
|
|
584
684
|
/**
|
|
585
685
|
* Names we recognise, matched as substrings of what a client reports.
|
|
@@ -966,7 +1066,8 @@ function recordRefusedCall(refused) {
|
|
|
966
1066
|
sdkVersion: SDK_VERSION,
|
|
967
1067
|
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
968
1068
|
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName),
|
|
969
|
-
...refused.repeated === true && { repeated: true }
|
|
1069
|
+
...refused.repeated === true && { repeated: true },
|
|
1070
|
+
...refused.errorSource === "arguments" ? refusedNames(refused.toolName, refused.arguments) : {}
|
|
970
1071
|
});
|
|
971
1072
|
} catch {}
|
|
972
1073
|
}
|
|
@@ -1004,6 +1105,11 @@ function recordPrimitiveCall(call) {
|
|
|
1004
1105
|
function isRecording() {
|
|
1005
1106
|
return sink !== void 0;
|
|
1006
1107
|
}
|
|
1108
|
+
/** Which declared arguments a refusal was over (contract, 3.10), as event fields. */
|
|
1109
|
+
function refusedNames(toolName, args) {
|
|
1110
|
+
const names = invalidArguments(schemaOf(toolName), args).map((name) => truncate(name, MAX_TOOL_NAME_LENGTH));
|
|
1111
|
+
return names.length === 0 ? {} : { invalidArguments: names };
|
|
1112
|
+
}
|
|
1007
1113
|
/** The fingerprint of a tool as last listed (contract, 3.8), as event fields. */
|
|
1008
1114
|
function definition(toolName) {
|
|
1009
1115
|
const hash = definitionOf(toolName);
|
|
@@ -1100,7 +1206,8 @@ function configure(config = {}) {
|
|
|
1100
1206
|
};
|
|
1101
1207
|
setCaptureParameterNames(config.captureParameterNames ?? false);
|
|
1102
1208
|
setServerVersion(firstNonEmpty(config.serverVersion, process.env["MCPSPAN_SERVER_VERSION"]));
|
|
1103
|
-
|
|
1209
|
+
const captureErrorMessages = config.captureErrorMessages ?? true;
|
|
1210
|
+
setEventSink((event) => reporter?.record(captureErrorMessages ? event : withoutErrorMessage(event)));
|
|
1104
1211
|
if (config.flushOnExit ?? true) installExitHook();
|
|
1105
1212
|
reporter.announce();
|
|
1106
1213
|
}
|
|
@@ -1162,9 +1269,16 @@ function describeSettings(config) {
|
|
|
1162
1269
|
config.maxBatchSize ?? null,
|
|
1163
1270
|
config.maxQueueSize ?? null,
|
|
1164
1271
|
config.captureParameterNames ?? false,
|
|
1272
|
+
config.captureErrorMessages ?? true,
|
|
1165
1273
|
firstNonEmpty(config.serverVersion, process.env["MCPSPAN_SERVER_VERSION"]) ?? null
|
|
1166
1274
|
]);
|
|
1167
1275
|
}
|
|
1276
|
+
/** The event as it is, less the text of its failure (contract, 5). */
|
|
1277
|
+
function withoutErrorMessage(event) {
|
|
1278
|
+
if (event.errorMessage === void 0) return event;
|
|
1279
|
+
const { errorMessage: _left, ...rest } = event;
|
|
1280
|
+
return rest;
|
|
1281
|
+
}
|
|
1168
1282
|
function firstNonEmpty(...values) {
|
|
1169
1283
|
for (const value of values) {
|
|
1170
1284
|
const trimmed = value?.trim();
|
package/dist/index.d.cts
CHANGED
|
@@ -60,6 +60,17 @@ interface McpspanConfig {
|
|
|
60
60
|
* `departureDate`, which usually means a tool description is not landing.
|
|
61
61
|
*/
|
|
62
62
|
captureParameterNames?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Sends the text of a failure: what a tool returned with `isError`, cut to
|
|
65
|
+
* 200 characters, or an exception's message, cut to 500.
|
|
66
|
+
*
|
|
67
|
+
* On by default, since that text is usually what says why a call failed.
|
|
68
|
+
* Turn it off when your tools can fail with something you would not send
|
|
69
|
+
* anywhere, as one that runs commands or reads files might quote a path or
|
|
70
|
+
* a token. Every failure is still recorded, with where it came from and
|
|
71
|
+
* the exception's type; only the text is left out.
|
|
72
|
+
*/
|
|
73
|
+
captureErrorMessages?: boolean;
|
|
63
74
|
}
|
|
64
75
|
/**
|
|
65
76
|
* Starts collecting, or stops if there is nothing to collect with.
|
|
@@ -201,6 +212,8 @@ interface ToolCallEvent {
|
|
|
201
212
|
responseBytes?: number;
|
|
202
213
|
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
214
|
definitionHash?: string;
|
|
215
|
+
/** For refused arguments: which ones did not match the tool's schema, by declared name (contract, 3.10). */
|
|
216
|
+
invalidArguments?: string[];
|
|
204
217
|
/** The arguments are the previous call's to the same tool in this session (contract, 3.9). */
|
|
205
218
|
repeated?: boolean;
|
|
206
219
|
/** When the call started, as an ISO 8601 timestamp. */
|
package/dist/index.d.mts
CHANGED
|
@@ -60,6 +60,17 @@ interface McpspanConfig {
|
|
|
60
60
|
* `departureDate`, which usually means a tool description is not landing.
|
|
61
61
|
*/
|
|
62
62
|
captureParameterNames?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Sends the text of a failure: what a tool returned with `isError`, cut to
|
|
65
|
+
* 200 characters, or an exception's message, cut to 500.
|
|
66
|
+
*
|
|
67
|
+
* On by default, since that text is usually what says why a call failed.
|
|
68
|
+
* Turn it off when your tools can fail with something you would not send
|
|
69
|
+
* anywhere, as one that runs commands or reads files might quote a path or
|
|
70
|
+
* a token. Every failure is still recorded, with where it came from and
|
|
71
|
+
* the exception's type; only the text is left out.
|
|
72
|
+
*/
|
|
73
|
+
captureErrorMessages?: boolean;
|
|
63
74
|
}
|
|
64
75
|
/**
|
|
65
76
|
* Starts collecting, or stops if there is nothing to collect with.
|
|
@@ -201,6 +212,8 @@ interface ToolCallEvent {
|
|
|
201
212
|
responseBytes?: number;
|
|
202
213
|
/** The tool's definition as last listed, fingerprinted (contract, 3.8). */
|
|
203
214
|
definitionHash?: string;
|
|
215
|
+
/** For refused arguments: which ones did not match the tool's schema, by declared name (contract, 3.10). */
|
|
216
|
+
invalidArguments?: string[];
|
|
204
217
|
/** The arguments are the previous call's to the same tool in this session (contract, 3.9). */
|
|
205
218
|
repeated?: boolean;
|
|
206
219
|
/** When the call started, as an ISO 8601 timestamp. */
|
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.5.0";
|
|
149
149
|
//#endregion
|
|
150
150
|
//#region src/transport.ts
|
|
151
151
|
/** Path the ingest API accepts batches on, appended to the configured endpoint. */
|
|
@@ -509,10 +509,16 @@ function currentCall() {
|
|
|
509
509
|
* the same tools as on any other.
|
|
510
510
|
*/
|
|
511
511
|
const listed = /* @__PURE__ */ new Map();
|
|
512
|
+
/** Each tool's input schema as last listed, to tell which arguments a refusal was over (contract, 3.10). */
|
|
513
|
+
const schemas = /* @__PURE__ */ new Map();
|
|
512
514
|
/** The latest fingerprint listed for a tool, if any listing in this process named it. */
|
|
513
515
|
function definitionOf(toolName) {
|
|
514
516
|
return listed.get(toolName);
|
|
515
517
|
}
|
|
518
|
+
/** The latest input schema listed for a tool, if any listing in this process named it. */
|
|
519
|
+
function schemaOf(toolName) {
|
|
520
|
+
return schemas.get(toolName);
|
|
521
|
+
}
|
|
516
522
|
/** Notes every tool in an answer to `tools/list`. Never throws. */
|
|
517
523
|
function noteListing(result) {
|
|
518
524
|
try {
|
|
@@ -523,6 +529,7 @@ function noteListing(result) {
|
|
|
523
529
|
if (typeof name !== "string") continue;
|
|
524
530
|
const hash = definitionHash(tool);
|
|
525
531
|
if (hash !== void 0) listed.set(name, hash);
|
|
532
|
+
schemas.set(name, tool.inputSchema);
|
|
526
533
|
}
|
|
527
534
|
} catch {}
|
|
528
535
|
}
|
|
@@ -579,6 +586,99 @@ function text(value) {
|
|
|
579
586
|
return `${out}"`;
|
|
580
587
|
}
|
|
581
588
|
//#endregion
|
|
589
|
+
//#region src/arguments.ts
|
|
590
|
+
/**
|
|
591
|
+
* Which top-level arguments of a refused call did not match the tool's input
|
|
592
|
+
* schema (contract, 3.10).
|
|
593
|
+
*
|
|
594
|
+
* The server's own refusal is not read: each validation library words it
|
|
595
|
+
* differently, and some quote the value the agent sent. The arguments are
|
|
596
|
+
* checked here instead, against the schema the server listed, by a small set
|
|
597
|
+
* of rules that never fail what they do not understand. Only names the schema
|
|
598
|
+
* declares come out, so nothing the client made up, and no value, is sent.
|
|
599
|
+
*/
|
|
600
|
+
/** Names sent at most, per call. */
|
|
601
|
+
const MAX_NAMES = 20;
|
|
602
|
+
/** The declared names whose arguments fail the schema, sorted, at most twenty. Never throws. */
|
|
603
|
+
function invalidArguments(schema, args) {
|
|
604
|
+
try {
|
|
605
|
+
if (!isObject(schema)) return [];
|
|
606
|
+
const values = args ?? {};
|
|
607
|
+
if (!isObject(values)) return [];
|
|
608
|
+
const names = /* @__PURE__ */ new Set();
|
|
609
|
+
if (Array.isArray(schema["required"])) {
|
|
610
|
+
for (const name of schema["required"]) if (typeof name === "string" && !Object.hasOwn(values, name)) names.add(name);
|
|
611
|
+
}
|
|
612
|
+
const properties = schema["properties"];
|
|
613
|
+
if (isObject(properties)) {
|
|
614
|
+
for (const [name, property] of Object.entries(properties)) if (Object.hasOwn(values, name) && !matches(property, values[name])) names.add(name);
|
|
615
|
+
}
|
|
616
|
+
return [...names].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, MAX_NAMES);
|
|
617
|
+
} catch {
|
|
618
|
+
return [];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/** Whether a value passes a schema under the checks the contract lists, and only those. */
|
|
622
|
+
function matches(schema, value) {
|
|
623
|
+
if (schema === false) return false;
|
|
624
|
+
if (!isObject(schema)) return true;
|
|
625
|
+
const type = schema["type"];
|
|
626
|
+
if (typeof type === "string" && !isType(type, value)) return false;
|
|
627
|
+
if (Array.isArray(type) && type.every((name) => typeof name === "string") && !type.some((name) => isType(name, value))) return false;
|
|
628
|
+
if (Array.isArray(schema["enum"])) {
|
|
629
|
+
const sent = canonical(value);
|
|
630
|
+
if (!schema["enum"].some((allowed) => canonical(allowed) === sent)) return false;
|
|
631
|
+
}
|
|
632
|
+
if (Object.hasOwn(schema, "const") && canonical(schema["const"]) !== canonical(value)) return false;
|
|
633
|
+
if (typeof value === "number") {
|
|
634
|
+
if (isNumber(schema["minimum"]) && value < schema["minimum"]) return false;
|
|
635
|
+
if (isNumber(schema["maximum"]) && value > schema["maximum"]) return false;
|
|
636
|
+
if (isNumber(schema["exclusiveMinimum"]) && value <= schema["exclusiveMinimum"]) return false;
|
|
637
|
+
if (isNumber(schema["exclusiveMaximum"]) && value >= schema["exclusiveMaximum"]) return false;
|
|
638
|
+
}
|
|
639
|
+
if (typeof value === "string") {
|
|
640
|
+
const length = [...value].length;
|
|
641
|
+
if (isNumber(schema["minLength"]) && length < schema["minLength"]) return false;
|
|
642
|
+
if (isNumber(schema["maxLength"]) && length > schema["maxLength"]) return false;
|
|
643
|
+
}
|
|
644
|
+
if (Array.isArray(value)) {
|
|
645
|
+
if (isNumber(schema["minItems"]) && value.length < schema["minItems"]) return false;
|
|
646
|
+
if (isNumber(schema["maxItems"]) && value.length > schema["maxItems"]) return false;
|
|
647
|
+
const items = schema["items"];
|
|
648
|
+
if (isObject(items) || typeof items === "boolean") {
|
|
649
|
+
if (!value.every((item) => matches(items, item))) return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (isObject(value)) {
|
|
653
|
+
if (Array.isArray(schema["required"])) {
|
|
654
|
+
for (const name of schema["required"]) if (typeof name === "string" && !Object.hasOwn(value, name)) return false;
|
|
655
|
+
}
|
|
656
|
+
const properties = schema["properties"];
|
|
657
|
+
if (isObject(properties)) {
|
|
658
|
+
for (const [name, property] of Object.entries(properties)) if (Object.hasOwn(value, name) && !matches(property, value[name])) return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
function isType(type, value) {
|
|
664
|
+
switch (type) {
|
|
665
|
+
case "string": return typeof value === "string";
|
|
666
|
+
case "number": return typeof value === "number";
|
|
667
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
668
|
+
case "boolean": return typeof value === "boolean";
|
|
669
|
+
case "object": return isObject(value);
|
|
670
|
+
case "array": return Array.isArray(value);
|
|
671
|
+
case "null": return value === null;
|
|
672
|
+
default: return true;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function isObject(value) {
|
|
676
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
677
|
+
}
|
|
678
|
+
function isNumber(value) {
|
|
679
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
582
682
|
//#region src/client.ts
|
|
583
683
|
/**
|
|
584
684
|
* Names we recognise, matched as substrings of what a client reports.
|
|
@@ -965,7 +1065,8 @@ function recordRefusedCall(refused) {
|
|
|
965
1065
|
sdkVersion: SDK_VERSION,
|
|
966
1066
|
...refused.sessionId !== void 0 && { sessionId: refused.sessionId },
|
|
967
1067
|
...refused.errorSource === "unknown_tool" ? {} : definition(refused.toolName),
|
|
968
|
-
...refused.repeated === true && { repeated: true }
|
|
1068
|
+
...refused.repeated === true && { repeated: true },
|
|
1069
|
+
...refused.errorSource === "arguments" ? refusedNames(refused.toolName, refused.arguments) : {}
|
|
969
1070
|
});
|
|
970
1071
|
} catch {}
|
|
971
1072
|
}
|
|
@@ -1003,6 +1104,11 @@ function recordPrimitiveCall(call) {
|
|
|
1003
1104
|
function isRecording() {
|
|
1004
1105
|
return sink !== void 0;
|
|
1005
1106
|
}
|
|
1107
|
+
/** Which declared arguments a refusal was over (contract, 3.10), as event fields. */
|
|
1108
|
+
function refusedNames(toolName, args) {
|
|
1109
|
+
const names = invalidArguments(schemaOf(toolName), args).map((name) => truncate(name, MAX_TOOL_NAME_LENGTH));
|
|
1110
|
+
return names.length === 0 ? {} : { invalidArguments: names };
|
|
1111
|
+
}
|
|
1006
1112
|
/** The fingerprint of a tool as last listed (contract, 3.8), as event fields. */
|
|
1007
1113
|
function definition(toolName) {
|
|
1008
1114
|
const hash = definitionOf(toolName);
|
|
@@ -1099,7 +1205,8 @@ function configure(config = {}) {
|
|
|
1099
1205
|
};
|
|
1100
1206
|
setCaptureParameterNames(config.captureParameterNames ?? false);
|
|
1101
1207
|
setServerVersion(firstNonEmpty(config.serverVersion, process.env["MCPSPAN_SERVER_VERSION"]));
|
|
1102
|
-
|
|
1208
|
+
const captureErrorMessages = config.captureErrorMessages ?? true;
|
|
1209
|
+
setEventSink((event) => reporter?.record(captureErrorMessages ? event : withoutErrorMessage(event)));
|
|
1103
1210
|
if (config.flushOnExit ?? true) installExitHook();
|
|
1104
1211
|
reporter.announce();
|
|
1105
1212
|
}
|
|
@@ -1161,9 +1268,16 @@ function describeSettings(config) {
|
|
|
1161
1268
|
config.maxBatchSize ?? null,
|
|
1162
1269
|
config.maxQueueSize ?? null,
|
|
1163
1270
|
config.captureParameterNames ?? false,
|
|
1271
|
+
config.captureErrorMessages ?? true,
|
|
1164
1272
|
firstNonEmpty(config.serverVersion, process.env["MCPSPAN_SERVER_VERSION"]) ?? null
|
|
1165
1273
|
]);
|
|
1166
1274
|
}
|
|
1275
|
+
/** The event as it is, less the text of its failure (contract, 5). */
|
|
1276
|
+
function withoutErrorMessage(event) {
|
|
1277
|
+
if (event.errorMessage === void 0) return event;
|
|
1278
|
+
const { errorMessage: _left, ...rest } = event;
|
|
1279
|
+
return rest;
|
|
1280
|
+
}
|
|
1167
1281
|
function firstNonEmpty(...values) {
|
|
1168
1282
|
for (const value of values) {
|
|
1169
1283
|
const trimmed = value?.trim();
|
package/package.json
CHANGED