autotel-devtools 23.1.0 → 24.1.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.
Files changed (33) hide show
  1. package/dist/cli.cjs +2 -2
  2. package/dist/cli.js +2 -2
  3. package/dist/{error-aggregator-B52JI8jL.d.ts → error-aggregator-BzhW482_.d.ts} +1 -1
  4. package/dist/{error-aggregator-DSwUvgFF.d.cts → error-aggregator-DkUrfGol.d.cts} +1 -1
  5. package/dist/{exporter-C88W22vY.d.ts → exporter-BRSxg3Du.d.ts} +25 -2
  6. package/dist/{exporter-OoWkSMCR.d.cts → exporter-BgjMeWvt.d.cts} +25 -2
  7. package/dist/fullpage.global.js +37 -30
  8. package/dist/genai/index.d.cts +1 -1
  9. package/dist/genai/index.d.ts +1 -1
  10. package/dist/{grpc-D0B3P9sI.cjs → grpc-AXr0CnZ_.cjs} +1 -1
  11. package/dist/{grpc-DY-C1jSU.js → grpc-C35twtEj.js} +1 -1
  12. package/dist/{http-1afd_01N.cjs → http-Dc57PtwQ.cjs} +256 -0
  13. package/dist/{http-gk1xapnA.js → http-JTSqQPVJ.js} +257 -1
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.d.cts +4 -4
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/server/exporter.d.cts +1 -1
  19. package/dist/server/exporter.d.ts +1 -1
  20. package/dist/server/index.cjs +2 -2
  21. package/dist/server/index.d.cts +4 -4
  22. package/dist/server/index.d.ts +4 -4
  23. package/dist/server/index.js +2 -2
  24. package/dist/{types-DM8y4A9Z.d.ts → types-Do48caKG.d.ts} +1 -1
  25. package/dist/{types-B0tjwFqj.d.cts → types-ZU74Na4z.d.cts} +1 -1
  26. package/dist/types-hVSf0DoT.d.cts +138 -0
  27. package/dist/types-hVSf0DoT.d.ts +138 -0
  28. package/dist/widget.global.js +14 -14
  29. package/dist/wire/index.d.cts +1 -1
  30. package/dist/wire/index.d.ts +1 -1
  31. package/package.json +2 -2
  32. package/dist/types-DHDvZnnn.d.cts +0 -47
  33. package/dist/types-DHDvZnnn.d.ts +0 -47
@@ -1,4 +1,4 @@
1
- import { r as SpanData } from "../types-DHDvZnnn.cjs";
1
+ import { r as SpanData } from "../types-hVSf0DoT.cjs";
2
2
  //#region src/widget/genai/detect.d.ts
3
3
  declare function isGenAiSpan(span: SpanData): boolean;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { r as SpanData } from "../types-DHDvZnnn.js";
1
+ import { r as SpanData } from "../types-hVSf0DoT.js";
2
2
  //#region src/widget/genai/detect.d.ts
3
3
  declare function isGenAiSpan(span: SpanData): boolean;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- const require_http = require('./http-1afd_01N.cjs');
1
+ const require_http = require('./http-Dc57PtwQ.cjs');
2
2
  let _grpc_grpc_js = require("@grpc/grpc-js");
3
3
 
4
4
  //#region src/server/grpc.ts
@@ -1,4 +1,4 @@
1
- import { c as decodeOtlpTraceRequest, o as decodeOtlpLogsRequest, s as decodeOtlpMetricsRequest } from "./http-gk1xapnA.js";
1
+ import { c as decodeOtlpTraceRequest, o as decodeOtlpLogsRequest, s as decodeOtlpMetricsRequest } from "./http-JTSqQPVJ.js";
2
2
  import { Server, ServerCredentials, status } from "@grpc/grpc-js";
3
3
 
4
4
  //#region src/server/grpc.ts
@@ -316,6 +316,200 @@ var ErrorAggregator = class {
316
316
  }
317
317
  };
318
318
 
