@nekuda/webmcp-sdk 0.4.0 → 0.6.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 +102 -6
- package/README.md +32 -0
- package/dist/define.d.ts +39 -0
- package/dist/index.d.ts +14 -6
- package/dist/index.js +300 -49
- package/dist/pages.d.ts +6 -0
- package/dist/register.d.ts +49 -9
- package/dist/spec.d.ts +15 -4
- package/dist/telemetry-events.d.ts +26 -2
- package/dist/telemetry-fields.d.ts +4 -0
- package/dist/telemetry.d.ts +47 -2
- package/dist/tracking.d.ts +31 -0
- package/dist/transport.d.ts +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.6.0 — 2026-09-18
|
|
4
|
+
|
|
5
|
+
Five additive changes, no breaking change. The wire schema stays at `2`. Four of them
|
|
6
|
+
are opt-in options — `builtWith`, `pages`, `sessionId` and `createCallTracker` — each
|
|
7
|
+
silent when unset, so **an npm consumer who sets none of them sends exactly the bytes
|
|
8
|
+
0.5.0 sent**. The fifth is a non-enumerable mark this SDK writes on the tool objects it
|
|
9
|
+
registers under a batch that is already posting to the collect edge; it is invisible to
|
|
10
|
+
enumeration and to `JSON` and never reaches the wire, so it changes no bytes either.
|
|
11
|
+
|
|
12
|
+
One behaviour is **CDN-build-only**. Page-level telemetry sampling is switched on by a
|
|
13
|
+
`bun build --define`, which only `infra/deploy-snippet.sh` passes when it builds the CDN
|
|
14
|
+
snippet; neither publish lane passes it, so **an npm install is unsampled** and sends
|
|
15
|
+
every page-level event, exactly as before.
|
|
16
|
+
|
|
17
|
+
### Page-level telemetry sampling — CDN builds only
|
|
18
|
+
|
|
19
|
+
- A build may sample the two page-level telemetry events (`sdk_init`,
|
|
20
|
+
`tool_registration`) with `--define '__WEBMCP_TELEMETRY_SAMPLE_RATE__="0.1"'`. The
|
|
21
|
+
decision is one coin per page load from the `sessionId`, so a page's events are kept
|
|
22
|
+
or dropped together, and every kept event carries `sampleRate` for weighting.
|
|
23
|
+
`tool_call` is never sampled. Unset, or anything outside (0, 1), sends everything,
|
|
24
|
+
which is the npm default and the previous behaviour byte for byte. The CDN snippet
|
|
25
|
+
builds at `0.1`: installed sites had reached five million page-level beacons a day.
|
|
26
|
+
|
|
27
|
+
### `tracking.builtWith`
|
|
28
|
+
|
|
29
|
+
- **`TrackingOptions.builtWith`** names what generated the integration, as
|
|
30
|
+
`<tool>[/<path>]@<version>` (`webmcp-kit/implement@<plugin version>`,
|
|
31
|
+
`webmcp-kit/connect-existing-tools@<plugin version>`), and is reported verbatim
|
|
32
|
+
as `config.builtWith` on `tool_registration`. Additive and optional: unset emits
|
|
33
|
+
exactly the bytes it did before. It exists so kit-built sites are countable on
|
|
34
|
+
the default-on channel without a Connect. Dropped, never truncated, when empty
|
|
35
|
+
or over 64 characters.
|
|
36
|
+
|
|
37
|
+
### Page-scoped tools
|
|
38
|
+
|
|
39
|
+
- `defineTool({ pages })` filters registration on load and SPA navigation; `matchPage`
|
|
40
|
+
and `currentPageKey` expose the fixture-pinned matching rule. SDK metadata stays off
|
|
41
|
+
the native tool object. `ready` covers the initial filtered set; `current()` and
|
|
42
|
+
`onChange` expose live names. Unregistration uses native abort signals, with legacy
|
|
43
|
+
`unregisterTool` support and explicit retention on surfaces supporting neither.
|
|
44
|
+
|
|
45
|
+
### A host may own the session and track its own tools
|
|
46
|
+
|
|
47
|
+
Two additive `tracking`-channel surfaces for a host that owns the page it runs on
|
|
48
|
+
(the CDN snippet). Nothing existing changes: both are opt-in, both are silent
|
|
49
|
+
under the same `trackingOutputs` gate as everything else on this channel, and a
|
|
50
|
+
consumer that sets neither emits exactly the bytes it did before.
|
|
51
|
+
|
|
52
|
+
- **`TrackingOptions.sessionId`** reports under a session identity the host
|
|
53
|
+
already has — a tab session that predates any `registerTools` call, or a
|
|
54
|
+
Journey runner's synthetic `syn_…` id — instead of the one this channel mints
|
|
55
|
+
in `sessionStorage` under an `apiKey`-derived namespace, which would split one
|
|
56
|
+
visit into two sessions the pipeline cannot rejoin. Supplying it means no
|
|
57
|
+
`sessionStorage` read or write at all for that batch, `last_seen` included, so
|
|
58
|
+
the 30-minute inactivity boundary becomes the host's to enforce; `visitorId` is
|
|
59
|
+
untouched. Validated like a stored id (non-empty, ≤ 64 chars, a string) —
|
|
60
|
+
anything else falls back to the minted session, because identity fields skip the
|
|
61
|
+
truncation ladder.
|
|
62
|
+
- **`createCallTracker(tool, tracking)`** emits the request/response pair for a
|
|
63
|
+
tool the host registered on `document.modelContext` itself, in the same bytes an
|
|
64
|
+
SDK-registered tool's call produces — same event names, same correlated `callId`,
|
|
65
|
+
same anonymous identity — so the projection reads one shape rather than two. The
|
|
66
|
+
host owns the rest: one tracker per invocation, `tool_call_request` before the
|
|
67
|
+
handler and `tool_call_response` after, and the `duration_ms` it reports. The
|
|
68
|
+
tool's `name` is its `stableKey`, since a host-wrapped tool has no
|
|
69
|
+
developer-authored durable identity to carry — and that rule is applied *over*
|
|
70
|
+
the caller's object, so a `stableKey` riding in on a tool descriptor from a page
|
|
71
|
+
the host does not control cannot key that tool differently from every other
|
|
72
|
+
reader of the same page.
|
|
73
|
+
|
|
74
|
+
### A tracked tool says so, for a host that owns the surface
|
|
75
|
+
|
|
76
|
+
- A tool registered by a batch that posts to the collect edge now carries the registry
|
|
77
|
+
symbol `Symbol.for("webmcp.sdk.tracked")` (non-enumerable, value `true`) on the object
|
|
78
|
+
handed to `registerTool`. It exists for a host that owns the page's WebMCP surface and
|
|
79
|
+
wraps what it finds there — the CDN snippet's coexistence gate — which could not
|
|
80
|
+
otherwise tell a tool whose calls this SDK already reports from a merchant's bare one,
|
|
81
|
+
and reported every such invocation a second time. Nothing else changes: the mark is
|
|
82
|
+
written only when the channel really is posting (an unkeyed batch, an empty
|
|
83
|
+
`tracking: {}`, a closed consent gate and an otel-only batch carry none), it is
|
|
84
|
+
invisible to enumeration and to `JSON`, and no export is added. It is the one change
|
|
85
|
+
here that is not an option a consumer sets — but being non-enumerable it reaches
|
|
86
|
+
neither the wire nor `JSON.stringify`, so no bytes move.
|
|
87
|
+
|
|
88
|
+
## 0.5.0 — 2026-08-24 — usage telemetry schema 2 (breaking wire format)
|
|
4
89
|
|
|
5
90
|
The usage-telemetry channel emits **three events instead of two**, carrying
|
|
6
91
|
derived signals instead of raw page and payload strings. `schema` bumps `1` → `2`.
|
|
@@ -11,11 +96,11 @@ unchanged. See `docs/telemetry-schema.md`.
|
|
|
11
96
|
|
|
12
97
|
### Breaking
|
|
13
98
|
|
|
14
|
-
- **`sdk_init` fires once per page load, not once per `registerTools` call.**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
events.
|
|
99
|
+
- **`sdk_init` fires once per page load, not once per `registerTools` call.** The
|
|
100
|
+
first telemetry-live batch schedules a next-task flush with its own key and
|
|
101
|
+
endpoint; an import-time one-second fallback still fires when nobody registers
|
|
102
|
+
anything, which is the only way a broken integration is visible. A page with
|
|
103
|
+
three `registerTools` calls no longer emits three "init" events.
|
|
19
104
|
- **Per-call information moved to the new `tool_registration` event**, one per
|
|
20
105
|
`registerTools` call: `registrationIndex`, `trigger`, `settleMs`, the `config.*`
|
|
21
106
|
booleans (moved off `sdk_init`, which fires before any batch may have run), and
|
|
@@ -65,6 +150,17 @@ unchanged. See `docs/telemetry-schema.md`.
|
|
|
65
150
|
|
|
66
151
|
### Added
|
|
67
152
|
|
|
153
|
+
- **One `console.error` when a configured publishable key is refused.** The telemetry
|
|
154
|
+
route now answers `401` for a key that was *sent* and did not resolve; the beacon is
|
|
155
|
+
still recorded, anonymously, so nothing is lost but the attribution. The SDK reports
|
|
156
|
+
that at most once per page load — naming the SDK, saying recording continues
|
|
157
|
+
anonymously before saying what broke, and pointing at re-running Connect. It never
|
|
158
|
+
echoes the key or anything off the wire. No throw, no retry, no second send, and no
|
|
159
|
+
change to the send path. Anonymous beacons are never inspected (they have no key to
|
|
160
|
+
be wrong about), and every other status, a network failure, and an environment with
|
|
161
|
+
no `console` stay silent as before. This narrows 0.2.0's "the SDK is silent by
|
|
162
|
+
default": it is the only output the package produces, and only a broken key produces
|
|
163
|
+
it.
|
|
68
164
|
- **`globalThis.__WEBMCP_TELEMETRY__ = false`** silences the channel page-wide
|
|
69
165
|
without touching a `registerTools` call site — the only lever a site has when the
|
|
70
166
|
calls come from generated code it does not edit. Strictly `false`, like GPC is
|
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @nekuda/webmcp-sdk
|
|
2
|
+
|
|
3
|
+
Define tools with `defineTool`, then pass them to `registerTools(tools, options)`.
|
|
4
|
+
`ready` resolves with initial per-tool outcomes; `unregister()` or `options.signal`
|
|
5
|
+
ends the batch. Browsers without a WebMCP surface report `unsupported`.
|
|
6
|
+
|
|
7
|
+
## Page-scoped tools
|
|
8
|
+
|
|
9
|
+
Set `pages: ["/products/*"]` on a tool definition to register it only on matching
|
|
10
|
+
pages. Missing/empty `pages` means everywhere; lists match any entry. `*` matches
|
|
11
|
+
any characters within one segment (`/products/*`, `/product.html*`), `**` zero or
|
|
12
|
+
more segments; queries and trailing slashes are ignored on both patterns and locations,
|
|
13
|
+
and repeated slashes collapse to one. Hash
|
|
14
|
+
routers use patterns such as `/#/product/*`. With no readable location (SSR),
|
|
15
|
+
all tools are eligible because the page cannot be evaluated.
|
|
16
|
+
|
|
17
|
+
The SDK reconciles tools on `pushState`, `replaceState`, `popstate`, and
|
|
18
|
+
`hashchange`, sharing one route listener. If History cannot be patched, registration
|
|
19
|
+
still succeeds and navigation detection falls back to `popstate`/`hashchange` only.
|
|
20
|
+
A failing route subscriber never escapes into the merchant's History calls or blocks
|
|
21
|
+
other subscribers. `registration.current()` lists live
|
|
22
|
+
names; `options.onChange(names)` keeps host displays in sync. `ready` covers only
|
|
23
|
+
the initially eligible tools. SDK-only `pages` never reaches native `registerTool`.
|
|
24
|
+
|
|
25
|
+
The [WebMCP draft](https://webmachinelearning.github.io/webmcp/), checked
|
|
26
|
+
2026-09-10, unregisters via the registration's **AbortSignal**, not
|
|
27
|
+
`unregisterTool`; this works on either resolved modelContext global. Legacy
|
|
28
|
+
surfaces exposing `unregisterTool(name)` are supported too. A surface that neither
|
|
29
|
+
reads the registration signal nor exposes that method keeps a tool once registered;
|
|
30
|
+
the SDK reports it as still live rather than claiming removal. Ending a batch always
|
|
31
|
+
removes its route subscription. Fixtures under `fixtures/` pin matching semantics
|
|
32
|
+
for tests and stay out of the published package.
|
package/dist/define.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import type { ToolAnnotations } from "./spec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Spec rule: tool names are 1–128 chars of ASCII alphanumerics, `_`, `-`, `.`.
|
|
4
|
+
*
|
|
5
|
+
* Exported for the seam pin only (`tests/tool-key-patterns.test.ts`), not re-exported by
|
|
6
|
+
* `index.ts`: the platform's catalog CHECKs the same shape, and a widening here that the
|
|
7
|
+
* database refuses would reject tools this SDK already accepted in the wild.
|
|
8
|
+
*/
|
|
9
|
+
export declare const NAME_PATTERN: RegExp;
|
|
10
|
+
/**
|
|
11
|
+
* `stableKey` rule: dot-namespaced `domain.action` — two or more `[a-z0-9_]+`
|
|
12
|
+
* segments joined by `.`. Rejects the common authoring mistake of copying the
|
|
13
|
+
* wire `name` into `stableKey` (e.g. `search_blog_posts`), which defeats the
|
|
14
|
+
* field's purpose: a `name` can be renamed freely, but a `stableKey` that is
|
|
15
|
+
* just a `name` copy renames right along with it.
|
|
16
|
+
*
|
|
17
|
+
* Exported for the seam pin only — see {@link NAME_PATTERN}.
|
|
18
|
+
*/
|
|
19
|
+
export declare const STABLE_KEY_PATTERN: RegExp;
|
|
2
20
|
/** How the tool came to exist. Reported on telemetry as `tools[].source`. */
|
|
3
21
|
export type ToolSource = "scanner_generated" | "merchant_authored";
|
|
4
22
|
/** What the tool is for. Reported on telemetry as `tools[].intent`. */
|
|
@@ -33,6 +51,8 @@ export interface ToolDefinition<TInput extends Record<string, unknown> = Record<
|
|
|
33
51
|
/** JSON Schema for `execute`'s input, as a plain object. */
|
|
34
52
|
inputSchema?: Record<string, unknown>;
|
|
35
53
|
annotations?: ToolAnnotations;
|
|
54
|
+
/** Page patterns where this tool is available; absent/empty means everywhere. SDK-only. */
|
|
55
|
+
pages?: string[];
|
|
36
56
|
/**
|
|
37
57
|
* Optional per-tool version, surfaced on emitted tracking events as `toolVersion`
|
|
38
58
|
* for drift analytics. Free-form string (e.g. semver or a codegen hash); when
|
|
@@ -51,6 +71,25 @@ export interface ToolDefinition<TInput extends Record<string, unknown> = Record<
|
|
|
51
71
|
* rather than per-batch because one tool answers while another transacts.
|
|
52
72
|
*/
|
|
53
73
|
intent?: ToolIntent;
|
|
74
|
+
/**
|
|
75
|
+
* The identity the connected platform assigned this tool, if the host knows it —
|
|
76
|
+
* an opaque string the SDK copies and never interprets.
|
|
77
|
+
*
|
|
78
|
+
* `stableKey` is the DEVELOPER's durable identity and survives renames; this is the
|
|
79
|
+
* server's, handed back once a site is connected. Both are reported because they
|
|
80
|
+
* answer different questions: a key the developer chose can collide across two
|
|
81
|
+
* scopes of one site, while the assigned id cannot, and only the developer's key
|
|
82
|
+
* exists before a site is connected at all. Absent is the ordinary state — a tool
|
|
83
|
+
* declared in a codebase that has never been connected simply has no such id, and
|
|
84
|
+
* the SDK never invents one.
|
|
85
|
+
*/
|
|
86
|
+
inventoryToolId?: string;
|
|
87
|
+
/**
|
|
88
|
+
* The contract revision the host believes this tool matches, if it knows one.
|
|
89
|
+
* Reported so a stale bundle can be told apart from a tool that genuinely changed
|
|
90
|
+
* shape; never validated here, and never used for anything on the page.
|
|
91
|
+
*/
|
|
92
|
+
contractRevision?: number;
|
|
54
93
|
/**
|
|
55
94
|
* The page-owned behavior. May return anything JSON-serializable, a plain string,
|
|
56
95
|
* or a ready-made `{ content: [...] }` result — the SDK normalizes for the agent.
|
package/dist/index.d.ts
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* and is never sent to the browser.
|
|
15
15
|
* 3. Registration lifecycle: `registerTools(tools, { signal?, tracking?, telemetry? })`
|
|
16
16
|
* registers on call
|
|
17
|
-
* and returns `{ ready, unregister, signal }`.
|
|
18
|
-
* `unregister()` or
|
|
17
|
+
* and returns `{ ready, current, unregister, signal }`. Page-scoped tools also
|
|
18
|
+
* register/unregister as routes change; `unregister()` or an external abort ends the batch.
|
|
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
|
|
@@ -37,7 +38,13 @@
|
|
|
37
38
|
* both outputs — but for THIS channel only; the default-on telemetry channel of
|
|
38
39
|
* point 6 has its own opt-out (`telemetry: false`) and `disabled` does not
|
|
39
40
|
* narrow it. See `src/tracking.ts`, `src/transport.ts`, and
|
|
40
|
-
* `examples/add-to-cart.ts`.
|
|
41
|
+
* `examples/add-to-cart.ts`. `TrackingOptions.sessionId` lets a host that is
|
|
42
|
+
* already the page's session authority report under its own id instead of the
|
|
43
|
+
* one this channel mints; `createCallTracker(tool, tracking)` lets that same
|
|
44
|
+
* host emit the pair for a tool it registered on the surface *itself*, in the
|
|
45
|
+
* same bytes. Both exist for one caller — a host of this SDK that owns the page
|
|
46
|
+
* (the CDN snippet) — and neither is part of what plugin codegen targets:
|
|
47
|
+
* generated code registers tools and reads nothing here.
|
|
41
48
|
* 6. Anonymous usage telemetry is a SECOND, independent channel: default-ON, so it
|
|
42
49
|
* reports from every site the SDK runs on, not only from `apiKey` tenants. Three
|
|
43
50
|
* events (`TelemetryEvent`), all `schema: 2` and joined on an in-memory
|
|
@@ -73,7 +80,8 @@
|
|
|
73
80
|
* platform stays a config-only change to the entry module (point 4).
|
|
74
81
|
*/
|
|
75
82
|
export { type AnyWebMCPTool, type ToolDefinition, type ToolIntent, type ToolSource, type WebMCPTool, defineTool, } from "./define.js";
|
|
76
|
-
export {
|
|
83
|
+
export { currentPageKey, matchPage } from "./pages.js";
|
|
84
|
+
export { type CallTracker, type RegisterToolsOptions, type ToolRegistration, type ToolRegistrationResult, type ToolRegistrationState, type TrackedCall, createCallTracker, registerTools, } from "./register.js";
|
|
77
85
|
export { type ModelContextLike, type RegisterToolOptions, type SpecTool, type ToolAnnotations, resolveModelContext, } from "./spec.js";
|
|
78
86
|
export type { AgentRuntime, FormFactor, FrameContext, PageVisibility, ReferrerClass, SurfaceGlobal, SurfaceInfo, SurfaceProvenance, } from "./telemetry-context.js";
|
|
79
87
|
export type { ClientContext, InstallMode, PageContext, RegisteredToolEntry, RegistrationTrigger, SdkInfo, SdkInitEvent, StrippedToolEntry, TelemetryEnvelope, TelemetryEvent, TelemetryEventName, ToolCallEvent, ToolCallOutcome, ToolCallResponseMetrics, ToolCallToolInfo, ToolRegistrationEvent, ToolRegistrationOutcome, TrackingConfigInfo, TruncatedTools, } from "./telemetry-events.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
1
|
// src/define.ts
|
|
10
2
|
var NAME_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
11
3
|
var STABLE_KEY_PATTERN = /^[a-z0-9_]+(\.[a-z0-9_]+)+$/;
|
|
@@ -57,14 +49,89 @@ function defineTool(definition) {
|
|
|
57
49
|
fail(field, `must be one of ${allowed.join(" | ")} when present (got ${JSON.stringify(value)})`);
|
|
58
50
|
}
|
|
59
51
|
}
|
|
52
|
+
if (definition.pages !== undefined && (!Array.isArray(definition.pages) || definition.pages.some((page) => typeof page !== "string"))) {
|
|
53
|
+
fail("pages", "must be an array of strings when present");
|
|
54
|
+
}
|
|
60
55
|
if (typeof execute !== "function") {
|
|
61
56
|
fail("execute", "must be a function");
|
|
62
57
|
}
|
|
63
58
|
return Object.freeze({ ...definition, name });
|
|
64
59
|
}
|
|
60
|
+
// src/pages.ts
|
|
61
|
+
function pageKey(value) {
|
|
62
|
+
const [path = "", hash = ""] = value.split("#", 2);
|
|
63
|
+
const key = path.split("?", 1)[0] + (hash.startsWith("/") ? `#${hash.split("?", 1)[0]}` : "");
|
|
64
|
+
return `/${key}`.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
65
|
+
}
|
|
66
|
+
function matchPage(patterns, location) {
|
|
67
|
+
if (!patterns?.length)
|
|
68
|
+
return true;
|
|
69
|
+
const key = pageKey(location);
|
|
70
|
+
return patterns.some((pattern) => {
|
|
71
|
+
if (!pattern)
|
|
72
|
+
return true;
|
|
73
|
+
const expression = pageKey(pattern).split("/").slice(1).map((segment) => segment === "**" ? "(?:/[^/]+)*" : `/${segment.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")}`).join("");
|
|
74
|
+
return new RegExp(`^${expression}$`).test(key);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function currentPageKey() {
|
|
78
|
+
try {
|
|
79
|
+
const location = globalThis.location;
|
|
80
|
+
return location ? `${location.pathname}${location.search ?? ""}${location.hash ?? ""}` : undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
var ROUTES = Symbol.for("webmcp.sdk.page-routes");
|
|
86
|
+
function onPageChange(listener) {
|
|
87
|
+
if (typeof globalThis.addEventListener !== "function" || !globalThis.history)
|
|
88
|
+
return () => {};
|
|
89
|
+
const history = globalThis.history;
|
|
90
|
+
let routes = history[ROUTES];
|
|
91
|
+
if (!routes) {
|
|
92
|
+
const listeners = new Set;
|
|
93
|
+
routes = {
|
|
94
|
+
listeners,
|
|
95
|
+
notify: () => {
|
|
96
|
+
for (const run of listeners) {
|
|
97
|
+
try {
|
|
98
|
+
run();
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const { notify } = routes;
|
|
104
|
+
let patched = false;
|
|
105
|
+
try {
|
|
106
|
+
history[ROUTES] = routes;
|
|
107
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
108
|
+
const original = history[method];
|
|
109
|
+
history[method] = function(...args) {
|
|
110
|
+
const result = original.apply(this, args);
|
|
111
|
+
if (patched)
|
|
112
|
+
notify();
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
patched = true;
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
const { listeners, notify } = routes;
|
|
120
|
+
if (listeners.size === 0) {
|
|
121
|
+
globalThis.addEventListener("popstate", notify);
|
|
122
|
+
globalThis.addEventListener("hashchange", notify);
|
|
123
|
+
}
|
|
124
|
+
listeners.add(listener);
|
|
125
|
+
return () => {
|
|
126
|
+
listeners.delete(listener);
|
|
127
|
+
if (listeners.size === 0) {
|
|
128
|
+
globalThis.removeEventListener("popstate", notify);
|
|
129
|
+
globalThis.removeEventListener("hashchange", notify);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
65
133
|
// src/spec.ts
|
|
66
|
-
function resolveModelContext(
|
|
67
|
-
const scope = g;
|
|
134
|
+
function resolveModelContext(scope = globalThis) {
|
|
68
135
|
return scope.document?.modelContext ?? scope.navigator?.modelContext;
|
|
69
136
|
}
|
|
70
137
|
|
|
@@ -72,7 +139,7 @@ function resolveModelContext(g = globalThis) {
|
|
|
72
139
|
var INGEST_BASE = "https://ingest.agentlane.com";
|
|
73
140
|
var DEFAULT_COLLECT_ENDPOINT = `${INGEST_BASE}/v1/collect`;
|
|
74
141
|
var DEFAULT_TELEMETRY_ENDPOINT = `${INGEST_BASE}/v1/telemetry`;
|
|
75
|
-
function tryFetch(scope, url, headers, json) {
|
|
142
|
+
function tryFetch(scope, url, headers, json, onResponse) {
|
|
76
143
|
const f = scope.fetch;
|
|
77
144
|
if (typeof f !== "function")
|
|
78
145
|
return;
|
|
@@ -83,11 +150,25 @@ function tryFetch(scope, url, headers, json) {
|
|
|
83
150
|
headers,
|
|
84
151
|
body: json
|
|
85
152
|
});
|
|
86
|
-
if (result && typeof result.
|
|
87
|
-
result.catch(() => {});
|
|
153
|
+
if (result && typeof result.then === "function") {
|
|
154
|
+
Promise.resolve(result).then((response) => onResponse?.(response)).catch(() => {});
|
|
88
155
|
}
|
|
89
156
|
} catch {}
|
|
90
157
|
}
|
|
158
|
+
var HTTP_UNAUTHORIZED = 401;
|
|
159
|
+
var KEY_REJECTED_MESSAGE = "[@nekuda/webmcp-sdk] The configured publishable key was not accepted. " + "Usage is still being recorded, but anonymously. " + "Re-run Connect to restore attributed reporting.";
|
|
160
|
+
var warnedScopes = new WeakSet;
|
|
161
|
+
function warnKeyRejected(scope) {
|
|
162
|
+
if (warnedScopes.has(scope))
|
|
163
|
+
return;
|
|
164
|
+
warnedScopes.add(scope);
|
|
165
|
+
const write = scope.console?.error;
|
|
166
|
+
if (typeof write === "function")
|
|
167
|
+
write.call(scope.console, KEY_REJECTED_MESSAGE);
|
|
168
|
+
}
|
|
169
|
+
function isUnauthorized(response) {
|
|
170
|
+
return typeof response === "object" && response !== null && response.status === HTTP_UNAUTHORIZED;
|
|
171
|
+
}
|
|
91
172
|
function sendToCollect(event, config, scope = globalThis) {
|
|
92
173
|
try {
|
|
93
174
|
const json = JSON.stringify(event);
|
|
@@ -105,9 +186,13 @@ function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
|
105
186
|
return;
|
|
106
187
|
const url = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
|
|
107
188
|
const headers = { "content-type": "application/json" };
|
|
108
|
-
|
|
189
|
+
const authenticated = typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
190
|
+
if (authenticated)
|
|
109
191
|
headers["x-api-key"] = apiKey;
|
|
110
|
-
tryFetch(scope, url, headers, json)
|
|
192
|
+
tryFetch(scope, url, headers, json, authenticated ? (response) => {
|
|
193
|
+
if (isUnauthorized(response))
|
|
194
|
+
warnKeyRejected(scope);
|
|
195
|
+
} : undefined);
|
|
111
196
|
} catch {}
|
|
112
197
|
}
|
|
113
198
|
var OTEL_LOGGER_NAME = "@nekuda/webmcp-sdk";
|
|
@@ -281,6 +366,14 @@ function getOrCreateSessionId(namespace) {
|
|
|
281
366
|
return id;
|
|
282
367
|
return fallbackId(memorySession, namespace, id);
|
|
283
368
|
}
|
|
369
|
+
function resolveSessionId(options, namespace) {
|
|
370
|
+
try {
|
|
371
|
+
const supplied = options.sessionId;
|
|
372
|
+
if (typeof supplied === "string" && usableId(supplied))
|
|
373
|
+
return supplied;
|
|
374
|
+
} catch {}
|
|
375
|
+
return getOrCreateSessionId(namespace);
|
|
376
|
+
}
|
|
284
377
|
function trackingOutputs(options) {
|
|
285
378
|
try {
|
|
286
379
|
if (!options || options.disabled)
|
|
@@ -331,9 +424,17 @@ function buildEventPayload(params) {
|
|
|
331
424
|
eventName: params.eventName,
|
|
332
425
|
ts: new Date().toISOString(),
|
|
333
426
|
...pageFields(),
|
|
334
|
-
...params.data
|
|
427
|
+
...params.data,
|
|
428
|
+
...isSyntheticTester() ? { syntheticTester: true } : {}
|
|
335
429
|
};
|
|
336
430
|
}
|
|
431
|
+
function isSyntheticTester() {
|
|
432
|
+
try {
|
|
433
|
+
return /^nekuda-synthetic-tester\/[0-9]/i.test(globalThis.navigator?.userAgent ?? "");
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
337
438
|
var MAX_EVENT_BYTES = 64 * 1024;
|
|
338
439
|
var MAX_ERROR_BYTES = 16 * 1024;
|
|
339
440
|
var TRUNCATABLE = ["response", "input", "error"];
|
|
@@ -469,7 +570,7 @@ function track(options, eventName, data, sinks = defaultSinks) {
|
|
|
469
570
|
const namespace = storageNamespace(options.apiKey);
|
|
470
571
|
const event = boundEventPayload(buildEventPayload({
|
|
471
572
|
visitorId: getOrCreateVisitorId(namespace),
|
|
472
|
-
sessionId:
|
|
573
|
+
sessionId: resolveSessionId(options, namespace),
|
|
473
574
|
eventName,
|
|
474
575
|
data
|
|
475
576
|
}));
|
|
@@ -761,6 +862,7 @@ var TELEMETRY_FIELDS = {
|
|
|
761
862
|
event: true,
|
|
762
863
|
ts: true,
|
|
763
864
|
sessionId: true,
|
|
865
|
+
sampleRate: true,
|
|
764
866
|
"sdk.name": true,
|
|
765
867
|
"sdk.version": true,
|
|
766
868
|
"sdk.installMode": true,
|
|
@@ -785,6 +887,7 @@ var TELEMETRY_FIELDS = {
|
|
|
785
887
|
"config.trackingEnabled": true,
|
|
786
888
|
"config.otelEnabled": true,
|
|
787
889
|
"config.customEndpoint": true,
|
|
890
|
+
"config.builtWith": true,
|
|
788
891
|
tools: true,
|
|
789
892
|
callId: true,
|
|
790
893
|
callIndex: true,
|
|
@@ -805,6 +908,8 @@ var TELEMETRY_FIELDS = {
|
|
|
805
908
|
var TELEMETRY_TOOL_FIELDS = {
|
|
806
909
|
name: true,
|
|
807
910
|
stableKey: true,
|
|
911
|
+
inventoryToolId: true,
|
|
912
|
+
contractRevision: true,
|
|
808
913
|
version: true,
|
|
809
914
|
schemaHash: true,
|
|
810
915
|
source: true,
|
|
@@ -999,9 +1104,20 @@ function shapeMetrics(inputSchema) {
|
|
|
999
1104
|
|
|
1000
1105
|
// src/telemetry.ts
|
|
1001
1106
|
var SDK_NAME = "@nekuda/webmcp-sdk";
|
|
1002
|
-
var SDK_VERSION = "0.
|
|
1107
|
+
var SDK_VERSION = "0.6.0";
|
|
1003
1108
|
var INSTALL_MODES = ["npm", "cdn_snippet"];
|
|
1004
1109
|
var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
|
|
1110
|
+
function parseSampleRate(raw) {
|
|
1111
|
+
const rate = typeof raw === "string" ? Number(raw) : Number.NaN;
|
|
1112
|
+
return Number.isFinite(rate) && rate > 0 && rate < 1 ? rate : 1;
|
|
1113
|
+
}
|
|
1114
|
+
var SDK_TELEMETRY_SAMPLE_RATE = parseSampleRate(typeof __WEBMCP_TELEMETRY_SAMPLE_RATE__ === "string" ? __WEBMCP_TELEMETRY_SAMPLE_RATE__ : undefined);
|
|
1115
|
+
var SAMPLED_EVENTS = new Set(["sdk_init", "tool_registration"]);
|
|
1116
|
+
function pageSampled(sessionId, rate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1117
|
+
if (rate >= 1)
|
|
1118
|
+
return true;
|
|
1119
|
+
return Number.parseInt(fnv1a(`sample:${sessionId}`), 16) / 4294967296 < rate;
|
|
1120
|
+
}
|
|
1005
1121
|
function isFieldParent(value) {
|
|
1006
1122
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1007
1123
|
}
|
|
@@ -1122,12 +1238,15 @@ function buildInitEvent(params = {}) {
|
|
|
1122
1238
|
}
|
|
1123
1239
|
function configFields(tracking) {
|
|
1124
1240
|
const { toBackend, toOtel } = trackingOutputs(tracking);
|
|
1241
|
+
const builtWith = safe(() => tracking?.builtWith);
|
|
1125
1242
|
return {
|
|
1126
1243
|
trackingEnabled: toBackend,
|
|
1127
1244
|
otelEnabled: toOtel,
|
|
1128
|
-
customEndpoint: Boolean(safe(() => tracking?.endpoint))
|
|
1245
|
+
customEndpoint: Boolean(safe(() => tracking?.endpoint)),
|
|
1246
|
+
...typeof builtWith === "string" && builtWith.length > 0 && builtWith.length <= MAX_BUILT_WITH_LENGTH ? { builtWith } : {}
|
|
1129
1247
|
};
|
|
1130
1248
|
}
|
|
1249
|
+
var MAX_BUILT_WITH_LENGTH = 64;
|
|
1131
1250
|
var MAX_TOOL_FIELD_BYTES = 4 * 1024;
|
|
1132
1251
|
function toolString(value) {
|
|
1133
1252
|
return sliceToBytes(value, MAX_TOOL_FIELD_BYTES);
|
|
@@ -1135,6 +1254,9 @@ function toolString(value) {
|
|
|
1135
1254
|
function toolEnum(allowed, value) {
|
|
1136
1255
|
return allowed.find((candidate) => candidate === value);
|
|
1137
1256
|
}
|
|
1257
|
+
function toolRevision(value) {
|
|
1258
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
1259
|
+
}
|
|
1138
1260
|
var ANNOTATION_HINTS = ["readOnlyHint", "untrustedContentHint"];
|
|
1139
1261
|
function annotationHints(annotations) {
|
|
1140
1262
|
const hints = {};
|
|
@@ -1156,6 +1278,8 @@ function toolEntry(entry) {
|
|
|
1156
1278
|
return {
|
|
1157
1279
|
name: toolString(tool.name),
|
|
1158
1280
|
stableKey: toolString(tool.stableKey),
|
|
1281
|
+
...typeof tool.inventoryToolId === "string" ? { inventoryToolId: toolString(tool.inventoryToolId) } : {},
|
|
1282
|
+
...toolRevision(tool.contractRevision) !== undefined ? { contractRevision: toolRevision(tool.contractRevision) } : {},
|
|
1159
1283
|
...tool.version !== undefined ? { version: toolString(tool.version) } : {},
|
|
1160
1284
|
...hash !== undefined ? { schemaHash: hash } : {},
|
|
1161
1285
|
...source !== undefined ? { source } : {},
|
|
@@ -1253,6 +1377,8 @@ function buildToolCallEvent(params) {
|
|
|
1253
1377
|
...sinceInit !== undefined ? { timeSinceInitMs: sinceInit } : {},
|
|
1254
1378
|
tool: {
|
|
1255
1379
|
stableKey: toolString(params.tool.stableKey),
|
|
1380
|
+
...typeof params.tool.inventoryToolId === "string" ? { inventoryToolId: toolString(params.tool.inventoryToolId) } : {},
|
|
1381
|
+
...toolRevision(params.tool.contractRevision) !== undefined ? { contractRevision: toolRevision(params.tool.contractRevision) } : {},
|
|
1256
1382
|
...hash !== undefined ? { schemaHash: hash } : {},
|
|
1257
1383
|
...intent !== undefined ? { intent } : {}
|
|
1258
1384
|
},
|
|
@@ -1334,12 +1460,13 @@ function pruneByAllowlist(event, fields = TELEMETRY_FIELDS, toolFields = TELEMET
|
|
|
1334
1460
|
return pruned;
|
|
1335
1461
|
}
|
|
1336
1462
|
var defaultSinks2 = {
|
|
1337
|
-
sendTelemetry: (event) => sendTelemetry(event, globalThis,
|
|
1463
|
+
sendTelemetry: (event) => sendTelemetry(event, globalThis, telemetryEndpoint(), telemetryApiKey())
|
|
1338
1464
|
};
|
|
1339
|
-
function batchTelemetrySinks(apiKey) {
|
|
1465
|
+
function batchTelemetrySinks(apiKey, endpoint) {
|
|
1340
1466
|
const own = batchApiKey(apiKey);
|
|
1467
|
+
const ownEndpoint = telemetryEndpointFrom(endpoint);
|
|
1341
1468
|
return {
|
|
1342
|
-
sendTelemetry: (event) => sendTelemetry(event, globalThis,
|
|
1469
|
+
sendTelemetry: (event) => sendTelemetry(event, globalThis, ownEndpoint ?? telemetryEndpoint(), own ?? telemetryApiKey())
|
|
1343
1470
|
};
|
|
1344
1471
|
}
|
|
1345
1472
|
function batchApiKey(apiKey) {
|
|
@@ -1352,16 +1479,24 @@ function telemetryTenantScope(apiKey) {
|
|
|
1352
1479
|
const resolved = resolveTelemetryKey(apiKey);
|
|
1353
1480
|
return resolved === undefined ? "" : fnv1a(resolved);
|
|
1354
1481
|
}
|
|
1355
|
-
function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS, toolFields = TELEMETRY_TOOL_FIELDS) {
|
|
1482
|
+
function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS, toolFields = TELEMETRY_TOOL_FIELDS, sampleRate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1356
1483
|
try {
|
|
1357
1484
|
if (!telemetryEnabled())
|
|
1358
1485
|
return;
|
|
1359
|
-
|
|
1486
|
+
const event = { ...build() };
|
|
1487
|
+
if (SAMPLED_EVENTS.has(String(event.event)) && sampleRate < 1) {
|
|
1488
|
+
if (!pageSampled(String(event.sessionId), sampleRate))
|
|
1489
|
+
return;
|
|
1490
|
+
event.sampleRate = sampleRate;
|
|
1491
|
+
}
|
|
1492
|
+
sinks.sendTelemetry(boundEventPayload(pruneByAllowlist(event, fields, toolFields)));
|
|
1360
1493
|
} catch {}
|
|
1361
1494
|
}
|
|
1362
1495
|
var initCancelled = false;
|
|
1363
1496
|
var initFlushed = false;
|
|
1497
|
+
var cancelInitFallback = () => {};
|
|
1364
1498
|
var capturedApiKey;
|
|
1499
|
+
var capturedEndpoint;
|
|
1365
1500
|
function captureTelemetryApiKey(apiKey) {
|
|
1366
1501
|
if (typeof apiKey === "string" && apiKey.trim().length > 0)
|
|
1367
1502
|
capturedApiKey = apiKey;
|
|
@@ -1369,17 +1504,44 @@ function captureTelemetryApiKey(apiKey) {
|
|
|
1369
1504
|
function telemetryApiKey() {
|
|
1370
1505
|
return capturedApiKey;
|
|
1371
1506
|
}
|
|
1507
|
+
function telemetryEndpointFrom(endpoint) {
|
|
1508
|
+
if (typeof endpoint !== "string")
|
|
1509
|
+
return;
|
|
1510
|
+
return safe(() => {
|
|
1511
|
+
const url = new URL(endpoint);
|
|
1512
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.pathname.endsWith("/v1/collect") || url.search !== "" || url.hash !== "") {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
url.pathname = `${url.pathname.slice(0, -"/v1/collect".length)}/v1/telemetry`;
|
|
1516
|
+
return url.toString();
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
function captureTelemetryEndpoint(endpoint) {
|
|
1520
|
+
const derived = telemetryEndpointFrom(endpoint);
|
|
1521
|
+
if (derived !== undefined)
|
|
1522
|
+
capturedEndpoint = derived;
|
|
1523
|
+
}
|
|
1524
|
+
function telemetryEndpoint() {
|
|
1525
|
+
return capturedEndpoint;
|
|
1526
|
+
}
|
|
1372
1527
|
function cancelInitEvent() {
|
|
1373
1528
|
initCancelled = true;
|
|
1529
|
+
cancelInitFallback();
|
|
1374
1530
|
}
|
|
1375
1531
|
function flushInitEvent(sinks) {
|
|
1376
1532
|
if (initFlushed)
|
|
1377
1533
|
return;
|
|
1378
1534
|
initFlushed = true;
|
|
1535
|
+
cancelInitFallback();
|
|
1379
1536
|
if (initCancelled)
|
|
1380
1537
|
return;
|
|
1381
1538
|
emitTelemetry(() => buildInitEvent(), sinks);
|
|
1382
1539
|
}
|
|
1540
|
+
function deferInitEventForBatch(sinks) {
|
|
1541
|
+
if (initFlushed || initCancelled)
|
|
1542
|
+
return;
|
|
1543
|
+
afterDelay(() => flushInitEvent(sinks), 0);
|
|
1544
|
+
}
|
|
1383
1545
|
function afterDelay(run, ms) {
|
|
1384
1546
|
const schedule = safe(() => globalThis.setTimeout);
|
|
1385
1547
|
if (typeof schedule !== "function")
|
|
@@ -1392,7 +1554,8 @@ function afterDelay(run, ms) {
|
|
|
1392
1554
|
safe(() => clear.call(globalThis, handle));
|
|
1393
1555
|
};
|
|
1394
1556
|
}
|
|
1395
|
-
|
|
1557
|
+
var INIT_FALLBACK_MS = 1000;
|
|
1558
|
+
cancelInitFallback = afterDelay(() => flushInitEvent(), INIT_FALLBACK_MS);
|
|
1396
1559
|
|
|
1397
1560
|
// src/register.ts
|
|
1398
1561
|
function isContentResult(value) {
|
|
@@ -1414,15 +1577,18 @@ function clock() {
|
|
|
1414
1577
|
const perf = safe(() => globalThis.performance);
|
|
1415
1578
|
const now = safe(() => perf?.now);
|
|
1416
1579
|
if (typeof now === "function") {
|
|
1417
|
-
const
|
|
1418
|
-
const
|
|
1419
|
-
if (
|
|
1420
|
-
return () => elapsedMs(
|
|
1580
|
+
const read = () => safe(() => now.call(perf));
|
|
1581
|
+
const start = read();
|
|
1582
|
+
if (start !== undefined)
|
|
1583
|
+
return () => elapsedMs(read(), start);
|
|
1421
1584
|
}
|
|
1422
1585
|
const read = () => safe(() => Date.now());
|
|
1423
1586
|
const start = read();
|
|
1424
1587
|
return () => elapsedMs(read(), start);
|
|
1425
1588
|
}
|
|
1589
|
+
function createCallTracker(tool, tracking) {
|
|
1590
|
+
return trackerFor({ ...tool, stableKey: tool.name }, tracking);
|
|
1591
|
+
}
|
|
1426
1592
|
function trackerFor(tool, tracking) {
|
|
1427
1593
|
if (!tracking)
|
|
1428
1594
|
return;
|
|
@@ -1437,9 +1603,24 @@ function trackerFor(tool, tracking) {
|
|
|
1437
1603
|
};
|
|
1438
1604
|
return (eventName, data) => track(tracking, eventName, { ...shared, ...data });
|
|
1439
1605
|
}
|
|
1606
|
+
var TRACKED_TOOL_FLAG = Symbol.for("webmcp.sdk.tracked");
|
|
1607
|
+
function markTracked(spec, tracking) {
|
|
1608
|
+
const { toBackend } = trackingOutputs(tracking);
|
|
1609
|
+
if (!toBackend)
|
|
1610
|
+
return spec;
|
|
1611
|
+
try {
|
|
1612
|
+
Object.defineProperty(spec, TRACKED_TOOL_FLAG, {
|
|
1613
|
+
value: true,
|
|
1614
|
+
writable: false,
|
|
1615
|
+
configurable: true,
|
|
1616
|
+
enumerable: false
|
|
1617
|
+
});
|
|
1618
|
+
} catch {}
|
|
1619
|
+
return spec;
|
|
1620
|
+
}
|
|
1440
1621
|
function toSpecTool(tool, channels) {
|
|
1441
|
-
const { tracking, telemetry, telemetrySinks, telemetryKey } = channels;
|
|
1442
|
-
|
|
1622
|
+
const { tracking, telemetry, telemetrySinks, telemetryKey, telemetryEndpoint } = channels;
|
|
1623
|
+
const spec = {
|
|
1443
1624
|
name: tool.name,
|
|
1444
1625
|
...tool.title !== undefined ? { title: tool.title } : {},
|
|
1445
1626
|
description: tool.description,
|
|
@@ -1451,7 +1632,7 @@ function toSpecTool(tool, channels) {
|
|
|
1451
1632
|
return normalizeResult(await tool.execute(input));
|
|
1452
1633
|
const callKey = telemetry ? resolveTelemetryKey(telemetryKey) : undefined;
|
|
1453
1634
|
const sequence = telemetry ? nextCall(tool.stableKey, telemetryTenantScope(callKey)) : undefined;
|
|
1454
|
-
const callSinks = sequence ? batchTelemetrySinks(callKey) : telemetrySinks;
|
|
1635
|
+
const callSinks = sequence ? batchTelemetrySinks(callKey, telemetryEndpoint) : telemetrySinks;
|
|
1455
1636
|
const elapsed = clock();
|
|
1456
1637
|
trackCall?.("tool_call_request", { input });
|
|
1457
1638
|
try {
|
|
@@ -1475,6 +1656,7 @@ function toSpecTool(tool, channels) {
|
|
|
1475
1656
|
}
|
|
1476
1657
|
}
|
|
1477
1658
|
};
|
|
1659
|
+
return markTracked(spec, tracking);
|
|
1478
1660
|
}
|
|
1479
1661
|
var REGISTRATION_TIMEOUT_MS = 2000;
|
|
1480
1662
|
var PAGEHIDE = "pagehide";
|
|
@@ -1490,7 +1672,7 @@ function onPagehide(run) {
|
|
|
1490
1672
|
safe(() => remove.call(g, PAGEHIDE, listener));
|
|
1491
1673
|
};
|
|
1492
1674
|
}
|
|
1493
|
-
function watchRegistration(tools, tracking, sinks) {
|
|
1675
|
+
function watchRegistration(tools, tracking, sinks, navigation = false) {
|
|
1494
1676
|
const entries = tools.map((tool) => ({ tool, outcome: "pending" }));
|
|
1495
1677
|
const elapsed = clock();
|
|
1496
1678
|
const { registrationIndex, trigger } = nextRegistration();
|
|
@@ -1505,7 +1687,7 @@ function watchRegistration(tools, tracking, sinks) {
|
|
|
1505
1687
|
const settleMs = elapsed();
|
|
1506
1688
|
emitTelemetry(() => buildToolRegistrationEvent({
|
|
1507
1689
|
registrationIndex,
|
|
1508
|
-
trigger,
|
|
1690
|
+
trigger: navigation ? "spa_navigation" : trigger,
|
|
1509
1691
|
settleMs,
|
|
1510
1692
|
tools: entries,
|
|
1511
1693
|
tracking
|
|
@@ -1543,15 +1725,24 @@ function registerTools(tools, options = {}) {
|
|
|
1543
1725
|
const tracking = safe(() => options.tracking);
|
|
1544
1726
|
const telemetryLive = telemetryOption !== undefined && telemetryEnabled(telemetryOption.value);
|
|
1545
1727
|
const apiKey = telemetryLive ? safe(() => tracking?.apiKey) : undefined;
|
|
1728
|
+
const endpoint = telemetryLive ? safe(() => tracking?.endpoint) : undefined;
|
|
1729
|
+
const telemetrySinks = telemetryLive ? batchTelemetrySinks(apiKey, endpoint) : undefined;
|
|
1546
1730
|
const channels = {
|
|
1547
1731
|
tracking,
|
|
1548
1732
|
telemetry: telemetryLive,
|
|
1549
|
-
...telemetryLive ? {
|
|
1733
|
+
...telemetryLive ? {
|
|
1734
|
+
telemetrySinks,
|
|
1735
|
+
telemetryKey: apiKey,
|
|
1736
|
+
telemetryEndpoint: endpoint
|
|
1737
|
+
} : {}
|
|
1550
1738
|
};
|
|
1551
1739
|
if (telemetryOption?.value === false)
|
|
1552
1740
|
cancelInitEvent();
|
|
1553
|
-
if (
|
|
1741
|
+
if (telemetrySinks) {
|
|
1554
1742
|
captureTelemetryApiKey(apiKey);
|
|
1743
|
+
captureTelemetryEndpoint(endpoint);
|
|
1744
|
+
deferInitEventForBatch(telemetrySinks);
|
|
1745
|
+
}
|
|
1555
1746
|
assertUniqueIdentities(tools);
|
|
1556
1747
|
const controller = new AbortController;
|
|
1557
1748
|
const external = options.signal;
|
|
@@ -1568,32 +1759,89 @@ function registerTools(tools, options = {}) {
|
|
|
1568
1759
|
state,
|
|
1569
1760
|
...error !== undefined ? { error } : {}
|
|
1570
1761
|
});
|
|
1571
|
-
const
|
|
1762
|
+
const active = new Map;
|
|
1763
|
+
const current = () => tools.filter((tool) => active.get(tool)?.registered).map((tool) => tool.name);
|
|
1764
|
+
const changed = () => {
|
|
1765
|
+
safe(() => options.onChange?.(current()));
|
|
1766
|
+
};
|
|
1767
|
+
const eligible = (tool) => {
|
|
1768
|
+
const page = currentPageKey();
|
|
1769
|
+
return page === undefined || matchPage(tool.pages, page);
|
|
1770
|
+
};
|
|
1771
|
+
const remove = (tool) => {
|
|
1772
|
+
const entry = active.get(tool);
|
|
1773
|
+
if (!entry?.removable)
|
|
1774
|
+
return;
|
|
1775
|
+
entry.lifetime.abort();
|
|
1776
|
+
safe(() => modelContext?.unregisterTool?.(tool.name));
|
|
1777
|
+
active.delete(tool);
|
|
1778
|
+
};
|
|
1572
1779
|
const settle = async (tool) => {
|
|
1573
1780
|
if (!modelContext)
|
|
1574
1781
|
return result(tool, "unsupported");
|
|
1575
1782
|
if (controller.signal.aborted)
|
|
1576
1783
|
return result(tool, "aborted");
|
|
1784
|
+
const entry = {
|
|
1785
|
+
lifetime: tool.pages?.length ? new AbortController : controller,
|
|
1786
|
+
removable: typeof modelContext.unregisterTool === "function",
|
|
1787
|
+
registered: false
|
|
1788
|
+
};
|
|
1789
|
+
active.set(tool, entry);
|
|
1577
1790
|
try {
|
|
1578
1791
|
await Promise.resolve(modelContext.registerTool(toSpecTool(tool, channels), {
|
|
1579
|
-
signal
|
|
1792
|
+
get signal() {
|
|
1793
|
+
entry.removable = true;
|
|
1794
|
+
return entry.lifetime.signal;
|
|
1795
|
+
}
|
|
1580
1796
|
}));
|
|
1581
|
-
|
|
1797
|
+
if (entry.lifetime.signal.aborted)
|
|
1798
|
+
return result(tool, "aborted");
|
|
1799
|
+
entry.registered = true;
|
|
1800
|
+
if (!eligible(tool))
|
|
1801
|
+
remove(tool);
|
|
1802
|
+
changed();
|
|
1803
|
+
return result(tool, entry.lifetime.signal.aborted ? "aborted" : "registered");
|
|
1582
1804
|
} catch (error) {
|
|
1583
|
-
if (
|
|
1805
|
+
if (active.get(tool) === entry)
|
|
1806
|
+
active.delete(tool);
|
|
1807
|
+
if (entry.lifetime.signal.aborted)
|
|
1584
1808
|
return result(tool, "aborted");
|
|
1585
1809
|
return result(tool, "failed", error);
|
|
1586
1810
|
}
|
|
1587
1811
|
};
|
|
1588
|
-
const
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1812
|
+
const register = (batch, navigation = false) => {
|
|
1813
|
+
const watch = channels.telemetry ? watchRegistration(batch, channels.tracking, channels.telemetrySinks, navigation) : undefined;
|
|
1814
|
+
const ready = Promise.all(batch.map(async (tool, index) => {
|
|
1815
|
+
const settled = await settle(tool);
|
|
1816
|
+
watch?.record(index, settled);
|
|
1817
|
+
return settled;
|
|
1818
|
+
}));
|
|
1819
|
+
if (watch)
|
|
1820
|
+
ready.then(() => watch.emit());
|
|
1821
|
+
return ready;
|
|
1822
|
+
};
|
|
1823
|
+
const reconcile = () => {
|
|
1824
|
+
if (controller.signal.aborted)
|
|
1825
|
+
return;
|
|
1826
|
+
for (const tool of active.keys())
|
|
1827
|
+
if (!eligible(tool))
|
|
1828
|
+
remove(tool);
|
|
1829
|
+
changed();
|
|
1830
|
+
const added = tools.filter((tool) => eligible(tool) && !active.has(tool));
|
|
1831
|
+
if (added.length)
|
|
1832
|
+
register(added, true);
|
|
1833
|
+
};
|
|
1834
|
+
const stop = !controller.signal.aborted && tools.some((tool) => tool.pages?.length) ? onPageChange(reconcile) : () => {};
|
|
1835
|
+
controller.signal.addEventListener("abort", () => {
|
|
1836
|
+
stop();
|
|
1837
|
+
for (const tool of active.keys())
|
|
1838
|
+
remove(tool);
|
|
1839
|
+
changed();
|
|
1840
|
+
}, { once: true });
|
|
1841
|
+
const ready = register(tools.filter(eligible));
|
|
1595
1842
|
return {
|
|
1596
1843
|
ready,
|
|
1844
|
+
current,
|
|
1597
1845
|
signal: controller.signal,
|
|
1598
1846
|
unregister() {
|
|
1599
1847
|
controller.abort();
|
|
@@ -1601,7 +1849,10 @@ function registerTools(tools, options = {}) {
|
|
|
1601
1849
|
};
|
|
1602
1850
|
}
|
|
1603
1851
|
export {
|
|
1604
|
-
|
|
1852
|
+
createCallTracker,
|
|
1853
|
+
currentPageKey,
|
|
1854
|
+
defineTool,
|
|
1855
|
+
matchPage,
|
|
1605
1856
|
registerTools,
|
|
1606
|
-
|
|
1857
|
+
resolveModelContext
|
|
1607
1858
|
};
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Match any page pattern; undefined/empty lists (and empty patterns) mean everywhere. */
|
|
2
|
+
export declare function matchPage(patterns: readonly string[] | undefined, location: string): boolean;
|
|
3
|
+
/** No readable location (SSR) means cannot evaluate: register all tools, not a root-page match. */
|
|
4
|
+
export declare function currentPageKey(): string | undefined;
|
|
5
|
+
/** One shared browser listener/History patch, including across duplicated SDK bundles. */
|
|
6
|
+
export declare function onPageChange(listener: () => void): () => void;
|
package/dist/register.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface ToolRegistrationResult {
|
|
|
16
16
|
error?: unknown;
|
|
17
17
|
}
|
|
18
18
|
export interface RegisterToolsOptions {
|
|
19
|
+
/** Called after the live name set changes, including initial registration and navigation. */
|
|
20
|
+
onChange?: (names: string[]) => void;
|
|
19
21
|
/**
|
|
20
22
|
* External lifetime for the registration (e.g. a component's unmount signal).
|
|
21
23
|
* Aborting it unregisters every tool in this batch, same as `unregister()`.
|
|
@@ -44,10 +46,11 @@ export interface RegisterToolsOptions {
|
|
|
44
46
|
*
|
|
45
47
|
* The other two events are per-batch, so the option is read per call: a *later*
|
|
46
48
|
* `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
|
-
*
|
|
49
|
+
* `sdk_init` also requires winning the race with its deferred flush. A batch
|
|
50
|
+
* registered at module scope or from an ordinary mount effect schedules the keyed
|
|
51
|
+
* batch flush; a later consent decision can still arrive after the bounded
|
|
52
|
+
* no-registration fallback. A site that wants the whole page silent regardless of
|
|
53
|
+
* when its `registerTools` calls run (generated code, a CDN snippet) should use a
|
|
51
54
|
* page-level lever instead: `globalThis.__WEBMCP_TELEMETRY__ = false` — strictly
|
|
52
55
|
* `false`, so a truthy `"0"` does not opt out — or Global Privacy Control
|
|
53
56
|
* (`navigator.globalPrivacyControl === true`), both of which are read at emit time
|
|
@@ -63,18 +66,55 @@ export interface ToolRegistration {
|
|
|
63
66
|
* cannot mask the rest.
|
|
64
67
|
*/
|
|
65
68
|
ready: Promise<ToolRegistrationResult[]>;
|
|
69
|
+
/** Currently registered names, in declaration order; `ready` covers only the initial page. */
|
|
70
|
+
current(): string[];
|
|
66
71
|
/** Unregister every tool in this batch. Idempotent. */
|
|
67
72
|
unregister(): void;
|
|
68
73
|
/** The signal carrying this registration's lifetime (aborted once unregistered). */
|
|
69
74
|
signal: AbortSignal;
|
|
70
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Emits one invocation's authenticated-channel events. The `callId` correlating a
|
|
78
|
+
* `tool_call_request` with its `tool_call_response` is closed over, so one tracker
|
|
79
|
+
* is one call — never one tool, never one page.
|
|
80
|
+
*/
|
|
81
|
+
export type CallTracker = (eventName: string, data: Record<string, unknown>) => void;
|
|
82
|
+
/**
|
|
83
|
+
* A tool a host observes but did not register through {@link registerTools} — the
|
|
84
|
+
* only identity it can offer is the wire `name` the agent surface already knows.
|
|
85
|
+
*/
|
|
86
|
+
export interface TrackedCall {
|
|
87
|
+
name: string;
|
|
88
|
+
/** Optional per-tool version, surfaced on the events as `toolVersion`. */
|
|
89
|
+
version?: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Emitter for a call on a tool this SDK did not register, or `undefined` when the
|
|
93
|
+
* authenticated channel has no live output — the same `trackingOutputs` answer
|
|
94
|
+
* {@link registerTools} gates its own per-call emitter on, so the two cannot
|
|
95
|
+
* disagree about whether the channel is live.
|
|
96
|
+
*
|
|
97
|
+
* This exists for one caller shape: a host that owns the page's WebMCP surface and
|
|
98
|
+
* wraps tools registered on it directly (the CDN snippet's coexistence path). Those
|
|
99
|
+
* calls must land on `/v1/collect` as the same bytes an SDK-registered tool's do —
|
|
100
|
+
* same event names, same correlated pair, same anonymous identity — so the
|
|
101
|
+
* projection reads one shape rather than two. Everything else the host must do
|
|
102
|
+
* itself: call it once per invocation (a reused tracker collapses two calls into
|
|
103
|
+
* one `callId`), emit `tool_call_request` before the handler and
|
|
104
|
+
* `tool_call_response` after, and measure the `duration_ms` it reports.
|
|
105
|
+
*
|
|
106
|
+
* `stableKey` is the tool's `name`: a host-wrapped tool has no developer-authored
|
|
107
|
+
* durable identity to carry, and inventing one would key the same tool differently
|
|
108
|
+
* from every other reader of the same page.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createCallTracker(tool: TrackedCall, tracking: TrackingOptions): CallTracker | undefined;
|
|
71
111
|
/**
|
|
72
112
|
* Register a batch of `defineTool` tools on the page's WebMCP surface.
|
|
73
113
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
114
|
+
* Registration starts immediately, filtered by `pages`; `ready` covers that initial set.
|
|
115
|
+
* Route changes reconcile scoped tools using per-tool signals (or legacy unregisterTool).
|
|
116
|
+
* A legacy surface that never reads the signal and has no unregister method retains a
|
|
117
|
+
* tool once registered. `unregister()`/external abort also removes the route subscription.
|
|
118
|
+
* Without a readable location (SSR), all tools are eligible; without a surface, no-op.
|
|
79
119
|
*/
|
|
80
120
|
export declare function registerTools(tools: readonly AnyWebMCPTool[], options?: RegisterToolsOptions): ToolRegistration;
|
package/dist/spec.d.ts
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* - `registerTool(tool, { signal })` returns a promise that settles when the
|
|
12
12
|
* registration completes; it rejects on a duplicate name, an invalid tool, an
|
|
13
13
|
* inactive document, or an abort.
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* - Current unregistration is the `AbortSignal` passed at registration (also
|
|
15
|
+
* verified against the 2026-09-10 draft). Legacy surfaces may instead expose
|
|
16
|
+
* `unregisterTool(name)`; a surface supporting neither retains registered tools.
|
|
17
17
|
* - Tool names: 1–128 chars of [A-Za-z0-9_\-.].
|
|
18
18
|
* - `annotations.readOnlyHint` / `annotations.untrustedContentHint`.
|
|
19
19
|
*
|
|
@@ -48,9 +48,20 @@ export interface RegisterToolOptions {
|
|
|
48
48
|
*/
|
|
49
49
|
export interface ModelContextLike {
|
|
50
50
|
registerTool(tool: SpecTool, options?: RegisterToolOptions): Promise<void>;
|
|
51
|
+
/** Pre-draft compatibility only; current native surfaces use the registration signal. */
|
|
52
|
+
unregisterTool?(name: string): void;
|
|
53
|
+
}
|
|
54
|
+
interface GlobalWithModelContext {
|
|
55
|
+
document?: {
|
|
56
|
+
modelContext?: ModelContextLike;
|
|
57
|
+
};
|
|
58
|
+
navigator?: {
|
|
59
|
+
modelContext?: ModelContextLike;
|
|
60
|
+
};
|
|
51
61
|
}
|
|
52
62
|
/**
|
|
53
63
|
* Resolve the page's WebMCP surface; `undefined` on non-supporting browsers.
|
|
54
64
|
* Injectable global scope for tests.
|
|
55
65
|
*/
|
|
56
|
-
export declare function resolveModelContext(
|
|
66
|
+
export declare function resolveModelContext(scope?: GlobalWithModelContext): ModelContextLike | undefined;
|
|
67
|
+
export {};
|
|
@@ -81,7 +81,17 @@ export interface ClientContext {
|
|
|
81
81
|
* even when nobody ever calls `registerTools`: a site that loads the SDK and never
|
|
82
82
|
* registers is a broken integration, and this event is the only way to see it.
|
|
83
83
|
*/
|
|
84
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Present only when a build sampled the page-level events: the rate this page was kept
|
|
86
|
+
* at, strictly between 0 and 1. Absent means every such event on the page was sent. A
|
|
87
|
+
* consumer counting page loads weights each sampled event by `1 / sampleRate`. Not on
|
|
88
|
+
* the envelope, because `tool_call` never carries it — sampling is per page, and a
|
|
89
|
+
* tool invocation is not a page-level fact.
|
|
90
|
+
*/
|
|
91
|
+
export interface SampledEvent {
|
|
92
|
+
sampleRate?: number;
|
|
93
|
+
}
|
|
94
|
+
export interface SdkInitEvent extends TelemetryEnvelope, SampledEvent {
|
|
85
95
|
event: "sdk_init";
|
|
86
96
|
sdk: SdkInfo;
|
|
87
97
|
surface: SurfaceInfo;
|
|
@@ -108,6 +118,8 @@ export interface TrackingConfigInfo {
|
|
|
108
118
|
trackingEnabled: boolean;
|
|
109
119
|
otelEnabled: boolean;
|
|
110
120
|
customEndpoint: boolean;
|
|
121
|
+
/** `TrackingOptions.builtWith`, verbatim, when set and within its length cap. */
|
|
122
|
+
builtWith?: string;
|
|
111
123
|
}
|
|
112
124
|
/**
|
|
113
125
|
* One tool in a `tool_registration` batch. The shape metrics are inherited as
|
|
@@ -117,6 +129,14 @@ export interface TrackingConfigInfo {
|
|
|
117
129
|
export interface RegisteredToolEntry extends Partial<ToolShapeMetrics> {
|
|
118
130
|
name: string;
|
|
119
131
|
stableKey: string;
|
|
132
|
+
/**
|
|
133
|
+
* The connected platform's own id for this tool, when the host supplied one. Opaque
|
|
134
|
+
* and optional: a tool declared in a codebase that has never been connected has none,
|
|
135
|
+
* and its absence is the honest reading rather than a degraded one.
|
|
136
|
+
*/
|
|
137
|
+
inventoryToolId?: string;
|
|
138
|
+
/** The contract revision the host believed this tool matched, when it knew one. */
|
|
139
|
+
contractRevision?: number;
|
|
120
140
|
version?: string;
|
|
121
141
|
schemaHash?: string;
|
|
122
142
|
source?: ToolSource;
|
|
@@ -154,7 +174,7 @@ export interface TruncatedTools {
|
|
|
154
174
|
* outcomes v1 computed and threw away. A site where 3 of 8 tools fail on a
|
|
155
175
|
* duplicate name is the failure mode this event exists to make visible.
|
|
156
176
|
*/
|
|
157
|
-
export interface ToolRegistrationEvent extends TelemetryEnvelope {
|
|
177
|
+
export interface ToolRegistrationEvent extends TelemetryEnvelope, SampledEvent {
|
|
158
178
|
event: "tool_registration";
|
|
159
179
|
/** 1-based position of this batch within the page load. */
|
|
160
180
|
registrationIndex: number;
|
|
@@ -176,6 +196,10 @@ export type ToolCallOutcome = "success" | "error";
|
|
|
176
196
|
/** `tool.*` on `tool_call` — identity and the two fields worth joining calls on. */
|
|
177
197
|
export interface ToolCallToolInfo {
|
|
178
198
|
stableKey: string;
|
|
199
|
+
/** The connected platform's own id, when the host supplied one. See the registration
|
|
200
|
+
* entry's field of the same name — same value, same optionality, same opacity. */
|
|
201
|
+
inventoryToolId?: string;
|
|
202
|
+
contractRevision?: number;
|
|
179
203
|
schemaHash?: string;
|
|
180
204
|
intent?: ToolIntent;
|
|
181
205
|
}
|
|
@@ -40,6 +40,7 @@ export declare const TELEMETRY_FIELDS: {
|
|
|
40
40
|
readonly event: true;
|
|
41
41
|
readonly ts: true;
|
|
42
42
|
readonly sessionId: true;
|
|
43
|
+
readonly sampleRate: true;
|
|
43
44
|
readonly "sdk.name": true;
|
|
44
45
|
readonly "sdk.version": true;
|
|
45
46
|
readonly "sdk.installMode": true;
|
|
@@ -64,6 +65,7 @@ export declare const TELEMETRY_FIELDS: {
|
|
|
64
65
|
readonly "config.trackingEnabled": true;
|
|
65
66
|
readonly "config.otelEnabled": true;
|
|
66
67
|
readonly "config.customEndpoint": true;
|
|
68
|
+
readonly "config.builtWith": true;
|
|
67
69
|
readonly tools: true;
|
|
68
70
|
readonly callId: true;
|
|
69
71
|
readonly callIndex: true;
|
|
@@ -90,6 +92,8 @@ export declare const TELEMETRY_FIELDS: {
|
|
|
90
92
|
export declare const TELEMETRY_TOOL_FIELDS: {
|
|
91
93
|
readonly name: true;
|
|
92
94
|
readonly stableKey: true;
|
|
95
|
+
readonly inventoryToolId: true;
|
|
96
|
+
readonly contractRevision: true;
|
|
93
97
|
readonly version: true;
|
|
94
98
|
readonly schemaHash: true;
|
|
95
99
|
readonly source: true;
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -43,6 +43,22 @@ export declare const SDK_VERSION: string;
|
|
|
43
43
|
* the overwhelming majority rather than emit a value no consumer can read.
|
|
44
44
|
*/
|
|
45
45
|
export declare const SDK_INSTALL_MODE: InstallMode;
|
|
46
|
+
/**
|
|
47
|
+
* Parse a sampling rate. Anything that is not a number strictly between 0 and 1 means
|
|
48
|
+
* "send everything": an unset define is how every npm build reaches a page, and a
|
|
49
|
+
* mistyped one must degrade to complete data rather than to silence.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseSampleRate(raw: unknown): number;
|
|
52
|
+
/** The rate this build samples page-level events at; `1` sends everything. */
|
|
53
|
+
export declare const SDK_TELEMETRY_SAMPLE_RATE: number;
|
|
54
|
+
/**
|
|
55
|
+
* Whether this page load is in the sample. Decided from the session id, which every
|
|
56
|
+
* event on the page shares, so `sdk_init` and the `tool_registration`s beside it are
|
|
57
|
+
* kept or dropped together — a sampled page is a complete page, never half of one. The
|
|
58
|
+
* hash is the same FNV-1a the channel already uses for `schemaHash`; its top 32 bits
|
|
59
|
+
* over 2^32 is a uniform enough coin for a rate that only has to hold on average.
|
|
60
|
+
*/
|
|
61
|
+
export declare function pageSampled(sessionId: string, rate?: number): boolean;
|
|
46
62
|
/**
|
|
47
63
|
* Read a value that may not exist, collapsing both an absent property and a
|
|
48
64
|
* throwing getter to `undefined`. Every browser global this module touches is
|
|
@@ -173,6 +189,9 @@ export declare function buildInitEvent(params?: InitEventParams): SdkInitEvent;
|
|
|
173
189
|
export interface RegisteredTelemetryTool {
|
|
174
190
|
name: string;
|
|
175
191
|
stableKey: string;
|
|
192
|
+
/** The connected platform's own id for this tool; opaque, copied, never interpreted. */
|
|
193
|
+
inventoryToolId?: string;
|
|
194
|
+
contractRevision?: number;
|
|
176
195
|
version?: string;
|
|
177
196
|
description?: string;
|
|
178
197
|
inputSchema?: unknown;
|
|
@@ -187,6 +206,12 @@ export interface RegisteredToolParams {
|
|
|
187
206
|
/** What the surface rejected with; templated into `failureSignature` when `failed`. */
|
|
188
207
|
error?: unknown;
|
|
189
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Cap on `config.builtWith`, in characters. Sized for `<tool>@<semver-with-prerelease>`
|
|
211
|
+
* with room to spare; it is the one caller-supplied string on `tool_registration`
|
|
212
|
+
* that is not copied off a tool, so the per-entry byte cap below does not cover it.
|
|
213
|
+
*/
|
|
214
|
+
export declare const MAX_BUILT_WITH_LENGTH = 64;
|
|
190
215
|
/** Where a batch sits in the page load, and what caused it. */
|
|
191
216
|
export interface RegistrationSequence {
|
|
192
217
|
/** 1-based position among the batches this page load reports. */
|
|
@@ -278,6 +303,9 @@ export declare function nextCall(stableKey: string, tenantScope?: string): ToolC
|
|
|
278
303
|
/** What `tool_call` reads off a tool — the two fields worth joining calls on. */
|
|
279
304
|
export interface CalledTelemetryTool {
|
|
280
305
|
stableKey: string;
|
|
306
|
+
/** The connected platform's own id for this tool; opaque, copied, never interpreted. */
|
|
307
|
+
inventoryToolId?: string;
|
|
308
|
+
contractRevision?: number;
|
|
281
309
|
/** Reported as a hash only; the schema itself never leaves the page. */
|
|
282
310
|
inputSchema?: unknown;
|
|
283
311
|
intent?: ToolIntent;
|
|
@@ -360,7 +388,7 @@ export interface TelemetrySinks {
|
|
|
360
388
|
* the raw batch option: a key resolved to a value pins it, closing the window in which
|
|
361
389
|
* the module key changes between the claim and the emit.
|
|
362
390
|
*/
|
|
363
|
-
export declare function batchTelemetrySinks(apiKey: unknown): TelemetrySinks;
|
|
391
|
+
export declare function batchTelemetrySinks(apiKey: unknown, endpoint?: unknown): TelemetrySinks;
|
|
364
392
|
/**
|
|
365
393
|
* The key a batch's beacons authenticate with, resolved now instead of at emit time:
|
|
366
394
|
* its own when it supplied one, otherwise the page's.
|
|
@@ -421,7 +449,7 @@ export declare function telemetryTenantScope(apiKey: unknown): string;
|
|
|
421
449
|
* global the builders read is guarded individually too; this is the backstop that
|
|
422
450
|
* keeps the guarantee true of assembly as a whole, not of each read in turn.
|
|
423
451
|
*/
|
|
424
|
-
export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: TelemetrySinks, fields?: TelemetryFieldMap, toolFields?: TelemetryToolFieldMap): void;
|
|
452
|
+
export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: TelemetrySinks, fields?: TelemetryFieldMap, toolFields?: TelemetryToolFieldMap, sampleRate?: number): void;
|
|
425
453
|
/**
|
|
426
454
|
* Record the `apiKey` of a `registerTools` call so the deferred `sdk_init` flush —
|
|
427
455
|
* which runs with no batch in hand, and may run before any batch exists — can
|
|
@@ -438,6 +466,15 @@ export declare function emitTelemetry(build: () => TelemetryEvent, sinks?: Telem
|
|
|
438
466
|
export declare function captureTelemetryApiKey(apiKey: unknown): void;
|
|
439
467
|
/** The key {@link captureTelemetryApiKey} last recorded, if any. */
|
|
440
468
|
export declare function telemetryApiKey(): string | undefined;
|
|
469
|
+
/**
|
|
470
|
+
* Record the telemetry endpoint for the deferred `sdk_init` flush. Like the captured
|
|
471
|
+
* key, a later usable value overwrites an earlier one while an absent or malformed
|
|
472
|
+
* value leaves the last usable value in place. URL parsing is best-effort because an
|
|
473
|
+
* observability option must never break the visitor's page.
|
|
474
|
+
*/
|
|
475
|
+
export declare function captureTelemetryEndpoint(endpoint: unknown): void;
|
|
476
|
+
/** The endpoint {@link captureTelemetryEndpoint} last recorded, if any. */
|
|
477
|
+
export declare function telemetryEndpoint(): string | undefined;
|
|
441
478
|
/**
|
|
442
479
|
* Cancel the deferred `sdk_init` below. Called by `registerTools({ telemetry: false })`,
|
|
443
480
|
* whose opt-out is page-level and not merely batch-level: the flush describes the
|
|
@@ -462,6 +499,12 @@ export declare function cancelInitEvent(): void;
|
|
|
462
499
|
* Exported for tests; the scheduled timer below is the one that runs it on a real page.
|
|
463
500
|
*/
|
|
464
501
|
export declare function flushInitEvent(sinks?: TelemetrySinks): void;
|
|
502
|
+
/**
|
|
503
|
+
* Give a telemetry-live registration batch the next task's `sdk_init` flush. This
|
|
504
|
+
* keeps the batch's key and endpoint together while leaving the rest of the current
|
|
505
|
+
* task available for a page-level opt-out or another synchronous registration.
|
|
506
|
+
*/
|
|
507
|
+
export declare function deferInitEventForBatch(sinks: TelemetrySinks): void;
|
|
465
508
|
/**
|
|
466
509
|
* Run `run` after `ms`, returning a canceller. Guarded end to end: a runtime with no
|
|
467
510
|
* timers (SSR) and a hostile `setTimeout` (an extension, a fake-timer harness) both
|
|
@@ -469,3 +512,5 @@ export declare function flushInitEvent(sinks?: TelemetrySinks): void;
|
|
|
469
512
|
* `registerTools`. The canceller is safe to call when nothing was scheduled.
|
|
470
513
|
*/
|
|
471
514
|
export declare function afterDelay(run: () => void, ms: number): () => void;
|
|
515
|
+
/** How long a page that never registers gets to expose a real batch configuration. */
|
|
516
|
+
export declare const INIT_FALLBACK_MS = 1000;
|
package/dist/tracking.d.ts
CHANGED
|
@@ -81,8 +81,39 @@ export declare function getOrCreateSessionId(namespace: string): string;
|
|
|
81
81
|
export interface TrackingOptions {
|
|
82
82
|
/** Presence (non-empty) enables the backend transport and namespaces storage. */
|
|
83
83
|
apiKey?: string;
|
|
84
|
+
/**
|
|
85
|
+
* What generated this integration, as `<tool>[/<path>]@<version>` —
|
|
86
|
+
* `webmcp-kit/implement@<plugin version>` for the plugin's codegen,
|
|
87
|
+
* `webmcp-kit/connect-existing-tools@<plugin version>` for its migration of
|
|
88
|
+
* hand-written tools. Reported once per batch as `config.builtWith` on the
|
|
89
|
+
* default-on channel so kit-built sites are countable without a Connect; a
|
|
90
|
+
* hand-written integration leaves it unset. Free text under {@link MAX_BUILT_WITH_LENGTH}
|
|
91
|
+
* characters; anything else is dropped rather than truncated, because a partial
|
|
92
|
+
* `<tool>@<vers` is a wrong answer, not a shorter one.
|
|
93
|
+
*/
|
|
94
|
+
builtWith?: string;
|
|
84
95
|
/** Override the collect endpoint; ignored without `apiKey`. */
|
|
85
96
|
endpoint?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Report under a session identity the host already owns, verbatim, instead of
|
|
99
|
+
* the one this module mints in `sessionStorage`. For a host that is itself the
|
|
100
|
+
* page's session authority — the CDN snippet, whose tab session predates any
|
|
101
|
+
* `registerTools` call, or a Journey runner replaying under a synthetic `syn_…`
|
|
102
|
+
* id — the storage-minted id would split one visit into two sessions the
|
|
103
|
+
* pipeline cannot rejoin, because it lives in an `apiKey`-derived namespace the
|
|
104
|
+
* host does not share.
|
|
105
|
+
*
|
|
106
|
+
* Supplying it takes this module out of the session business entirely for that
|
|
107
|
+
* batch: nothing is read from or written to `sessionStorage`, including
|
|
108
|
+
* `last_seen`, so the 30-minute inactivity boundary is the host's to enforce.
|
|
109
|
+
* The `visitorId` is unaffected — a different lifetime, still this module's.
|
|
110
|
+
*
|
|
111
|
+
* Validated like a stored id ({@link MAX_ID_LENGTH}), because identity fields are
|
|
112
|
+
* copied onto every event without passing through the truncation ladder: an
|
|
113
|
+
* empty, oversized or non-string value falls back to the minted session rather
|
|
114
|
+
* than breaking `boundEventPayload`'s fit guarantee.
|
|
115
|
+
*/
|
|
116
|
+
sessionId?: string;
|
|
86
117
|
/** Emit each event as an OTEL LogRecord via the global `LoggerProvider`. */
|
|
87
118
|
otel?: boolean;
|
|
88
119
|
/**
|
package/dist/transport.d.ts
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* pages that never key the authenticated one, so a keyless send is the normal
|
|
24
24
|
* case, not a failure — the backend attributes those by CORS `Origin` instead.
|
|
25
25
|
*
|
|
26
|
+
* That path has exactly one observable failure signal, and it is a `console.error`,
|
|
27
|
+
* never a throw or a retry: a *keyed* beacon whose key the backend cannot resolve
|
|
28
|
+
* is still recorded (anonymously) and answered `401`, so a site shipping a broken
|
|
29
|
+
* key is otherwise indistinguishable from a working one. See {@link warnKeyRejected}.
|
|
30
|
+
*
|
|
26
31
|
* The second, independent output is OTEL (`emitOtelLog`): each event becomes a
|
|
27
32
|
* LogRecord on the global `LoggerProvider` via the optional peer dep
|
|
28
33
|
* `@opentelemetry/api-logs`. The host app owns exporters and processing; a
|
|
@@ -65,6 +70,13 @@ export declare function sendToCollect(event: TrackingEvent, config: CollectConfi
|
|
|
65
70
|
* wire. Note that adding the header makes the request non-simple under CORS, so
|
|
66
71
|
* an authenticated beacon costs a preflight the anonymous one does not.
|
|
67
72
|
*
|
|
73
|
+
* A `401` on this path means the key that WAS sent resolved to nothing; the beacon
|
|
74
|
+
* itself was still recorded, anonymously. That is reported through
|
|
75
|
+
* {@link warnKeyRejected} and nothing else — no throw, no retry, no second send.
|
|
76
|
+
* The response is only inspected for an authenticated beacon: an anonymous one has
|
|
77
|
+
* no key to be wrong about, so a `401` there would be a backend fault the developer
|
|
78
|
+
* can do nothing with, and blaming their key for it is worse than silence.
|
|
79
|
+
*
|
|
68
80
|
* The event stays a bare `object` here so this module never has to import the
|
|
69
81
|
* assembled shape from `telemetry.ts` (which imports this function).
|
|
70
82
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nekuda/webmcp-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|