@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 +6 -6
- package/dist/index.d.ts +3 -2
- package/dist/index.js +51 -11
- package/dist/register.d.ts +5 -4
- package/dist/telemetry.d.ts +18 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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.**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
23
|
-
*
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 ? {
|
|
1582
|
+
...telemetryLive ? {
|
|
1583
|
+
telemetrySinks,
|
|
1584
|
+
telemetryKey: apiKey,
|
|
1585
|
+
telemetryEndpoint: endpoint
|
|
1586
|
+
} : {}
|
|
1550
1587
|
};
|
|
1551
1588
|
if (telemetryOption?.value === false)
|
|
1552
1589
|
cancelInitEvent();
|
|
1553
|
-
if (
|
|
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
|
-
|
|
1644
|
+
defineTool,
|
|
1605
1645
|
registerTools,
|
|
1606
|
-
|
|
1646
|
+
resolveModelContext
|
|
1607
1647
|
};
|
package/dist/register.d.ts
CHANGED
|
@@ -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
|
|
48
|
-
* registered at module scope
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -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.
|
|
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",
|