319
+ //#endregion
320
+ //#region src/server/webmcp-aggregator.ts
321
+ /** Span names this fold consumes. Everything else in the trace is ignored. */
322
+ const INSTALL = "webmcp.install";
323
+ const REGISTER = "webmcp.tool.register";
324
+ const EXECUTE = "webmcp.tool.execute";
325
+ const WITHDRAW = "webmcp.tool.withdraw";
326
+ /** How many recent executions a tool carries. Enough to see a pattern, not a log. */
327
+ const RECENT_CALLS = 5;
328
+ /**
329
+ * `{"content":[{"type":"text","text":` + `}]}` and the quoting around it.
330
+ *
331
+ * Chrome does not unwrap the MCP envelope, so an enveloped result spends these
332
+ * bytes of the agent's context on structure carrying no information. Measured
333
+ * rather than derived: 45 bytes wrapped versus 13 plain for the same text.
334
+ */
335
+ const ENVELOPE_OVERHEAD_BYTES = 32;
336
+ const median = (values) => {
337
+ if (values.length === 0) return 0;
338
+ const sorted = [...values].sort((a, b) => a - b);
339
+ const mid = Math.floor(sorted.length / 2);
340
+ return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
341
+ };
342
+ const csv = (value) => {
343
+ const raw = require_json_fields.asString(value);
344
+ if (!raw) return [];
345
+ return raw.split(",").map((part) => part.trim()).filter(Boolean);
346
+ };
347
+ /**
348
+ * Spans with no installation id — emitted by an `autotel-webmcp` older than the
349
+ * lifecycle release. Grouping them under one key per service keeps them
350
+ * readable as a single legacy installation rather than scattering one
351
+ * installation per tool.
352
+ */
353
+ const UNKNOWN_INSTALLATION = "unknown";
354
+ const isWebMcpSpan = (span) => span.name === INSTALL || span.name === REGISTER || span.name === EXECUTE || span.name === WITHDRAW;
355
+ function foldWebMcpTools(traces, activityWindow) {
356
+ const tools = /* @__PURE__ */ new Map();
357
+ /** Installations seen, and whether each one ever registered anything. */
358
+ const installations = /* @__PURE__ */ new Map();
359
+ const latestInstallation = /* @__PURE__ */ new Map();
360
+ const spans = traces.flatMap((trace) => trace.spans.map((span) => ({
361
+ span,
362
+ service: trace.service
363
+ }))).sort((a, b) => a.span.startTime - b.span.startTime);
364
+ for (const { span, service } of spans) {
365
+ if (!isWebMcpSpan(span)) continue;
366
+ if (activityWindow && span.startTime > activityWindow.end) continue;
367
+ const attrs = span.attributes ?? {};
368
+ const installationId = require_json_fields.asString(attrs["webmcp.installation.id"]) ?? UNKNOWN_INSTALLATION;
369
+ const installationKey = `${service}${installationId}`;
370
+ if (!installations.has(installationKey)) installations.set(installationKey, false);
371
+ if (span.name === INSTALL) {
372
+ latestInstallation.set(service, installationId);
373
+ continue;
374
+ }
375
+ const name = require_json_fields.asString(attrs["webmcp.tool.name"]) ?? require_json_fields.asString(attrs["gen_ai.tool.name"]);
376
+ if (!name) continue;
377
+ const key = `${service}${installationId}${name}`;
378
+ let tool = tools.get(key);
379
+ if (!tool) {
380
+ tool = {
381
+ name,
382
+ installationId,
383
+ service,
384
+ sessionId: require_json_fields.asString(attrs["session.id"]),
385
+ observedAtRegistration: false,
386
+ offered: false,
387
+ firstSeen: span.startTime,
388
+ lastSeen: span.startTime,
389
+ annotationsSent: [],
390
+ annotationsDropped: [],
391
+ labelMismatch: false,
392
+ redefined: false,
393
+ refusedCalls: 0,
394
+ calls: 0,
395
+ errors: 0,
396
+ envelopeCalls: 0,
397
+ envelopeBytes: 0,
398
+ substitutedCalls: 0,
399
+ resultBytes: 0,
400
+ medianResultBytes: 0,
401
+ resultSizes: [],
402
+ recentCalls: [],
403
+ traceId: span.traceId,
404
+ spanId: span.spanId
405
+ };
406
+ tools.set(key, tool);
407
+ }
408
+ tool.firstSeen = Math.min(tool.firstSeen, span.startTime);
409
+ tool.lastSeen = Math.max(tool.lastSeen, span.startTime);
410
+ tool.sessionId ??= require_json_fields.asString(attrs["session.id"]);
411
+ switch (span.name) {
412
+ case REGISTER:
413
+ installations.set(installationKey, true);
414
+ tool.observedAtRegistration = true;
415
+ tool.offered = true;
416
+ tool.descriptionLength = require_json_fields.asNumber(attrs["webmcp.tool.description.length"]);
417
+ tool.hasInputSchema = require_json_fields.asBoolean(attrs["webmcp.tool.has_input_schema"]);
418
+ tool.annotationsSent = csv(attrs["webmcp.annotations.sent"]);
419
+ tool.annotationsDropped = csv(attrs["webmcp.annotations.dropped"]);
420
+ tool.title = require_json_fields.asString(attrs["webmcp.tool.title"]);
421
+ tool.labelMismatch = require_json_fields.asBoolean(attrs["webmcp.tool.label_mismatch"]) ?? false;
422
+ tool.descriptor = require_json_fields.asString(attrs["webmcp.tool.descriptor"]);
423
+ if (require_json_fields.asBoolean(attrs["webmcp.tool.redefined"])) tool.redefined = true;
424
+ tool.traceId = span.traceId;
425
+ tool.spanId = span.spanId;
426
+ break;
427
+ case WITHDRAW:
428
+ tool.offered = false;
429
+ break;
430
+ case EXECUTE: {
431
+ if (activityWindow && span.startTime < activityWindow.start) break;
432
+ tool.calls += 1;
433
+ const bytes = require_json_fields.asNumber(attrs["webmcp.result.bytes"]) ?? 0;
434
+ tool.resultBytes += bytes;
435
+ tool.resultSizes.push(bytes);
436
+ if (require_json_fields.asBoolean(attrs["webmcp.result.envelope"])) {
437
+ tool.envelopeCalls += 1;
438
+ tool.envelopeBytes += ENVELOPE_OVERHEAD_BYTES;
439
+ }
440
+ if (require_json_fields.asBoolean(attrs["webmcp.result.substituted"])) tool.substitutedCalls += 1;
441
+ const refused = require_json_fields.asBoolean(attrs["webmcp.result.refused"]) ?? false;
442
+ const refusal = require_json_fields.asString(attrs["webmcp.result.refusal"]);
443
+ if (refused) tool.refusedCalls += 1;
444
+ const failed = span.status.code === "ERROR" || (require_json_fields.asBoolean(attrs["webmcp.result.error"]) ?? false) || require_json_fields.asString(attrs["error.type"]) !== void 0;
445
+ if (failed) tool.errors += 1;
446
+ tool.recentCalls.push({
447
+ timestamp: span.startTime,
448
+ durationMs: span.duration,
449
+ resultBytes: bytes,
450
+ resultType: require_json_fields.asString(attrs["webmcp.result.type"]),
451
+ envelope: require_json_fields.asBoolean(attrs["webmcp.result.envelope"]) ?? false,
452
+ substituted: require_json_fields.asBoolean(attrs["webmcp.result.substituted"]) ?? false,
453
+ error: failed,
454
+ seq: require_json_fields.asNumber(attrs["webmcp.execute.seq"]),
455
+ refused,
456
+ ...refusal === "confirm" || refusal === "unavailable" ? { refusal } : {},
457
+ input: require_json_fields.asString(attrs["webmcp.input"]) ?? require_json_fields.asString(attrs["gen_ai.tool.call.arguments"]),
458
+ result: require_json_fields.asString(attrs["webmcp.result"]) ?? require_json_fields.asString(attrs["gen_ai.tool.call.result"]),
459
+ traceId: span.traceId,
460
+ spanId: span.spanId
461
+ });
462
+ break;
463
+ }
464
+ }
465
+ }
466
+ const list = [...tools.values()].map((tool) => {
467
+ const { resultSizes, ...rest } = tool;
468
+ return {
469
+ ...rest,
470
+ offered: rest.offered && (latestInstallation.get(rest.service) ?? rest.installationId) === rest.installationId,
471
+ medianResultBytes: median(resultSizes),
472
+ recentCalls: [...tool.recentCalls].sort((a, b) => b.timestamp - a.timestamp).slice(0, RECENT_CALLS)
473
+ };
474
+ });
475
+ list.sort((a, b) => Number(b.offered) - Number(a.offered) || b.calls - a.calls || a.name.localeCompare(b.name));
476
+ return {
477
+ tools: list,
478
+ summary: summarize(list, installations)
479
+ };
480
+ }
481
+ function summarize(tools, installations) {
482
+ const summary = {
483
+ installations: installations.size,
484
+ emptyInstallations: [...installations.values()].filter((saw) => !saw).length,
485
+ toolsOffered: 0,
486
+ toolsWithdrawn: 0,
487
+ calls: 0,
488
+ errors: 0,
489
+ resultBytes: 0,
490
+ envelopeBytes: 0,
491
+ toolsWithDroppedAnnotations: 0,
492
+ toolsWithoutInputSchema: 0,
493
+ toolsWithLabelMismatch: 0,
494
+ toolsRedefined: 0,
495
+ refusedCalls: 0
496
+ };
497
+ for (const tool of tools) {
498
+ if (tool.offered) summary.toolsOffered += 1;
499
+ else summary.toolsWithdrawn += 1;
500
+ summary.calls += tool.calls;
501
+ summary.errors += tool.errors;
502
+ summary.resultBytes += tool.resultBytes;
503
+ summary.envelopeBytes += tool.envelopeBytes;
504
+ if (tool.annotationsDropped.length > 0) summary.toolsWithDroppedAnnotations += 1;
505
+ if (tool.observedAtRegistration && tool.hasInputSchema === false) summary.toolsWithoutInputSchema += 1;
506
+ if (tool.labelMismatch) summary.toolsWithLabelMismatch += 1;
507
+ if (tool.redefined) summary.toolsRedefined += 1;
508
+ summary.refusedCalls += tool.refusedCalls;
509
+ }
510
+ return summary;
511
+ }
512
+
319
513
  //#endregion
