@nekuda/webmcp-sdk 0.4.0 → 0.6.0-dev.17.1

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,105 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased — usage telemetry schema 2 (breaking wire format)
3
+ ## Unreleased — telemetry beacons without a preflight
4
+
5
+ - The telemetry beacon is sent as `text/plain` instead of `application/json`. The
6
+ body is unchanged; the header makes an anonymous beacon a CORS simple request, so
7
+ the browser stops sending an `OPTIONS` preflight before every event. Keyed beacons
8
+ still carry `x-api-key` and still preflight. The edge has mapped `text/plain` to
9
+ the same template since ADR-0014, so no server change is needed.
10
+
11
+ ## 0.6.0 — 2026-09-18
12
+
13
+ Five additive changes, no breaking change. The wire schema stays at `2`. Four of them
14
+ are opt-in options — `builtWith`, `pages`, `sessionId` and `createCallTracker` — each
15
+ silent when unset, so **an npm consumer who sets none of them sends exactly the bytes
16
+ 0.5.0 sent**. The fifth is a non-enumerable mark this SDK writes on the tool objects it
17
+ registers under a batch that is already posting to the collect edge; it is invisible to
18
+ enumeration and to `JSON` and never reaches the wire, so it changes no bytes either.
19
+
20
+ Beside them is a **mechanism nothing switches on**: page-level telemetry sampling exists
21
+ as a `bun build --define`, and no build this release ships passes a rate. **Every
22
+ install, npm and CDN snippet alike, is unsampled** and sends every page-level event,
23
+ exactly as 0.5.0 did.
24
+
25
+ ### Page-level telemetry sampling — available, off everywhere
26
+
27
+ - A build **may** sample the two page-level telemetry events (`sdk_init`,
28
+ `tool_registration`) with `--define '__WEBMCP_TELEMETRY_SAMPLE_RATE__="0.1"'`. The
29
+ decision is one coin per page load from the `sessionId`, so a page's events are kept
30
+ or dropped together, and every kept event carries `sampleRate` for weighting.
31
+ `tool_call` is never sampled. Unset, `1`, or anything outside (0, 1), sends
32
+ everything — the previous behaviour byte for byte.
33
+ - **Nothing opts in.** Neither publish lane passes the define, and the CDN snippet
34
+ build (`infra/deploy-snippet.sh`) defines the rate as `1`. These two events carry
35
+ the connects, registered-tool counts, refreshes and unique user agents the product
36
+ counts active users from, so they ship complete; the beacon volume gets a fast-path
37
+ ingest queue rather than a tenth of the observations. Turning sampling on for one
38
+ deploy is `SNIPPET_TELEMETRY_SAMPLE_RATE=<rate>` in front of that script, and the
39
+ kept events then say what rate they were kept at.
40
+
41
+ ### `tracking.builtWith`
42
+
43
+ - **`TrackingOptions.builtWith`** names what generated the integration, as
44
+ `<tool>[/<path>]@<version>` (`webmcp-kit/implement@<plugin version>`,
45
+ `webmcp-kit/connect-existing-tools@<plugin version>`), and is reported verbatim
46
+ as `config.builtWith` on `tool_registration`. Additive and optional: unset emits
47
+ exactly the bytes it did before. It exists so kit-built sites are countable on
48
+ the default-on channel without a Connect. Dropped, never truncated, when empty
49
+ or over 64 characters.
50
+
51
+ ### Page-scoped tools
52
+
53
+ - `defineTool({ pages })` filters registration on load and SPA navigation; `matchPage`
54
+ and `currentPageKey` expose the fixture-pinned matching rule. SDK metadata stays off
55
+ the native tool object. `ready` covers the initial filtered set; `current()` and
56
+ `onChange` expose live names. Unregistration uses native abort signals, with legacy
57
+ `unregisterTool` support and explicit retention on surfaces supporting neither.
58
+
59
+ ### A host may own the session and track its own tools
60
+
61
+ Two additive `tracking`-channel surfaces for a host that owns the page it runs on
62
+ (the CDN snippet). Nothing existing changes: both are opt-in, both are silent
63
+ under the same `trackingOutputs` gate as everything else on this channel, and a
64
+ consumer that sets neither emits exactly the bytes it did before.
65
+
66
+ - **`TrackingOptions.sessionId`** reports under a session identity the host
67
+ already has — a tab session that predates any `registerTools` call, or a
68
+ Journey runner's synthetic `syn_…` id — instead of the one this channel mints
69
+ in `sessionStorage` under an `apiKey`-derived namespace, which would split one
70
+ visit into two sessions the pipeline cannot rejoin. Supplying it means no
71
+ `sessionStorage` read or write at all for that batch, `last_seen` included, so
72
+ the 30-minute inactivity boundary becomes the host's to enforce; `visitorId` is
73
+ untouched. Validated like a stored id (non-empty, ≤ 64 chars, a string) —
74
+ anything else falls back to the minted session, because identity fields skip the
75
+ truncation ladder.
76
+ - **`createCallTracker(tool, tracking)`** emits the request/response pair for a
77
+ tool the host registered on `document.modelContext` itself, in the same bytes an
78
+ SDK-registered tool's call produces — same event names, same correlated `callId`,
79
+ same anonymous identity — so the projection reads one shape rather than two. The
80
+ host owns the rest: one tracker per invocation, `tool_call_request` before the
81
+ handler and `tool_call_response` after, and the `duration_ms` it reports. The
82
+ tool's `name` is its `stableKey`, since a host-wrapped tool has no
83
+ developer-authored durable identity to carry — and that rule is applied *over*
84
+ the caller's object, so a `stableKey` riding in on a tool descriptor from a page
85
+ the host does not control cannot key that tool differently from every other
86
+ reader of the same page.
87
+
88
+ ### A tracked tool says so, for a host that owns the surface
89
+
90
+ - A tool registered by a batch that posts to the collect edge now carries the registry
91
+ symbol `Symbol.for("webmcp.sdk.tracked")` (non-enumerable, value `true`) on the object
92
+ handed to `registerTool`. It exists for a host that owns the page's WebMCP surface and
93
+ wraps what it finds there — the CDN snippet's coexistence gate — which could not
94
+ otherwise tell a tool whose calls this SDK already reports from a merchant's bare one,
95
+ and reported every such invocation a second time. Nothing else changes: the mark is
96
+ written only when the channel really is posting (an unkeyed batch, an empty
97
+ `tracking: {}`, a closed consent gate and an otel-only batch carry none), it is
98
+ invisible to enumeration and to `JSON`, and no export is added. It is the one change
99
+ here that is not an option a consumer sets — but being non-enumerable it reaches
100
+ neither the wire nor `JSON.stringify`, so no bytes move.
101
+
102
+ ## 0.5.0 — 2026-08-24 — usage telemetry schema 2 (breaking wire format)
4
103
 
