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 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), which client called,
215
- and the SDK version. For a resource or a prompt, the same, under the name it was
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.4.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
- setEventSink((event) => reporter?.record(event));
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.4.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
- setEventSink((event) => reporter?.record(event));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpspan",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Self-hosted analytics for MCP servers: which tools, resources and prompts get used, by which client, how fast, and why they fail.",
5
5
  "keywords": [
6
6
  "mcp",