320
514
  //#region src/server/telemetry-limits.ts
321
515
  const defaultLimit = 100;
@@ -2754,6 +2948,44 @@ var DevtoolsServer = class {
2754
2948
  } while (cursor);
2755
2949
  return { errors: aggregator.getErrorGroups() };
2756
2950
  }
2951
+ /**
2952
+ * Fold WebMCP lifecycle history through the window end into the tool surface
2953
+ * an agent is offered, while counting executions only inside the window.
2954
+ *
2955
+ * Drains every page rather than folding the first one: an inventory built
2956
+ * from a page of results does not fail, it *under-reports* — "2 tools dropped
2957
+ * annotations" when the answer is 6 — and gets more wrong the more traffic
2958
+ * there is, which is backwards for an observability answer.
2959
+ *
2960
+ * The span-name filter is composed here rather than accepted from the client:
2961
+ * this endpoint answers one question, and a caller-supplied predicate could
2962
+ * only narrow it into a wrong answer.
2963
+ */
2964
+ queryWebMcp(args) {
2965
+ const traces = [];
2966
+ const seenCursors = /* @__PURE__ */ new Set();
2967
+ let cursor;
2968
+ do {
2969
+ const result = this.store.queryTraces({
2970
+ query: "name ^ \"webmcp.\"",
2971
+ window: args.window ? {
2972
+ start: 0,
2973
+ end: args.window.end
2974
+ } : void 0,
2975
+ limit: args.limit,
2976
+ cursor
2977
+ });
2978
+ if (result.errors) return {
2979
+ webmcp: foldWebMcpTools([]),
2980
+ errors_parse: result.errors
2981
+ };
2982
+ traces.push(...result.traces);
2983
+ cursor = result.nextCursor ?? void 0;
2984
+ if (cursor && seenCursors.has(cursor)) break;
2985
+ if (cursor) seenCursors.add(cursor);
2986
+ } while (cursor);
2987
+ return { webmcp: foldWebMcpTools(traces, args.window) };
2988
+ }
2757
2989
  /** Run a log query against the durable store. */
