@nekuda/webmcp-sdk 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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased — usage telemetry schema 2 (breaking wire format)
3
+ ## 0.5.0 — 2026-08-24 — usage telemetry schema 2 (breaking wire format)
4
4
 
5
5
  The usage-telemetry channel emits **three events instead of two**, carrying
6
6
  derived signals instead of raw page and payload strings. `schema` bumps `1` → `2`.
@@ -11,11 +11,11 @@ unchanged. See `docs/telemetry-schema.md`.
11
11
 
12
12
  ### Breaking
13
13
 
14
- - **`sdk_init` fires once per page load, not once per `registerTools` call.** It is
15
- scheduled at import time and flushed on the next task, so it now fires even when
16
- nobody registers anything — which is the only way a broken integration is
17
- visible. A page with three `registerTools` calls no longer emits three "init"
18
- events.
14
+ - **`sdk_init` fires once per page load, not once per `registerTools` call.** The
15
+ first telemetry-live batch schedules a next-task flush with its own key and
16
+ endpoint; an import-time one-second fallback still fires when nobody registers
17
+ anything, which is the only way a broken integration is visible. A page with
18
+ three `registerTools` calls no longer emits three "init" events.
19
19
  - **Per-call information moved to the new `tool_registration` event**, one per
20
20
  `registerTools` call: `registrationIndex`, `trigger`, `settleMs`, the `config.*`
21
21
  booleans (moved off `sdk_init`, which fires before any batch may have run), and
package/dist/index.d.ts CHANGED
@@ -19,8 +19,9 @@
19
19
  * Browsers without a WebMCP surface are a graceful no-op (`state: "unsupported"`).
20
20
  * 4. "Connect later without rewrite": generated modules only `defineTool` and
21
21
  * EXPORT tools; one entry module calls `registerTools`. Connecting a site to the
22
- * platform later (API key, telemetry) changes wrapper internals and config only —
23
- * generated modules and their imports do not change. See `examples/add-to-cart.ts`.
22
+ * platform later adds a publishable tracking key to wrapper config only — generated
23
+ * modules and their imports do not change. The default-on anonymous channel in point 6
24
+ * is independent of Connect. See `examples/add-to-cart.ts`.
24
25
  * 5. Anonymous tool-call tracking is opt-in via `registerTools`'s `tracking`
25
26
  * option (see `TrackingOptions`) and default-silent: with neither `apiKey`
26
27
  * nor `otel` set, no event is built at all — no identity resolution, no