5
104
  The usage-telemetry channel emits **three events instead of two**, carrying
6
105
  derived signals instead of raw page and payload strings. `schema` bumps `1` → `2`.
@@ -11,11 +110,11 @@ unchanged. See `docs/telemetry-schema.md`.
11
110
 
12
111
  ### Breaking
13
112
 
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.
113
+ - **`sdk_init` fires once per page load, not once per `registerTools` call.** The
114
+ first telemetry-live batch schedules a next-task flush with its own key and
115
+ endpoint; an import-time one-second fallback still fires when nobody registers
116
+ anything, which is the only way a broken integration is visible. A page with
117
+ three `registerTools` calls no longer emits three "init" events.
19
118
  - **Per-call information moved to the new `tool_registration` event**, one per
20
119
  `registerTools` call: `registrationIndex`, `trigger`, `settleMs`, the `config.*`
21
120
  booleans (moved off `sdk_init`, which fires before any batch may have run), and
@@ -65,6 +164,17 @@ unchanged. See `docs/telemetry-schema.md`.
65
164
 
66
165
  ### Added
67
166
 
167
+ - **One `console.error` when a configured publishable key is refused.** The telemetry
168
+ route now answers `401` for a key that was *sent* and did not resolve; the beacon is
169
+ still recorded, anonymously, so nothing is lost but the attribution. The SDK reports
170
+ that at most once per page load — naming the SDK, saying recording continues
171
+ anonymously before saying what broke, and pointing at re-running Connect. It never
172
+ echoes the key or anything off the wire. No throw, no retry, no second send, and no
173
+ change to the send path. Anonymous beacons are never inspected (they have no key to
174
+ be wrong about), and every other status, a network failure, and an environment with
175
+ no `console` stay silent as before. This narrows 0.2.0's "the SDK is silent by
176
+ default": it is the only output the package produces, and only a broken key produces
177
+ it.
68
178
  - **`globalThis.__WEBMCP_TELEMETRY__ = false`** silences the channel page-wide
69
179
  without touching a `registerTools` call site — the only lever a site has when the
70
180
  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 }`. Unregistration happens ONLY via
18
- * `unregister()` or the external signal aborting (page teardown / SPA unmount).
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 (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
@@ -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 { type RegisterToolsOptions, type ToolRegistration, type ToolRegistrationResult, type ToolRegistrationState, registerTools, } from "./register.js";
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";