2758
2990
  queryLogs(args) {
2759
2991
  return this.store.queryLogs(args);
@@ -3757,6 +3989,30 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
3757
3989
  }
3758
3990
  return;
3759
3991
  }
3992
+ if (req.method === "POST" && url === "/api/query/webmcp") {
3993
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
3994
+ sendJson(res, 403, { error: "Forbidden" });
3995
+ return;
3996
+ }
3997
+ try {
3998
+ const body = await readJsonBody(req);
3999
+ const result = devtools.queryWebMcp({
4000
+ window: body.window,
4001
+ limit: body.limit
4002
+ });
4003
+ if (result.errors_parse) {
4004
+ sendJson(res, 400, { errors: result.errors_parse });
4005
+ return;
4006
+ }
4007
+ sendJson(res, 200, { webmcp: result.webmcp });
4008
+ } catch (e) {
4009
+ sendJson(res, 400, {
4010
+ error: "Invalid query request",
4011
+ message: e instanceof Error ? e.message : String(e)
4012
+ });
4013
+ }
4014
+ return;
4015
+ }
3760
4016
  if (req.method === "GET" && url === "/api/metrics") {
3761
4017
  if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
3762
4018
  sendJson(res, 403, { error: "Forbidden" });
@@ -1,5 +1,5 @@
1
1
  import { i as encodeTraces } from "./wire-2Rmfg6IT.js";
2
- import { i as asString, o as stringAttr, t as asObject } from "./json-fields-CPjKZ2WH.js";
2
+ import { i as asString, n as asBoolean, o as stringAttr, r as asNumber, t as asObject } from "./json-fields-CPjKZ2WH.js";
3
3
  import { t as pickRoot } from "./trace-root-EHnvuA7f.js";
4
4
  import { a as compileWhere, t as parse } from "./parse-D_RmPPQs.js";
5
5
  import { t as getResourceName } from "./resource-utils-B4UVvfnH.js";
@@ -287,6 +287,200 @@ var ErrorAggregator = class {
287
287
  }
288
288
  };
289
289
 