package/dist/index.js CHANGED
@@ -999,7 +999,7 @@ function shapeMetrics(inputSchema) {
999
999
 
1000
1000
  // src/telemetry.ts
1001
1001
  var SDK_NAME = "@nekuda/webmcp-sdk";
1002
- var SDK_VERSION = "0.4.0";
1002
+ var SDK_VERSION = "0.5.0";
1003
1003
  var INSTALL_MODES = ["npm", "cdn_snippet"];
1004
1004
  var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
1005
1005
  function isFieldParent(value) {
@@ -1334,12 +1334,13 @@ function pruneByAllowlist(event, fields = TELEMETRY_FIELDS, toolFields = TELEMET
1334
1334
  return pruned;
1335
1335
  }
1336
1336
  var defaultSinks2 = {
1337
- sendTelemetry: (event) => sendTelemetry(event, globalThis, undefined, telemetryApiKey())
1337
+ sendTelemetry: (event) => sendTelemetry(event, globalThis, telemetryEndpoint(), telemetryApiKey())
1338
1338
  };
1339
- function batchTelemetrySinks(apiKey) {
1339
+ function batchTelemetrySinks(apiKey, endpoint) {
1340
1340
  const own = batchApiKey(apiKey);
1341
+ const ownEndpoint = telemetryEndpointFrom(endpoint);
1341
1342
  return {
1342
- sendTelemetry: (event) => sendTelemetry(event, globalThis, undefined, own ?? telemetryApiKey())
1343
+ sendTelemetry: (event) => sendTelemetry(event, globalThis, ownEndpoint ?? telemetryEndpoint(), own ?? telemetryApiKey())
1343
1344
  };
1344
1345
  }
1345
1346
  function batchApiKey(apiKey) {
@@ -1361,7 +1362,9 @@ function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS,
1361
1362
  }
1362
1363
  var initCancelled = false;
1363
1364
  var initFlushed = false;
1365
+ var cancelInitFallback = () => {};
1364
1366
  var capturedApiKey;
1367
+ var capturedEndpoint;
1365
1368
  function captureTelemetryApiKey(apiKey) {
1366
1369
  if (typeof apiKey === "string" && apiKey.trim().length > 0)
1367
1370
  capturedApiKey = apiKey;
@@ -1369,17 +1372,44 @@ function captureTelemetryApiKey(apiKey) {
1369
1372
  function telemetryApiKey() {
1370
1373
  return capturedApiKey;
1371
1374
  }
1375
+ function telemetryEndpointFrom(endpoint) {
1376
+ if (typeof endpoint !== "string")
1377
+ return;
1378
+ return safe(() => {
1379
+ const url = new URL(endpoint);
1380
+ if (!["http:", "https:"].includes(url.protocol) || !url.pathname.endsWith("/v1/collect") || url.search !== "" || url.hash !== "") {
1381
+ return;
1382
+ }
1383
+ url.pathname = `${url.pathname.slice(0, -"/v1/collect".length)}/v1/telemetry`;
1384
+ return url.toString();
1385
+ });
1386
+ }
1387
+ function captureTelemetryEndpoint(endpoint) {
1388
+ const derived = telemetryEndpointFrom(endpoint);
1389
+ if (derived !== undefined)
1390
+ capturedEndpoint = derived;
1391
+ }
1392
+ function telemetryEndpoint() {
1393
+ return capturedEndpoint;
1394
+ }
1372
1395
  function cancelInitEvent() {
1373
1396
  initCancelled = true;
1397
+ cancelInitFallback();
1374
1398
  }
1375
1399
  function flushInitEvent(sinks) {
1376
1400
  if (initFlushed)
1377
1401
  return;
1378
1402
  initFlushed = true;
1403
+ cancelInitFallback();
1379
1404
  if (initCancelled)
1380
1405
  return;
1381
1406
  emitTelemetry(() => buildInitEvent(), sinks);
1382
1407
  }
1408
+ function deferInitEventForBatch(sinks) {
1409
+ if (initFlushed || initCancelled)
1410
+ return;
1411
+ afterDelay(() => flushInitEvent(sinks), 0);
1412
+ }
1383
1413
  function afterDelay(run, ms) {
1384
1414
  const schedule = safe(() => globalThis.setTimeout);
1385
1415
  if (typeof schedule !== "function")
@@ -1392,7 +1422,8 @@ function afterDelay(run, ms) {
1392
1422
  safe(() => clear.call(globalThis, handle));
1393
1423
  };
1394
1424
  }
1395
- afterDelay(() => flushInitEvent(), 0);
1425
+ var INIT_FALLBACK_MS = 1000;
1426
+ cancelInitFallback = afterDelay(() => flushInitEvent(), INIT_FALLBACK_MS);
1396
1427
 
1397
1428
  // src/register.ts
1398
1429
  function isContentResult(value) {
@@ -1438,7 +1469,7 @@ function trackerFor(tool, tracking) {
1438
1469
  return (eventName, data) => track(tracking, eventName, { ...shared, ...data });
1439
1470
  }
1440
1471
  function toSpecTool(tool, channels) {
1441
- const { tracking, telemetry, telemetrySinks, telemetryKey } = channels;
1472
+ const { tracking, telemetry, telemetrySinks, telemetryKey, telemetryEndpoint: telemetryEndpoint2 } = channels;
1442
1473
  return {
1443
1474
  name: tool.name,
1444
1475
  ...tool.title !== undefined ? { title: tool.title } : {},
@@ -1451,7 +1482,7 @@ function toSpecTool(tool, channels) {
1451
1482
  return normalizeResult(await tool.execute(input));
1452
1483
  const callKey = telemetry ? resolveTelemetryKey(telemetryKey) : undefined;
1453
1484
  const sequence = telemetry ? nextCall(tool.stableKey, telemetryTenantScope(callKey)) : undefined;
1454
- const callSinks = sequence ? batchTelemetrySinks(callKey) : telemetrySinks;
1485
+ const callSinks = sequence ? batchTelemetrySinks(callKey, telemetryEndpoint2) : telemetrySinks;
1455
1486
  const elapsed = clock();
1456
1487
  trackCall?.("tool_call_request", { input });
1457
1488
  try {
@@ -1543,15 +1574,24 @@ function registerTools(tools, options = {}) {
1543
1574
  const tracking = safe(() => options.tracking);
1544
1575
  const telemetryLive = telemetryOption !== undefined && telemetryEnabled(telemetryOption.value);
1545
1576
  const apiKey = telemetryLive ? safe(() => tracking?.apiKey) : undefined;
1577
+ const endpoint = telemetryLive ? safe(() => tracking?.endpoint) : undefined;
1578
+ const telemetrySinks = telemetryLive ? batchTelemetrySinks(apiKey, endpoint) : undefined;
1546
1579
  const channels = {
1547
1580
  tracking,
1548
1581
  telemetry: telemetryLive,
1549
- ...telemetryLive ? { telemetrySinks: batchTelemetrySinks(apiKey), telemetryKey: apiKey } : {}
1582
+ ...telemetryLive ? {
1583
+ telemetrySinks,
1584
+ telemetryKey: apiKey,
1585
+ telemetryEndpoint: endpoint
1586
+ } : {}
1550
1587
  };
1551
1588
  if (telemetryOption?.value === false)
1552
1589
  cancelInitEvent();
1553
- if (telemetryLive)
1590
+ if (telemetrySinks) {
1554
1591
  captureTelemetryApiKey(apiKey);
1592
+ captureTelemetryEndpoint(endpoint);
1593
+ deferInitEventForBatch(telemetrySinks);
1594
+ }
1555
1595
  assertUniqueIdentities(tools);
1556
1596
  const controller = new AbortController;
1557
1597
  const external = options.signal;
@@ -1601,7 +1641,7 @@ function registerTools(tools, options = {}) {
1601
1641
  };
1602
1642
  }
1603
1643
  export {
1604
- resolveModelContext,
1644
+ defineTool,
1605
1645
  registerTools,
1606
- defineTool
1646
+ resolveModelContext
1607
1647
  };
@@ -44,10 +44,11 @@ export interface RegisterToolsOptions {
44
44
  *
45
45
  * The other two events are per-batch, so the option is read per call: a *later*
46
46
  * `registerTools` that omits it reports its own batch and calls. Cancelling
47
- * `sdk_init` also requires winning the race with its deferred flush — a batch
48
- * registered at module scope does, one registered from a mount effect or a consent
49
- * callback may not. A site that wants the whole page silent regardless of where its
50
- * `registerTools` calls come from (generated code, a CDN snippet) should use a
47
+ * `sdk_init` also requires winning the race with its deferred flush. A batch
48
+ * registered at module scope or from an ordinary mount effect schedules the keyed
49
+ * batch flush; a later consent decision can still arrive after the bounded
50
+ * no-registration fallback. A site that wants the whole page silent regardless of
51
+ * when its `registerTools` calls run (generated code, a CDN snippet) should use a
51
52
  * page-level lever instead: `globalThis.__WEBMCP_TELEMETRY__ = false` — strictly
52
53
  * `false`, so a truthy `"0"` does not opt out — or Global Privacy Control
53
54
  * (`navigator.globalPrivacyControl === true`), both of which are read at emit time
@@ -360,7 +360,7 @@ export interface TelemetrySinks {
360
360
  * the raw batch option: a key resolved to a value pins it, closing the window in which
361
361
  * the module key changes between the claim and the emit.
362
362
  */
363
- export declare function batchTelemetrySinks(apiKey: unknown): TelemetrySinks;
363
+ export declare function batchTelemetrySinks(apiKey: unknown, endpoint?: unknown): TelemetrySinks;
364
364
  /**
365
365
  * The key a batch's beacons authenticate with, resolved now instead of at emit time:
366
366
  * its own when it supplied one, otherwise the page's.
@@ -438,6 +438,15 @@ export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: Telem
438
438
  export declare function captureTelemetryApiKey(apiKey: unknown): void;
439
439
  /** The key {@link captureTelemetryApiKey} last recorded, if any. */
440
440
  export declare function telemetryApiKey(): string | undefined;
441
+ /**
442
+ * Record the telemetry endpoint for the deferred `sdk_init` flush. Like the captured
443
+ * key, a later usable value overwrites an earlier one while an absent or malformed
444
+ * value leaves the last usable value in place. URL parsing is best-effort because an
445
+ * observability option must never break the visitor's page.
446
+ */
447
+ export declare function captureTelemetryEndpoint(endpoint: unknown): void;
448
+ /** The endpoint {@link captureTelemetryEndpoint} last recorded, if any. */
449
+ export declare function telemetryEndpoint(): string | undefined;
441
450
  /**
442
451
  * Cancel the deferred `sdk_init` below. Called by `registerTools({ telemetry: false })`,
443
452
  * whose opt-out is page-level and not merely batch-level: the flush describes the
@@ -462,6 +471,12 @@ export declare function cancelInitEvent(): void;
462
471
  * Exported for tests; the scheduled timer below is the one that runs it on a real page.
463
472
  */
464
473
  export declare function flushInitEvent(sinks?: TelemetrySinks): void;
474
+ /**
475
+ * Give a telemetry-live registration batch the next task's `sdk_init` flush. This
476
+ * keeps the batch's key and endpoint together while leaving the rest of the current
477
+ * task available for a page-level opt-out or another synchronous registration.
478
+ */
479
+ export declare function deferInitEventForBatch(sinks: TelemetrySinks): void;
465
480
  /**
466
481
  * Run `run` after `ms`, returning a canceller. Guarded end to end: a runtime with no
467
482
  * timers (SSR) and a hostile `setTimeout` (an extension, a fake-timer harness) both
@@ -469,3 +484,5 @@ export declare function flushInitEvent(sinks?: TelemetrySinks): void;
469
484
  * `registerTools`. The canceller is safe to call when nothing was scheduled.
470
485
  */
471
486
  export declare function afterDelay(run: () => void, ms: number): () => void;
487
+ /** How long a page that never registers gets to expose a real batch configuration. */
488
+ export declare const INIT_FALLBACK_MS = 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nekuda/webmcp-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Phase-1 WebMCP SDK: a thin wrapper over document.modelContext that plugin-generated code targets — defineTool + register/unregister lifecycle. This package pins the plugin↔SDK seam; anonymous tool-call tracking (backend transport via apiKey, OTEL LogRecords via otel) is opt-in through registerTools and default-silent, while anonymous usage telemetry is a separate unauthenticated channel that is on by default (opt out with telemetry: false).",
6
6
  "main": "./dist/index.js",