@nekuda/webmcp-sdk 0.5.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 +110 -0
- package/README.md +32 -0
- package/dist/define.d.ts +39 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +255 -43
- package/dist/pages.d.ts +6 -0
- package/dist/register.d.ts +44 -5
- 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 +29 -1
- package/dist/tracking.d.ts +31 -0
- package/dist/transport.d.ts +24 -0
- package/package.json +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
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
|
+
|
|
3
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
|
|
@@ -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,8 +14,8 @@
|
|
|
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
|
|
@@ -38,7 +38,13 @@
|
|
|
38
38
|
* both outputs — but for THIS channel only; the default-on telemetry channel of
|
|
39
39
|
* point 6 has its own opt-out (`telemetry: false`) and `disabled` does not
|
|
40
40
|
* narrow it. See `src/tracking.ts`, `src/transport.ts`, and
|
|
41
|
-
* `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.
|
|
42
48
|
* 6. Anonymous usage telemetry is a SECOND, independent channel: default-ON, so it
|
|
43
49
|
* reports from every site the SDK runs on, not only from `apiKey` tenants. Three
|
|
44
50
|
* events (`TelemetryEvent`), all `schema: 2` and joined on an in-memory
|
|
@@ -74,7 +80,8 @@
|
|
|
74
80
|
* platform stays a config-only change to the entry module (point 4).
|
|
75
81
|
*/
|
|
76
82
|
export { type AnyWebMCPTool, type ToolDefinition, type ToolIntent, type ToolSource, type WebMCPTool, defineTool, } from "./define.js";
|
|
77
|
-
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";
|
|
78
85
|
export { type ModelContextLike, type RegisterToolOptions, type SpecTool, type ToolAnnotations, resolveModelContext, } from "./spec.js";
|
|
79
86
|
export type { AgentRuntime, FormFactor, FrameContext, PageVisibility, ReferrerClass, SurfaceGlobal, SurfaceInfo, SurfaceProvenance, } from "./telemetry-context.js";
|
|
80
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,22 +49,97 @@ 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
|
|
|
71
138
|
// src/transport.ts
|
|
72
|
-
var INGEST_BASE = "https://ingest.agentlane.
|
|
139
|
+
var INGEST_BASE = "https://ingest.agentlane.dev";
|
|
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);
|
|
@@ -98,16 +179,21 @@ function sendToCollect(event, config, scope = globalThis) {
|
|
|
98
179
|
tryFetch(scope, url, headers, json);
|
|
99
180
|
} catch {}
|
|
100
181
|
}
|
|
182
|
+
var TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
101
183
|
function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
102
184
|
try {
|
|
103
185
|
const json = JSON.stringify(event);
|
|
104
186
|
if (json === undefined)
|
|
105
187
|
return;
|
|
106
188
|
const url = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
|
|
107
|
-
const headers = { "content-type":
|
|
108
|
-
|
|
189
|
+
const headers = { "content-type": TELEMETRY_CONTENT_TYPE };
|
|
190
|
+
const authenticated = typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
191
|
+
if (authenticated)
|
|
109
192
|
headers["x-api-key"] = apiKey;
|
|
110
|
-
tryFetch(scope, url, headers, json)
|
|
193
|
+
tryFetch(scope, url, headers, json, authenticated ? (response) => {
|
|
194
|
+
if (isUnauthorized(response))
|
|
195
|
+
warnKeyRejected(scope);
|
|
196
|
+
} : undefined);
|
|
111
197
|
} catch {}
|
|
112
198
|
}
|
|
113
199
|
var OTEL_LOGGER_NAME = "@nekuda/webmcp-sdk";
|
|
@@ -281,6 +367,14 @@ function getOrCreateSessionId(namespace) {
|
|
|
281
367
|
return id;
|
|
282
368
|
return fallbackId(memorySession, namespace, id);
|
|
283
369
|
}
|
|
370
|
+
function resolveSessionId(options, namespace) {
|
|
371
|
+
try {
|
|
372
|
+
const supplied = options.sessionId;
|
|
373
|
+
if (typeof supplied === "string" && usableId(supplied))
|
|
374
|
+
return supplied;
|
|
375
|
+
} catch {}
|
|
376
|
+
return getOrCreateSessionId(namespace);
|
|
377
|
+
}
|
|
284
378
|
function trackingOutputs(options) {
|
|
285
379
|
try {
|
|
286
380
|
if (!options || options.disabled)
|
|
@@ -331,9 +425,17 @@ function buildEventPayload(params) {
|
|
|
331
425
|
eventName: params.eventName,
|
|
332
426
|
ts: new Date().toISOString(),
|
|
333
427
|
...pageFields(),
|
|
334
|
-
...params.data
|
|
428
|
+
...params.data,
|
|
429
|
+
...isSyntheticTester() ? { syntheticTester: true } : {}
|
|
335
430
|
};
|
|
336
431
|
}
|
|
432
|
+
function isSyntheticTester() {
|
|
433
|
+
try {
|
|
434
|
+
return /^nekuda-synthetic-tester\/[0-9]/i.test(globalThis.navigator?.userAgent ?? "");
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
337
439
|
var MAX_EVENT_BYTES = 64 * 1024;
|
|
338
440
|
var MAX_ERROR_BYTES = 16 * 1024;
|
|
339
441
|
var TRUNCATABLE = ["response", "input", "error"];
|
|
@@ -469,7 +571,7 @@ function track(options, eventName, data, sinks = defaultSinks) {
|
|
|
469
571
|
const namespace = storageNamespace(options.apiKey);
|
|
470
572
|
const event = boundEventPayload(buildEventPayload({
|
|
471
573
|
visitorId: getOrCreateVisitorId(namespace),
|
|
472
|
-
sessionId:
|
|
574
|
+
sessionId: resolveSessionId(options, namespace),
|
|
473
575
|
eventName,
|
|
474
576
|
data
|
|
475
577
|
}));
|
|
@@ -761,6 +863,7 @@ var TELEMETRY_FIELDS = {
|
|
|
761
863
|
event: true,
|
|
762
864
|
ts: true,
|
|
763
865
|
sessionId: true,
|
|
866
|
+
sampleRate: true,
|
|
764
867
|
"sdk.name": true,
|
|
765
868
|
"sdk.version": true,
|
|
766
869
|
"sdk.installMode": true,
|
|
@@ -785,6 +888,7 @@ var TELEMETRY_FIELDS = {
|
|
|
785
888
|
"config.trackingEnabled": true,
|
|
786
889
|
"config.otelEnabled": true,
|
|
787
890
|
"config.customEndpoint": true,
|
|
891
|
+
"config.builtWith": true,
|
|
788
892
|
tools: true,
|
|
789
893
|
callId: true,
|
|
790
894
|
callIndex: true,
|
|
@@ -805,6 +909,8 @@ var TELEMETRY_FIELDS = {
|
|
|
805
909
|
var TELEMETRY_TOOL_FIELDS = {
|
|
806
910
|
name: true,
|
|
807
911
|
stableKey: true,
|
|
912
|
+
inventoryToolId: true,
|
|
913
|
+
contractRevision: true,
|
|
808
914
|
version: true,
|
|
809
915
|
schemaHash: true,
|
|
810
916
|
source: true,
|
|
@@ -999,9 +1105,20 @@ function shapeMetrics(inputSchema) {
|
|
|
999
1105
|
|
|
1000
1106
|
// src/telemetry.ts
|
|
1001
1107
|
var SDK_NAME = "@nekuda/webmcp-sdk";
|
|
1002
|
-
var SDK_VERSION = "0.
|
|
1108
|
+
var SDK_VERSION = "0.6.0-dev.17.1";
|
|
1003
1109
|
var INSTALL_MODES = ["npm", "cdn_snippet"];
|
|
1004
1110
|
var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
|
|
1111
|
+
function parseSampleRate(raw) {
|
|
1112
|
+
const rate = typeof raw === "string" ? Number(raw) : Number.NaN;
|
|
1113
|
+
return Number.isFinite(rate) && rate > 0 && rate < 1 ? rate : 1;
|
|
1114
|
+
}
|
|
1115
|
+
var SDK_TELEMETRY_SAMPLE_RATE = parseSampleRate(typeof __WEBMCP_TELEMETRY_SAMPLE_RATE__ === "string" ? __WEBMCP_TELEMETRY_SAMPLE_RATE__ : undefined);
|
|
1116
|
+
var SAMPLED_EVENTS = new Set(["sdk_init", "tool_registration"]);
|
|
1117
|
+
function pageSampled(sessionId, rate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1118
|
+
if (rate >= 1)
|
|
1119
|
+
return true;
|
|
1120
|
+
return Number.parseInt(fnv1a(`sample:${sessionId}`), 16) / 4294967296 < rate;
|
|
1121
|
+
}
|
|
1005
1122
|
function isFieldParent(value) {
|
|
1006
1123
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1007
1124
|
}
|
|
@@ -1122,12 +1239,15 @@ function buildInitEvent(params = {}) {
|
|
|
1122
1239
|
}
|
|
1123
1240
|
function configFields(tracking) {
|
|
1124
1241
|
const { toBackend, toOtel } = trackingOutputs(tracking);
|
|
1242
|
+
const builtWith = safe(() => tracking?.builtWith);
|
|
1125
1243
|
return {
|
|
1126
1244
|
trackingEnabled: toBackend,
|
|
1127
1245
|
otelEnabled: toOtel,
|
|
1128
|
-
customEndpoint: Boolean(safe(() => tracking?.endpoint))
|
|
1246
|
+
customEndpoint: Boolean(safe(() => tracking?.endpoint)),
|
|
1247
|
+
...typeof builtWith === "string" && builtWith.length > 0 && builtWith.length <= MAX_BUILT_WITH_LENGTH ? { builtWith } : {}
|
|
1129
1248
|
};
|
|
1130
1249
|
}
|
|
1250
|
+
var MAX_BUILT_WITH_LENGTH = 64;
|
|
1131
1251
|
var MAX_TOOL_FIELD_BYTES = 4 * 1024;
|
|
1132
1252
|
function toolString(value) {
|
|
1133
1253
|
return sliceToBytes(value, MAX_TOOL_FIELD_BYTES);
|
|
@@ -1135,6 +1255,9 @@ function toolString(value) {
|
|
|
1135
1255
|
function toolEnum(allowed, value) {
|
|
1136
1256
|
return allowed.find((candidate) => candidate === value);
|
|
1137
1257
|
}
|
|
1258
|
+
function toolRevision(value) {
|
|
1259
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
1260
|
+
}
|
|
1138
1261
|
var ANNOTATION_HINTS = ["readOnlyHint", "untrustedContentHint"];
|
|
1139
1262
|
function annotationHints(annotations) {
|
|
1140
1263
|
const hints = {};
|
|
@@ -1156,6 +1279,8 @@ function toolEntry(entry) {
|
|
|
1156
1279
|
return {
|
|
1157
1280
|
name: toolString(tool.name),
|
|
1158
1281
|
stableKey: toolString(tool.stableKey),
|
|
1282
|
+
...typeof tool.inventoryToolId === "string" ? { inventoryToolId: toolString(tool.inventoryToolId) } : {},
|
|
1283
|
+
...toolRevision(tool.contractRevision) !== undefined ? { contractRevision: toolRevision(tool.contractRevision) } : {},
|
|
1159
1284
|
...tool.version !== undefined ? { version: toolString(tool.version) } : {},
|
|
1160
1285
|
...hash !== undefined ? { schemaHash: hash } : {},
|
|
1161
1286
|
...source !== undefined ? { source } : {},
|
|
@@ -1253,6 +1378,8 @@ function buildToolCallEvent(params) {
|
|
|
1253
1378
|
...sinceInit !== undefined ? { timeSinceInitMs: sinceInit } : {},
|
|
1254
1379
|
tool: {
|
|
1255
1380
|
stableKey: toolString(params.tool.stableKey),
|
|
1381
|
+
...typeof params.tool.inventoryToolId === "string" ? { inventoryToolId: toolString(params.tool.inventoryToolId) } : {},
|
|
1382
|
+
...toolRevision(params.tool.contractRevision) !== undefined ? { contractRevision: toolRevision(params.tool.contractRevision) } : {},
|
|
1256
1383
|
...hash !== undefined ? { schemaHash: hash } : {},
|
|
1257
1384
|
...intent !== undefined ? { intent } : {}
|
|
1258
1385
|
},
|
|
@@ -1353,11 +1480,17 @@ function telemetryTenantScope(apiKey) {
|
|
|
1353
1480
|
const resolved = resolveTelemetryKey(apiKey);
|
|
1354
1481
|
return resolved === undefined ? "" : fnv1a(resolved);
|
|
1355
1482
|
}
|
|
1356
|
-
function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS, toolFields = TELEMETRY_TOOL_FIELDS) {
|
|
1483
|
+
function emitTelemetry(build, sinks = defaultSinks2, fields = TELEMETRY_FIELDS, toolFields = TELEMETRY_TOOL_FIELDS, sampleRate = SDK_TELEMETRY_SAMPLE_RATE) {
|
|
1357
1484
|
try {
|
|
1358
1485
|
if (!telemetryEnabled())
|
|
1359
1486
|
return;
|
|
1360
|
-
|
|
1487
|
+
const event = { ...build() };
|
|
1488
|
+
if (SAMPLED_EVENTS.has(String(event.event)) && sampleRate < 1) {
|
|
1489
|
+
if (!pageSampled(String(event.sessionId), sampleRate))
|
|
1490
|
+
return;
|
|
1491
|
+
event.sampleRate = sampleRate;
|
|
1492
|
+
}
|
|
1493
|
+
sinks.sendTelemetry(boundEventPayload(pruneByAllowlist(event, fields, toolFields)));
|
|
1361
1494
|
} catch {}
|
|
1362
1495
|
}
|
|
1363
1496
|
var initCancelled = false;
|
|
@@ -1445,15 +1578,18 @@ function clock() {
|
|
|
1445
1578
|
const perf = safe(() => globalThis.performance);
|
|
1446
1579
|
const now = safe(() => perf?.now);
|
|
1447
1580
|
if (typeof now === "function") {
|
|
1448
|
-
const
|
|
1449
|
-
const
|
|
1450
|
-
if (
|
|
1451
|
-
return () => elapsedMs(
|
|
1581
|
+
const read = () => safe(() => now.call(perf));
|
|
1582
|
+
const start = read();
|
|
1583
|
+
if (start !== undefined)
|
|
1584
|
+
return () => elapsedMs(read(), start);
|
|
1452
1585
|
}
|
|
1453
1586
|
const read = () => safe(() => Date.now());
|
|
1454
1587
|
const start = read();
|
|
1455
1588
|
return () => elapsedMs(read(), start);
|
|
1456
1589
|
}
|
|
1590
|
+
function createCallTracker(tool, tracking) {
|
|
1591
|
+
return trackerFor({ ...tool, stableKey: tool.name }, tracking);
|
|
1592
|
+
}
|
|
1457
1593
|
function trackerFor(tool, tracking) {
|
|
1458
1594
|
if (!tracking)
|
|
1459
1595
|
return;
|
|
@@ -1468,9 +1604,24 @@ function trackerFor(tool, tracking) {
|
|
|
1468
1604
|
};
|
|
1469
1605
|
return (eventName, data) => track(tracking, eventName, { ...shared, ...data });
|
|
1470
1606
|
}
|
|
1607
|
+
var TRACKED_TOOL_FLAG = Symbol.for("webmcp.sdk.tracked");
|
|
1608
|
+
function markTracked(spec, tracking) {
|
|
1609
|
+
const { toBackend } = trackingOutputs(tracking);
|
|
1610
|
+
if (!toBackend)
|
|
1611
|
+
return spec;
|
|
1612
|
+
try {
|
|
1613
|
+
Object.defineProperty(spec, TRACKED_TOOL_FLAG, {
|
|
1614
|
+
value: true,
|
|
1615
|
+
writable: false,
|
|
1616
|
+
configurable: true,
|
|
1617
|
+
enumerable: false
|
|
1618
|
+
});
|
|
1619
|
+
} catch {}
|
|
1620
|
+
return spec;
|
|
1621
|
+
}
|
|
1471
1622
|
function toSpecTool(tool, channels) {
|
|
1472
|
-
const { tracking, telemetry, telemetrySinks, telemetryKey, telemetryEndpoint
|
|
1473
|
-
|
|
1623
|
+
const { tracking, telemetry, telemetrySinks, telemetryKey, telemetryEndpoint } = channels;
|
|
1624
|
+
const spec = {
|
|
1474
1625
|
name: tool.name,
|
|
1475
1626
|
...tool.title !== undefined ? { title: tool.title } : {},
|
|
1476
1627
|
description: tool.description,
|
|
@@ -1482,7 +1633,7 @@ function toSpecTool(tool, channels) {
|
|
|
1482
1633
|
return normalizeResult(await tool.execute(input));
|
|
1483
1634
|
const callKey = telemetry ? resolveTelemetryKey(telemetryKey) : undefined;
|
|
1484
1635
|
const sequence = telemetry ? nextCall(tool.stableKey, telemetryTenantScope(callKey)) : undefined;
|
|
1485
|
-
const callSinks = sequence ? batchTelemetrySinks(callKey,
|
|
1636
|
+
const callSinks = sequence ? batchTelemetrySinks(callKey, telemetryEndpoint) : telemetrySinks;
|
|
1486
1637
|
const elapsed = clock();
|
|
1487
1638
|
trackCall?.("tool_call_request", { input });
|
|
1488
1639
|
try {
|
|
@@ -1506,6 +1657,7 @@ function toSpecTool(tool, channels) {
|
|
|
1506
1657
|
}
|
|
1507
1658
|
}
|
|
1508
1659
|
};
|
|
1660
|
+
return markTracked(spec, tracking);
|
|
1509
1661
|
}
|
|
1510
1662
|
var REGISTRATION_TIMEOUT_MS = 2000;
|
|
1511
1663
|
var PAGEHIDE = "pagehide";
|
|
@@ -1521,7 +1673,7 @@ function onPagehide(run) {
|
|
|
1521
1673
|
safe(() => remove.call(g, PAGEHIDE, listener));
|
|
1522
1674
|
};
|
|
1523
1675
|
}
|
|
1524
|
-
function watchRegistration(tools, tracking, sinks) {
|
|
1676
|
+
function watchRegistration(tools, tracking, sinks, navigation = false) {
|
|
1525
1677
|
const entries = tools.map((tool) => ({ tool, outcome: "pending" }));
|
|
1526
1678
|
const elapsed = clock();
|
|
1527
1679
|
const { registrationIndex, trigger } = nextRegistration();
|
|
@@ -1536,7 +1688,7 @@ function watchRegistration(tools, tracking, sinks) {
|
|
|
1536
1688
|
const settleMs = elapsed();
|
|
1537
1689
|
emitTelemetry(() => buildToolRegistrationEvent({
|
|
1538
1690
|
registrationIndex,
|
|
1539
|
-
trigger,
|
|
1691
|
+
trigger: navigation ? "spa_navigation" : trigger,
|
|
1540
1692
|
settleMs,
|
|
1541
1693
|
tools: entries,
|
|
1542
1694
|
tracking
|
|
@@ -1608,32 +1760,89 @@ function registerTools(tools, options = {}) {
|
|
|
1608
1760
|
state,
|
|
1609
1761
|
...error !== undefined ? { error } : {}
|
|
1610
1762
|
});
|
|
1611
|
-
const
|
|
1763
|
+
const active = new Map;
|
|
1764
|
+
const current = () => tools.filter((tool) => active.get(tool)?.registered).map((tool) => tool.name);
|
|
1765
|
+
const changed = () => {
|
|
1766
|
+
safe(() => options.onChange?.(current()));
|
|
1767
|
+
};
|
|
1768
|
+
const eligible = (tool) => {
|
|
1769
|
+
const page = currentPageKey();
|
|
1770
|
+
return page === undefined || matchPage(tool.pages, page);
|
|
1771
|
+
};
|
|
1772
|
+
const remove = (tool) => {
|
|
1773
|
+
const entry = active.get(tool);
|
|
1774
|
+
if (!entry?.removable)
|
|
1775
|
+
return;
|
|
1776
|
+
entry.lifetime.abort();
|
|
1777
|
+
safe(() => modelContext?.unregisterTool?.(tool.name));
|
|
1778
|
+
active.delete(tool);
|
|
1779
|
+
};
|
|
1612
1780
|
const settle = async (tool) => {
|
|
1613
1781
|
if (!modelContext)
|
|
1614
1782
|
return result(tool, "unsupported");
|
|
1615
1783
|
if (controller.signal.aborted)
|
|
1616
1784
|
return result(tool, "aborted");
|
|
1785
|
+
const entry = {
|
|
1786
|
+
lifetime: tool.pages?.length ? new AbortController : controller,
|
|
1787
|
+
removable: typeof modelContext.unregisterTool === "function",
|
|
1788
|
+
registered: false
|
|
1789
|
+
};
|
|
1790
|
+
active.set(tool, entry);
|
|
1617
1791
|
try {
|
|
1618
1792
|
await Promise.resolve(modelContext.registerTool(toSpecTool(tool, channels), {
|
|
1619
|
-
signal
|
|
1793
|
+
get signal() {
|
|
1794
|
+
entry.removable = true;
|
|
1795
|
+
return entry.lifetime.signal;
|
|
1796
|
+
}
|
|
1620
1797
|
}));
|
|
1621
|
-
|
|
1798
|
+
if (entry.lifetime.signal.aborted)
|
|
1799
|
+
return result(tool, "aborted");
|
|
1800
|
+
entry.registered = true;
|
|
1801
|
+
if (!eligible(tool))
|
|
1802
|
+
remove(tool);
|
|
1803
|
+
changed();
|
|
1804
|
+
return result(tool, entry.lifetime.signal.aborted ? "aborted" : "registered");
|
|
1622
1805
|
} catch (error) {
|
|
1623
|
-
if (
|
|
1806
|
+
if (active.get(tool) === entry)
|
|
1807
|
+
active.delete(tool);
|
|
1808
|
+
if (entry.lifetime.signal.aborted)
|
|
1624
1809
|
return result(tool, "aborted");
|
|
1625
1810
|
return result(tool, "failed", error);
|
|
1626
1811
|
}
|
|
1627
1812
|
};
|
|
1628
|
-
const
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1813
|
+
const register = (batch, navigation = false) => {
|
|
1814
|
+
const watch = channels.telemetry ? watchRegistration(batch, channels.tracking, channels.telemetrySinks, navigation) : undefined;
|
|
1815
|
+
const ready = Promise.all(batch.map(async (tool, index) => {
|
|
1816
|
+
const settled = await settle(tool);
|
|
1817
|
+
watch?.record(index, settled);
|
|
1818
|
+
return settled;
|
|
1819
|
+
}));
|
|
1820
|
+
if (watch)
|
|
1821
|
+
ready.then(() => watch.emit());
|
|
1822
|
+
return ready;
|
|
1823
|
+
};
|
|
1824
|
+
const reconcile = () => {
|
|
1825
|
+
if (controller.signal.aborted)
|
|
1826
|
+
return;
|
|
1827
|
+
for (const tool of active.keys())
|
|
1828
|
+
if (!eligible(tool))
|
|
1829
|
+
remove(tool);
|
|
1830
|
+
changed();
|
|
1831
|
+
const added = tools.filter((tool) => eligible(tool) && !active.has(tool));
|
|
1832
|
+
if (added.length)
|
|
1833
|
+
register(added, true);
|
|
1834
|
+
};
|
|
1835
|
+
const stop = !controller.signal.aborted && tools.some((tool) => tool.pages?.length) ? onPageChange(reconcile) : () => {};
|
|
1836
|
+
controller.signal.addEventListener("abort", () => {
|
|
1837
|
+
stop();
|
|
1838
|
+
for (const tool of active.keys())
|
|
1839
|
+
remove(tool);
|
|
1840
|
+
changed();
|
|
1841
|
+
}, { once: true });
|
|
1842
|
+
const ready = register(tools.filter(eligible));
|
|
1635
1843
|
return {
|
|
1636
1844
|
ready,
|
|
1845
|
+
current,
|
|
1637
1846
|
signal: controller.signal,
|
|
1638
1847
|
unregister() {
|
|
1639
1848
|
controller.abort();
|
|
@@ -1641,7 +1850,10 @@ function registerTools(tools, options = {}) {
|
|
|
1641
1850
|
};
|
|
1642
1851
|
}
|
|
1643
1852
|
export {
|
|
1853
|
+
createCallTracker,
|
|
1854
|
+
currentPageKey,
|
|
1644
1855
|
defineTool,
|
|
1856
|
+
matchPage,
|
|
1645
1857
|
registerTools,
|
|
1646
1858
|
resolveModelContext
|
|
1647
1859
|
};
|
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()`.
|
|
@@ -64,18 +66,55 @@ export interface ToolRegistration {
|
|
|
64
66
|
* cannot mask the rest.
|
|
65
67
|
*/
|
|
66
68
|
ready: Promise<ToolRegistrationResult[]>;
|
|
69
|
+
/** Currently registered names, in declaration order; `ready` covers only the initial page. */
|
|
70
|
+
current(): string[];
|
|
67
71
|
/** Unregister every tool in this batch. Idempotent. */
|
|
68
72
|
unregister(): void;
|
|
69
73
|
/** The signal carrying this registration's lifetime (aborted once unregistered). */
|
|
70
74
|
signal: AbortSignal;
|
|
71
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;
|
|
72
111
|
/**
|
|
73
112
|
* Register a batch of `defineTool` tools on the page's WebMCP surface.
|
|
74
113
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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.
|
|
80
119
|
*/
|
|
81
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;
|
|
@@ -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
|
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,9 +70,28 @@ 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
|
+
* The body is sent as `text/plain`, not `application/json`, and that is a cost
|
|
74
|
+
* decision, not a formatting one: `text/plain` is a CORS-safelisted content type,
|
|
75
|
+
* so an anonymous beacon is a "simple request" and the browser sends it with no
|
|
76
|
+
* OPTIONS preflight. With `application/json` every page load paid two requests
|
|
77
|
+
* per event at the edge; at 2026-09 volume that preflight was roughly half of the
|
|
78
|
+
* seven million requests a day the collect API billed. The edge maps `text/plain`
|
|
79
|
+
* to the same template as JSON (ingestion-edge/telemetry.tf), so nothing changes
|
|
80
|
+
* on the wire but the header. The keyed beacon keeps `x-api-key`, which forces a
|
|
81
|
+
* preflight regardless of content type; that path is a small fraction of traffic.
|
|
82
|
+
*
|
|
83
|
+
* A `401` on this path means the key that WAS sent resolved to nothing; the beacon
|
|
84
|
+
* itself was still recorded, anonymously. That is reported through
|
|
85
|
+
* {@link warnKeyRejected} and nothing else — no throw, no retry, no second send.
|
|
86
|
+
* The response is only inspected for an authenticated beacon: an anonymous one has
|
|
87
|
+
* no key to be wrong about, so a `401` there would be a backend fault the developer
|
|
88
|
+
* can do nothing with, and blaming their key for it is worse than silence.
|
|
89
|
+
*
|
|
68
90
|
* The event stays a bare `object` here so this module never has to import the
|
|
69
91
|
* assembled shape from `telemetry.ts` (which imports this function).
|
|
70
92
|
*/
|
|
93
|
+
/** CORS-safelisted, so an anonymous beacon needs no preflight — see sendTelemetry. */
|
|
94
|
+
export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
71
95
|
export declare function sendTelemetry(event: object, scope?: object, endpoint?: string, apiKey?: unknown): void;
|
|
72
96
|
/** The `Logger.emit` slice we use — structural, no hard OTEL type coupling. */
|
|
73
97
|
interface LoggerLike {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nekuda/webmcp-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-dev.17.1",
|
|
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",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"CHANGELOG.md"
|
|
17
|
+
],
|
|
15
18
|
"publishConfig": {
|
|
16
19
|
"access": "public"
|
|
17
20
|
},
|
|
@@ -36,5 +39,7 @@
|
|
|
36
39
|
"optional": true
|
|
37
40
|
}
|
|
38
41
|
},
|
|
39
|
-
"trustedDependencies": [
|
|
42
|
+
"trustedDependencies": [
|
|
43
|
+
"@biomejs/biome"
|
|
44
|
+
]
|
|
40
45
|
}
|