290
+ //#endregion
291
+ //#region src/server/webmcp-aggregator.ts
292
+ /** Span names this fold consumes. Everything else in the trace is ignored. */
293
+ const INSTALL = "webmcp.install";
294
+ const REGISTER = "webmcp.tool.register";
295
+ const EXECUTE = "webmcp.tool.execute";
296
+ const WITHDRAW = "webmcp.tool.withdraw";
297
+ /** How many recent executions a tool carries. Enough to see a pattern, not a log. */
298
+ const RECENT_CALLS = 5;
299
+ /**
300
+ * `{"content":[{"type":"text","text":` + `}]}` and the quoting around it.
301
+ *
302
+ * Chrome does not unwrap the MCP envelope, so an enveloped result spends these
303
+ * bytes of the agent's context on structure carrying no information. Measured
304
+ * rather than derived: 45 bytes wrapped versus 13 plain for the same text.
305
+ */
306
+ const ENVELOPE_OVERHEAD_BYTES = 32;
307
+ const median = (values) => {
308
+ if (values.length === 0) return 0;
309
+ const sorted = [...values].sort((a, b) => a - b);
310
+ const mid = Math.floor(sorted.length / 2);
311
+ return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
312
+ };
313
+ const csv = (value) => {
314
+ const raw = asString(value);
315
+ if (!raw) return [];
316
+ return raw.split(",").map((part) => part.trim()).filter(Boolean);
317
+ };
318
+ /**
319
+ * Spans with no installation id — emitted by an `autotel-webmcp` older than the
320
+ * lifecycle release. Grouping them under one key per service keeps them
321
+ * readable as a single legacy installation rather than scattering one
322
+ * installation per tool.
323
+ */
324
+ const UNKNOWN_INSTALLATION = "unknown";
325
+ const isWebMcpSpan = (span) => span.name === INSTALL || span.name === REGISTER || span.name === EXECUTE || span.name === WITHDRAW;
326
+ function foldWebMcpTools(traces, activityWindow) {
327
+ const tools = /* @__PURE__ */ new Map();
328
+ /** Installations seen, and whether each one ever registered anything. */
329
+ const installations = /* @__PURE__ */ new Map();
330
+ const latestInstallation = /* @__PURE__ */ new Map();
331
+ const spans = traces.flatMap((trace) => trace.spans.map((span) => ({
332
+ span,
333
+ service: trace.service
334
+ }))).sort((a, b) => a.span.startTime - b.span.startTime);
335
+ for (const { span, service } of spans) {
336
+ if (!isWebMcpSpan(span)) continue;
337
+ if (activityWindow && span.startTime > activityWindow.end) continue;
338
+ const attrs = span.attributes ?? {};
339
+ const installationId = asString(attrs["webmcp.installation.id"]) ?? UNKNOWN_INSTALLATION;
340
+ const installationKey = `${service}${installationId}`;
341
+ if (!installations.has(installationKey)) installations.set(installationKey, false);
342
+ if (span.name === INSTALL) {
343
+ latestInstallation.set(service, installationId);
344
+ continue;
345
+ }
346
+ const name = asString(attrs["webmcp.tool.name"]) ?? asString(attrs["gen_ai.tool.name"]);
347
+ if (!name) continue;
348
+ const key = `${service}${installationId}${name}`;
349
+ let tool = tools.get(key);
350
+ if (!tool) {
351
+ tool = {
352
+ name,
353
+ installationId,
354
+ service,
355
+ sessionId: asString(attrs["session.id"]),
356
+ observedAtRegistration: false,
357
+ offered: false,
358
+ firstSeen: span.startTime,
359
+ lastSeen: span.startTime,
360
+ annotationsSent: [],
361
+ annotationsDropped: [],
362
+ labelMismatch: false,
363
+ redefined: false,
364
+ refusedCalls: 0,
365
+ calls: 0,
366
+ errors: 0,
367
+ envelopeCalls: 0,
368
+ envelopeBytes: 0,
369
+ substitutedCalls: 0,
370
+ resultBytes: 0,
371
+ medianResultBytes: 0,
372
+ resultSizes: [],
373
+ recentCalls: [],
374
+ traceId: span.traceId,
375
+ spanId: span.spanId
376
+ };
377
+ tools.set(key, tool);
378
+ }
379
+ tool.firstSeen = Math.min(tool.firstSeen, span.startTime);
380
+ tool.lastSeen = Math.max(tool.lastSeen, span.startTime);
381
+ tool.sessionId ??= asString(attrs["session.id"]);
382
+ switch (span.name) {
383
+ case REGISTER:
384
+ installations.set(installationKey, true);
385
+ tool.observedAtRegistration = true;
386
+ tool.offered = true;
387
+ tool.descriptionLength = asNumber(attrs["webmcp.tool.description.length"]);
388
+ tool.hasInputSchema = asBoolean(attrs["webmcp.tool.has_input_schema"]);
389
+ tool.annotationsSent = csv(attrs["webmcp.annotations.sent"]);
390
+ tool.annotationsDropped = csv(attrs["webmcp.annotations.dropped"]);
391
+ tool.title = asString(attrs["webmcp.tool.title"]);
392
+ tool.labelMismatch = asBoolean(attrs["webmcp.tool.label_mismatch"]) ?? false;
393
+ tool.descriptor = asString(attrs["webmcp.tool.descriptor"]);
394
+ if (asBoolean(attrs["webmcp.tool.redefined"])) tool.redefined = true;
395
+ tool.traceId = span.traceId;
396
+ tool.spanId = span.spanId;
397
+ break;
398
+ case WITHDRAW:
399
+ tool.offered = false;
400
+ break;
401
+ case EXECUTE: {
402
+ if (activityWindow && span.startTime < activityWindow.start) break;
403
+ tool.calls += 1;
404
+ const bytes = asNumber(attrs["webmcp.result.bytes"]) ?? 0;
405
+ tool.resultBytes += bytes;
406
+ tool.resultSizes.push(bytes);
407
+ if (asBoolean(attrs["webmcp.result.envelope"])) {
408
+ tool.envelopeCalls += 1;
409
+ tool.envelopeBytes += ENVELOPE_OVERHEAD_BYTES;
410
+ }
411
+ if (asBoolean(attrs["webmcp.result.substituted"])) tool.substitutedCalls += 1;
412
+ const refused = asBoolean(attrs["webmcp.result.refused"]) ?? false;
413
+ const refusal = asString(attrs["webmcp.result.refusal"]);
414
+ if (refused) tool.refusedCalls += 1;
415
+ const failed = span.status.code === "ERROR" || (asBoolean(attrs["webmcp.result.error"]) ?? false) || asString(attrs["error.type"]) !== void 0;
416
+ if (failed) tool.errors += 1;
417
+ tool.recentCalls.push({
418
+ timestamp: span.startTime,
419
+ durationMs: span.duration,
420
+ resultBytes: bytes,
421
+ resultType: asString(attrs["webmcp.result.type"]),
422
+ envelope: asBoolean(attrs["webmcp.result.envelope"]) ?? false,
423
+ substituted: asBoolean(attrs["webmcp.result.substituted"]) ?? false,
424
+ error: failed,
425
+ seq: asNumber(attrs["webmcp.execute.seq"]),
426
+ refused,
427
+ ...refusal === "confirm" || refusal === "unavailable" ? { refusal } : {},
428
+ input: asString(attrs["webmcp.input"]) ?? asString(attrs["gen_ai.tool.call.arguments"]),
429
+ result: asString(attrs["webmcp.result"]) ?? asString(attrs["gen_ai.tool.call.result"]),
430
+ traceId: span.traceId,
431
+ spanId: span.spanId
432
+ });
433
+ break;
434
+ }
435
+ }
436
+ }
437
+ const list = [...tools.values()].map((tool) => {
438
+ const { resultSizes, ...rest } = tool;
439
+ return {
440
+ ...rest,
441
+ offered: rest.offered && (latestInstallation.get(rest.service) ?? rest.installationId) === rest.installationId,
442
+ medianResultBytes: median(resultSizes),
443
+ recentCalls: [...tool.recentCalls].sort((a, b) => b.timestamp - a.timestamp).slice(0, RECENT_CALLS)
444
+ };
445
+ });
446
+ list.sort((a, b) => Number(b.offered) - Number(a.offered) || b.calls - a.calls || a.name.localeCompare(b.name));
447
+ return {
448
+ tools: list,
449
+ summary: summarize(list, installations)
450
+ };
451
+ }
452
+ function summarize(tools, installations) {
453
+ const summary = {
454
+ installations: installations.size,
455
+ emptyInstallations: [...installations.values()].filter((saw) => !saw).length,
456
+ toolsOffered: 0,
457
+ toolsWithdrawn: 0,
458
+ calls: 0,
459
+ errors: 0,
460
+ resultBytes: 0,
461
+ envelopeBytes: 0,
462
+ toolsWithDroppedAnnotations: 0,
463
+ toolsWithoutInputSchema: 0,
464
+ toolsWithLabelMismatch: 0,
465
+ toolsRedefined: 0,
466
+ refusedCalls: 0
467
+ };
468
+ for (const tool of tools) {
469
+ if (tool.offered) summary.toolsOffered += 1;
470
+ else summary.toolsWithdrawn += 1;
471
+ summary.calls += tool.calls;
472
+ summary.errors += tool.errors;
473
+ summary.resultBytes += tool.resultBytes;
474
+ summary.envelopeBytes += tool.envelopeBytes;
475
+ if (tool.annotationsDropped.length > 0) summary.toolsWithDroppedAnnotations += 1;
476
+ if (tool.observedAtRegistration && tool.hasInputSchema === false) summary.toolsWithoutInputSchema += 1;
477
+ if (tool.labelMismatch) summary.toolsWithLabelMismatch += 1;
478
+ if (tool.redefined) summary.toolsRedefined += 1;
479
+ summary.refusedCalls += tool.refusedCalls;
480
+ }
481
+ return summary;
482
+ }
483
+
290
484
  //#endregion
291
485
  //#region src/server/telemetry-limits.ts
292
486
  const defaultLimit = 100;
@@ -2725,6 +2919,44 @@ var DevtoolsServer = class {
2725
2919
  } while (cursor);
2726
2920
  return { errors: aggregator.getErrorGroups() };
2727
2921
  }
2922
+ /**
2923
+ * Fold WebMCP lifecycle history through the window end into the tool surface
2924
+ * an agent is offered, while counting executions only inside the window.
2925
+ *
2926
+ * Drains every page rather than folding the first one: an inventory built
2927
+ * from a page of results does not fail, it *under-reports* — "2 tools dropped
2928
+ * annotations" when the answer is 6 — and gets more wrong the more traffic
2929
+ * there is, which is backwards for an observability answer.
2930
+ *
2931
+ * The span-name filter is composed here rather than accepted from the client:
2932
+ * this endpoint answers one question, and a caller-supplied predicate could
2933
+ * only narrow it into a wrong answer.
2934
+ */
2935
+ queryWebMcp(args) {
2936
+ const traces = [];
2937
+ const seenCursors = /* @__PURE__ */ new Set();
2938
+ let cursor;
2939
+ do {
2940
+ const result = this.store.queryTraces({
2941
+ query: "name ^ \"webmcp.\"",
2942
+ window: args.window ? {
2943
+ start: 0,
2944
+ end: args.window.end
2945
+ } : void 0,
2946
+ limit: args.limit,
2947
+ cursor
2948
+ });
2949
+ if (result.errors) return {
2950
+ webmcp: foldWebMcpTools([]),
2951
+ errors_parse: result.errors
2952
+ };
2953
+ traces.push(...result.traces);
2954
+ cursor = result.nextCursor ?? void 0;
2955
+ if (cursor && seenCursors.has(cursor)) break;
2956
+ if (cursor) seenCursors.add(cursor);
2957
+ } while (cursor);
2958
+ return { webmcp: foldWebMcpTools(traces, args.window) };
2959
+ }
2728
2960
  /** Run a log query against the durable store. */
2729
2961
  queryLogs(args) {
2730
2962
  return this.store.queryLogs(args);
@@ -3728,6 +3960,30 @@ function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
3728
3960
  }
3729
3961
  return;
3730
3962
  }
3963
+ if (req.method === "POST" && url === "/api/query/webmcp") {
3964
+ if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
3965
+ sendJson(res, 403, { error: "Forbidden" });
3966
+ return;
3967
+ }
3968
+ try {
3969
+ const body = await readJsonBody(req);
3970
+ const result = devtools.queryWebMcp({
3971
+ window: body.window,
3972
+ limit: body.limit
3973
+ });
3974
+ if (result.errors_parse) {
3975
+ sendJson(res, 400, { errors: result.errors_parse });
3976
+ return;
3977
+ }
3978
+ sendJson(res, 200, { webmcp: result.webmcp });
3979
+ } catch (e) {
3980
+ sendJson(res, 400, {
3981
+ error: "Invalid query request",
3982
+ message: e instanceof Error ? e.message : String(e)
3983
+ });
3984
+ }
3985
+ return;
3986
+ }
3731
3987
  if (req.method === "GET" && url === "/api/metrics") {
3732
3988
  if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
3733
3989
  sendJson(res, 403, { error: "Forbidden" });
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_http = require('./http-1afd_01N.cjs');
2
+ const require_http = require('./http-Dc57PtwQ.cjs');
3
3
  const require_listen = require('./listen-l09RRHht.cjs');
4
4
  const require_server_exporter = require('./server/exporter.cjs');
5
5
  const require_server_log_exporter = require('./server/log-exporter.cjs');
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
- import { n as SpanAttributes, t as AttributeValue } from "./types-DHDvZnnn.cjs";
2
- import { a as SpanData, i as LogData, n as ErrorGroup, o as TraceData, t as DevtoolsData } from "./types-B0tjwFqj.cjs";
3
- import { n as DevtoolsServer, t as DevtoolsSpanExporter } from "./exporter-OoWkSMCR.cjs";
1
+ import { n as SpanAttributes, t as AttributeValue } from "./types-hVSf0DoT.cjs";
2
+ import { a as SpanData, i as LogData, n as ErrorGroup, o as TraceData, t as DevtoolsData } from "./types-ZU74Na4z.cjs";
3
+ import { n as DevtoolsServer, t as DevtoolsSpanExporter } from "./exporter-BgjMeWvt.cjs";
4
4
  import { DevtoolsLogExporter } from "./server/log-exporter.cjs";
5
5
  import { DevtoolsRemoteExporter } from "./server/remote-exporter.cjs";
6
- import { t as ErrorAggregator } from "./error-aggregator-DSwUvgFF.cjs";
6
+ import { t as ErrorAggregator } from "./error-aggregator-DkUrfGol.cjs";
7
7
  import { Server } from "node:http";
8
8
  //#region src/index.d.ts
9
9
  interface CreateDevtoolsOptions {
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { n as SpanAttributes, t as AttributeValue } from "./types-DHDvZnnn.js";
2
- import { a as SpanData, i as LogData, n as ErrorGroup, o as TraceData, t as DevtoolsData } from "./types-DM8y4A9Z.js";
3
- import { n as DevtoolsServer, t as DevtoolsSpanExporter } from "./exporter-C88W22vY.js";
1
+ import { n as SpanAttributes, t as AttributeValue } from "./types-hVSf0DoT.js";
2
+ import { a as SpanData, i as LogData, n as ErrorGroup, o as TraceData, t as DevtoolsData } from "./types-Do48caKG.js";
3
+ import { n as DevtoolsServer, t as DevtoolsSpanExporter } from "./exporter-BRSxg3Du.js";
4
4
  import { DevtoolsLogExporter } from "./server/log-exporter.js";
5
5
  import { DevtoolsRemoteExporter } from "./server/remote-exporter.js";
6
- import { t as ErrorAggregator } from "./error-aggregator-B52JI8jL.js";
6
+ import { t as ErrorAggregator } from "./error-aggregator-BzhW482_.js";
7
7
  import { Server } from "node:http";
8
8
  //#region src/index.d.ts
9
9
  interface CreateDevtoolsOptions {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as ErrorAggregator, g as hostHeaderIsLoopback, l as DevtoolsServer, r as resolveSourceRoot, t as attachDevtoolsRoutes } from "./http-gk1xapnA.js";
1
+ import { C as ErrorAggregator, g as hostHeaderIsLoopback, l as DevtoolsServer, r as resolveSourceRoot, t as attachDevtoolsRoutes } from "./http-JTSqQPVJ.js";
2
2
  import { t as listenLoopbackDualStack } from "./listen-D-lLgfro.js";
3
3
  import { DevtoolsSpanExporter } from "./server/exporter.js";
4
4
  import { DevtoolsLogExporter } from "./server/log-exporter.js";
@@ -1,2 +1,2 @@
1
- import { t as DevtoolsSpanExporter } from "../exporter-OoWkSMCR.cjs";
1
+ import { t as DevtoolsSpanExporter } from "../exporter-BgjMeWvt.cjs";
2
2
  export { DevtoolsSpanExporter };
@@ -1,2 +1,2 @@
1
- import { t as DevtoolsSpanExporter } from "../exporter-C88W22vY.js";
1
+ import { t as DevtoolsSpanExporter } from "../exporter-BRSxg3Du.js";
2
2
  export { DevtoolsSpanExporter };
@@ -1,9 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_http = require('../http-1afd_01N.cjs');
2
+ const require_http = require('../http-Dc57PtwQ.cjs');
3
3
  const require_server_exporter = require('./exporter.cjs');
4
4
  const require_server_log_exporter = require('./log-exporter.cjs');
5
5
  const require_server_remote_exporter = require('./remote-exporter.cjs');
6
- const require_grpc = require('../grpc-D0B3P9sI.cjs');
6
+ const require_grpc = require('../grpc-AXr0CnZ_.cjs');
7
7
 
8
8
  exports.DEVTOOLS_IDENTITY = require_http.DEVTOOLS_IDENTITY;
9
9
  exports.DevtoolsLogExporter = require_server_log_exporter.DevtoolsLogExporter;
@@ -1,9 +1,9 @@
1
- import "../types-DHDvZnnn.cjs";
2
- import { a as SpanData, i as LogData, n as ErrorGroup, o as TraceData, r as ErrorOccurrence, t as DevtoolsData } from "../types-B0tjwFqj.cjs";
3
- import { a as DevtoolsStoreOptions, c as QueryLogsArgs, d as QueryTracesArgs, f as QueryTracesResult, i as DevtoolsStore, l as QueryLogsResult, m as TimeWindow, n as DevtoolsServer, o as MetricCatalogEntry, p as SPAN_SCHEMA, r as DevtoolsServerOptions, s as MetricSeries, t as DevtoolsSpanExporter, u as QueryMetricSeriesArgs } from "../exporter-OoWkSMCR.cjs";
1
+ import "../types-hVSf0DoT.cjs";
2
+ import { a as SpanData, i as LogData, n as ErrorGroup, o as TraceData, r as ErrorOccurrence, t as DevtoolsData } from "../types-ZU74Na4z.cjs";
3
+ import { a as DevtoolsStoreOptions, c as QueryLogsArgs, d as QueryTracesArgs, f as QueryTracesResult, i as DevtoolsStore, l as QueryLogsResult, m as TimeWindow, n as DevtoolsServer, o as MetricCatalogEntry, p as SPAN_SCHEMA, r as DevtoolsServerOptions, s as MetricSeries, t as DevtoolsSpanExporter, u as QueryMetricSeriesArgs } from "../exporter-BgjMeWvt.cjs";
4
4
  import { DevtoolsLogExporter } from "./log-exporter.cjs";
5
5
  import { DevtoolsRemoteExporter, DevtoolsRemoteExporterOptions } from "./remote-exporter.cjs";
6
- import { t as ErrorAggregator } from "../error-aggregator-DSwUvgFF.cjs";
6
+ import { t as ErrorAggregator } from "../error-aggregator-DkUrfGol.cjs";
7
7
  import { AgentRawEvent, OtelMetricRecord } from "autotel-agents";
8
8
  import { Server } from "node:http";
9
9
  //#region src/server/http.d.ts