@nekuda/webmcp-sdk 0.7.0-dev.20.1 → 0.7.0-dev.22.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 +26 -13
- package/README.md +273 -30
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -2
- package/dist/register.d.ts +3 -2
- package/dist/telemetry.d.ts +4 -3
- package/dist/transport.d.ts +21 -16
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## Unreleased — the telemetry key rides the query string
|
|
4
|
-
|
|
5
|
-
- A keyed telemetry beacon sends its publishable key as the `key` query parameter
|
|
6
|
-
instead of the `x-api-key` header. The header made every keyed beacon a non-simple
|
|
7
|
-
request with an `OPTIONS` preflight, and every hosted-snippet install is keyed. The
|
|
8
|
-
backend reads both; nothing else changes.
|
|
9
|
-
|
|
10
3
|
## 0.7.0 — 2026-09-21
|
|
11
4
|
|
|
12
5
|
One change, made twice: a page load costs the ingest edge one request instead of
|
|
13
6
|
four. The wire schema stays at `2` and every event still arrives — the two savings
|
|
14
|
-
are a
|
|
15
|
-
second of them puts a request shape on the wire that no earlier backend parses: a
|
|
7
|
+
are a request shape and an envelope, not a sample. **A minor, not a patch**, because
|
|
8
|
+
the second of them puts a request shape on the wire that no earlier backend parses: a
|
|
16
9
|
`batch` is quarantined as `unknown_event` by any transform older than the one that
|
|
17
10
|
ships with it. The events an install emits, and the fields on them, are byte for
|
|
18
11
|
byte 0.6.0's.
|
|
19
12
|
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
- The README is rewritten for people using the package: what it does, install, defining
|
|
16
|
+
and registering tools, page-scoped tools, browser support, the optional tool-call
|
|
17
|
+
analytics, and exactly what the default-on usage telemetry sends, never sends, and how
|
|
18
|
+
to turn it off. The npm description now says what the package does.
|
|
19
|
+
|
|
20
20
|
### Telemetry beacons without a preflight
|
|
21
21
|
|
|
22
22
|
- The telemetry beacon is sent as `text/plain` instead of `application/json`. The
|
|
23
|
-
body is unchanged; the
|
|
24
|
-
the browser stops sending an `OPTIONS` preflight before every event.
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
body is unchanged; the content type makes an anonymous beacon a CORS simple
|
|
24
|
+
request, so the browser stops sending an `OPTIONS` preflight before every event.
|
|
25
|
+
The edge has mapped `text/plain` to the same template since ADR-0014, so no server
|
|
26
|
+
change is needed.
|
|
27
|
+
- A **keyed** beacon sends its publishable key as the `key` query parameter instead
|
|
28
|
+
of the `x-api-key` header, because the content type alone was not enough: a custom
|
|
29
|
+
request header is what makes a request non-simple, so a keyed beacon kept paying
|
|
30
|
+
the preflight — and every hosted-snippet install is keyed, since the manifest hands
|
|
31
|
+
it a default publishable key. The backend accepts both and the header still wins,
|
|
32
|
+
so older bundles are unaffected.
|
|
33
|
+
- **This reverses 0.2.0's "not `?api_key=`" for this channel only.** That decision is
|
|
34
|
+
unchanged for `/v1/collect`, which carries the *secret* key and whose authorizer
|
|
35
|
+
declares the header as its identity source. It does not carry to telemetry: the key
|
|
36
|
+
there is *publishable* and already sits in the page's HTML, the soft authorizer
|
|
37
|
+
declares no identity source at all, the stage keeps no access logs, and the WAF both
|
|
38
|
+
redacts the query string from its logs and now disables sampled requests, which
|
|
39
|
+
would otherwise have shown the URI in the console.
|
|
27
40
|
|
|
28
41
|
### One beacon per page load
|
|
29
42
|
|
package/README.md
CHANGED
|
@@ -1,32 +1,275 @@
|
|
|
1
1
|
# @nekuda/webmcp-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
3
|
+
Let AI agents use your website. This SDK turns the things your site already does,
|
|
4
|
+
such as searching products, adding to cart or booking a slot, into **tools** that a
|
|
5
|
+
browser agent can call through [WebMCP](https://webmachinelearning.github.io/webmcp/),
|
|
6
|
+
the draft web standard for exposing page actions to agents.
|
|
7
|
+
|
|
8
|
+
You describe each tool once. The SDK registers it with the browser, adds and removes it
|
|
9
|
+
as the user moves between pages, keeps working when the WebMCP draft changes, and does
|
|
10
|
+
nothing at all in browsers that don't support it yet.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { defineTool, registerTools } from "@nekuda/webmcp-sdk";
|
|
14
|
+
|
|
15
|
+
const addToCart = defineTool({
|
|
16
|
+
stableKey: "cart.add",
|
|
17
|
+
name: "add_to_cart",
|
|
18
|
+
description: "Add a product to the shopping cart by SKU.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
sku: { type: "string", description: "Product SKU" },
|
|
23
|
+
quantity: { type: "integer", minimum: 1, default: 1 },
|
|
24
|
+
},
|
|
25
|
+
required: ["sku"],
|
|
26
|
+
},
|
|
27
|
+
async execute({ sku, quantity = 1 }: { sku: string; quantity?: number }) {
|
|
28
|
+
const res = await fetch("/cart/add", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "content-type": "application/json" },
|
|
31
|
+
body: JSON.stringify({ sku, quantity }),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) throw new Error(`Could not add ${sku} to the cart`);
|
|
34
|
+
return await res.json();
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
registerTools([addToCart]);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
An agent visiting the page now sees an `add_to_cart` tool, with your description and
|
|
42
|
+
input schema, and can call it.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm install @nekuda/webmcp-sdk
|
|
48
|
+
# or: pnpm add @nekuda/webmcp-sdk · yarn add @nekuda/webmcp-sdk · bun add @nekuda/webmcp-sdk
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The package is a browser ES module with TypeScript types included. It has no runtime
|
|
52
|
+
dependencies. `@opentelemetry/api-logs` is an optional peer, needed only if you turn on
|
|
53
|
+
[OpenTelemetry output](#tool-call-analytics-optional).
|
|
54
|
+
|
|
55
|
+
**No bundler?** Serve `node_modules/@nekuda/webmcp-sdk/dist/index.js` from your site and
|
|
56
|
+
map the package name to it:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<script type="importmap">
|
|
60
|
+
{ "imports": { "@nekuda/webmcp-sdk": "/vendor/webmcp-sdk/index.js" } }
|
|
61
|
+
</script>
|
|
62
|
+
<script type="module" src="/webmcp/entry.js"></script>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Defining a tool
|
|
66
|
+
|
|
67
|
+
`defineTool` checks the definition straight away, freezes it and returns it. It does not
|
|
68
|
+
touch the browser, so a module that only defines tools is safe to import anywhere,
|
|
69
|
+
including tests and server-side code. An invalid definition throws a `TypeError` when the
|
|
70
|
+
module loads, not later when an agent calls the tool.
|
|
71
|
+
|
|
72
|
+
| Field | Required | What it is |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `stableKey` | yes | Your permanent ID for the tool, in `domain.action` form (`cart.add`, `catalog.search`): lowercase letters, digits and `_`, with at least one dot. Keep it the same when you rename the tool; analytics follow the tool through renames by this key. It is never sent to the browser API. |
|
|
75
|
+
| `name` | no | The name agents see: 1–128 characters of `A–Z a–z 0–9 _ - .`. Defaults to `stableKey`. |
|
|
76
|
+
| `description` | yes | What the tool does, written for the agent. Be specific about when to use it. |
|
|
77
|
+
| `inputSchema` | no | A JSON Schema object describing `execute`'s input. |
|
|
78
|
+
| `title` | no | A human-readable display name. |
|
|
79
|
+
| `annotations` | no | WebMCP hints, for example `{ readOnlyHint: true }` for a tool that only reads. |
|
|
80
|
+
| `pages` | no | Paths the tool is available on. Leave it out to make the tool available everywhere. See [Tools for specific pages](#tools-for-specific-pages). |
|
|
81
|
+
| `version` | no | Your version string for the tool (semver, a build hash, anything). |
|
|
82
|
+
| `intent` | no | `"answer"`, `"act"` or `"transact"`: whether the tool reads, changes something, or moves money. |
|
|
83
|
+
| `source` | no | `"scanner_generated"` or `"merchant_authored"`. |
|
|
84
|
+
| `execute` | yes | Your code. It receives the agent's input and may be `async`. |
|
|
85
|
+
|
|
86
|
+
**Return values.** Return any JSON value or a plain string and the SDK wraps it in the
|
|
87
|
+
result format agents expect. If you already build a WebMCP `{ content: [...] }` result,
|
|
88
|
+
it is passed through unchanged.
|
|
89
|
+
|
|
90
|
+
**Errors.** Throw when the action fails. The error goes back to the agent as it is, so
|
|
91
|
+
write the message for the agent ("No product with SKU 123") and not for your logs.
|
|
92
|
+
|
|
93
|
+
**TypeScript.** Declare the input as a `type`, not an `interface`:
|
|
94
|
+
`defineTool<{ sku: string }>(…)` works, but an `interface` fails the
|
|
95
|
+
`Record<string, unknown>` constraint.
|
|
96
|
+
|
|
97
|
+
## Registering tools
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const registration = registerTools([addToCart, searchProducts], options);
|
|
101
|
+
|
|
102
|
+
const results = await registration.ready;
|
|
103
|
+
// [{ stableKey: "cart.add", name: "add_to_cart", state: "registered" }, …]
|
|
104
|
+
|
|
105
|
+
registration.current(); // names registered right now, e.g. ["add_to_cart"]
|
|
106
|
+
registration.unregister(); // remove every tool in this call
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Each tool in `ready` ends in one of four states:
|
|
110
|
+
|
|
111
|
+
| State | Meaning |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `registered` | The browser accepted the tool. |
|
|
114
|
+
| `unsupported` | This browser has no WebMCP API. Nothing happened, and nothing needs to be done. |
|
|
115
|
+
| `aborted` | The registration was cancelled before the tool registered. |
|
|
116
|
+
| `failed` | The browser refused the tool. The reason is on `error`. |
|
|
117
|
+
|
|
118
|
+
`ready` never rejects, so you don't need a `try`/`catch` around it. `registerTools` itself
|
|
119
|
+
throws only for a programming error: two tools in one call with the same `name` or the
|
|
120
|
+
same `stableKey`.
|
|
121
|
+
|
|
122
|
+
| Option | What it does |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `signal` | An `AbortSignal`. Aborting it is the same as calling `unregister()`. |
|
|
125
|
+
| `onChange(names)` | Called with the current tool names whenever they change, for example after navigation. |
|
|
126
|
+
| `tracking` | Connects tool calls to your AgentLane account. See [Tool-call analytics](#tool-call-analytics-optional). |
|
|
127
|
+
| `telemetry` | Set to `false` to turn off anonymous usage telemetry for this call. See [Usage telemetry](#usage-telemetry). |
|
|
128
|
+
|
|
129
|
+
### Where to call it
|
|
130
|
+
|
|
131
|
+
Keep tool definitions in their own modules and call `registerTools` in one place that
|
|
132
|
+
owns the tools' lifetime.
|
|
133
|
+
|
|
134
|
+
**React (and Vite, Remix or any React SPA)**: register in an effect and unregister on
|
|
135
|
+
cleanup.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import { useEffect } from "react";
|
|
139
|
+
import { registerTools } from "@nekuda/webmcp-sdk";
|
|
140
|
+
import { addToCart } from "./tools/cart";
|
|
141
|
+
|
|
142
|
+
export function WebMCPTools() {
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const registration = registerTools([addToCart]);
|
|
145
|
+
return () => registration.unregister();
|
|
146
|
+
}, []);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Next.js App Router**: put the same component in a file marked `"use client"` and render
|
|
152
|
+
it from `app/layout.tsx`. Never call `registerTools` from a server component.
|
|
153
|
+
|
|
154
|
+
**Plain or server-rendered pages**: load one module with `<script type="module">` from
|
|
155
|
+
your shared layout.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { registerTools } from "@nekuda/webmcp-sdk";
|
|
159
|
+
import { addToCart } from "./tools/cart.js";
|
|
160
|
+
|
|
161
|
+
const registration = registerTools([addToCart]);
|
|
162
|
+
addEventListener("pagehide", () => registration.unregister(), { once: true });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Tools that need state**: register tools such as "update cart item" or "start checkout"
|
|
166
|
+
only while that state exists (for example, a non-empty cart or a signed-in user). Tie the
|
|
167
|
+
`registerTools` call to that state and unregister when it goes away.
|
|
168
|
+
|
|
169
|
+
## Tools for specific pages
|
|
170
|
+
|
|
171
|
+
Give a tool a `pages` list and it is only offered on matching pages:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
defineTool({
|
|
175
|
+
stableKey: "product.add_review",
|
|
176
|
+
description: "Post a review of the product on this page.",
|
|
177
|
+
pages: ["/products/*"],
|
|
178
|
+
execute: postReview,
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- `*` matches within one path segment (`/products/*`, `/product.html*`), and `**` matches
|
|
183
|
+
any number of segments (`/docs/**`).
|
|
184
|
+
- A tool with several patterns is available when any of them matches.
|
|
185
|
+
- Query strings and trailing slashes are ignored, and repeated slashes count as one.
|
|
186
|
+
- Hash routers work too: `/#/product/*`.
|
|
187
|
+
- During server-side rendering there is no location to check, so every tool counts as
|
|
188
|
+
eligible.
|
|
189
|
+
|
|
190
|
+
In a single-page app the SDK adds and removes page-scoped tools on every navigation
|
|
191
|
+
(`pushState`, `replaceState`, back/forward and hash changes), so you don't need to call
|
|
192
|
+
anything yourself. `ready` reports only the tools eligible on the first page; use
|
|
193
|
+
`onChange` or `current()` to follow later changes. If your app locks down the History
|
|
194
|
+
API, navigation is still detected through back/forward and hash changes.
|
|
195
|
+
|
|
196
|
+
## Browser support
|
|
197
|
+
|
|
198
|
+
The SDK works wherever the WebMCP API is present, as `document.modelContext` or
|
|
199
|
+
`navigator.modelContext`, whether a browser provides it natively or an extension does.
|
|
200
|
+
WebMCP is a draft that still changes month to month. The SDK tracks it so your code
|
|
201
|
+
doesn't have to, which is why you should call the SDK and not `modelContext` directly.
|
|
202
|
+
|
|
203
|
+
Where the API is missing, every tool reports `unsupported` and nothing else happens: no
|
|
204
|
+
errors and no changes to your page. You can ship the same code to every browser.
|
|
205
|
+
|
|
206
|
+
## Tool-call analytics (optional)
|
|
207
|
+
|
|
208
|
+
Connect your site in the [AgentLane dashboard](https://app.agentlane.com) to see which tools agents call, how
|
|
209
|
+
often they succeed and where they fail. Connecting means adding the site's publishable
|
|
210
|
+
key. Your tool definitions don't change.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
registerTools([addToCart], {
|
|
214
|
+
tracking: { apiKey: "wmk_…" }, // publishable key from the AgentLane dashboard
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
With a key, the SDK sends an event for each tool call to AgentLane. It is off unless you
|
|
219
|
+
set it. Two other settings are available:
|
|
220
|
+
|
|
221
|
+
- `otel: true` also, or instead, emits each tool-call event as an OpenTelemetry log record
|
|
222
|
+
through your app's global `LoggerProvider`, so you can send it to your own
|
|
223
|
+
observability stack. Your app owns the exporter, and `apiKey` isn't needed for this.
|
|
224
|
+
- `disabled: true` switches tool-call analytics off completely, for example until the
|
|
225
|
+
visitor accepts analytics cookies. While it is set, no identifier is created, nothing
|
|
226
|
+
is stored and nothing is sent.
|
|
227
|
+
|
|
228
|
+
Tool-call analytics keeps an anonymous visitor and session identifier in the browser's
|
|
229
|
+
storage, so ask for consent wherever your analytics consent rules require it.
|
|
230
|
+
|
|
231
|
+
## Usage telemetry
|
|
232
|
+
|
|
233
|
+
The SDK sends anonymous usage telemetry by default, so we can see which versions are in
|
|
234
|
+
use and catch breakage. It needs no key and **stores nothing in the browser** (no
|
|
235
|
+
cookies, no localStorage, no identifiers).
|
|
236
|
+
|
|
237
|
+
**Sent:** SDK version, whether WebMCP is available, browser family, device type, page
|
|
238
|
+
language and route pattern (`/products/:id`, never the URL); each tool's name, whether it
|
|
239
|
+
registered, and the shape of its schema; and per tool call, the outcome, duration, result
|
|
240
|
+
size and a scrubbed error template.
|
|
241
|
+
|
|
242
|
+
**Never sent:** tool inputs or results, raw error messages, URLs, query strings, referrer
|
|
243
|
+
URLs, page titles or the full user-agent.
|
|
244
|
+
|
|
245
|
+
To count daily visitors, our server stores a one-way hash of site, IP and user-agent under
|
|
246
|
+
a key that rotates every day. It can't be reversed or linked across sites or days. If the
|
|
247
|
+
page sets `tracking.apiKey`, telemetry is attributed to your site.
|
|
248
|
+
|
|
249
|
+
**Turn it off:**
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
globalThis.__WEBMCP_TELEMETRY__ = false; // whole page; set before the SDK loads
|
|
253
|
+
registerTools(tools, { telemetry: false }); // one call
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
It is also off automatically when the browser sends
|
|
257
|
+
[Global Privacy Control](https://globalprivacycontrol.org/) and during server-side
|
|
258
|
+
rendering. `tracking.disabled` does not affect it.
|
|
259
|
+
|
|
260
|
+
## Advanced
|
|
261
|
+
|
|
262
|
+
These exports are for code that hosts the SDK on a page it controls, such as a tag
|
|
263
|
+
loader. Most sites don't need them.
|
|
264
|
+
|
|
265
|
+
- `matchPage(patterns, location)` and `currentPageKey()`: the page matcher `pages` uses.
|
|
266
|
+
- `resolveModelContext()`: returns the WebMCP object the SDK would use, or `undefined`.
|
|
267
|
+
- `registerTools(tools, { modelContext })`: register against a specific WebMCP object.
|
|
268
|
+
- `tracking.sessionId` and `createCallTracker(tool, tracking)`: report tool calls under
|
|
269
|
+
your own session ID, including for tools you registered with the browser yourself.
|
|
270
|
+
- The telemetry event types (`TelemetryEvent`, `SdkInitEvent`, `ToolRegistrationEvent`,
|
|
271
|
+
`ToolCallEvent`, …) are exported for anyone consuming the beacons.
|
|
272
|
+
|
|
273
|
+
## Changelog
|
|
274
|
+
|
|
275
|
+
See `CHANGELOG.md`, which ships in the package.
|
package/dist/index.d.ts
CHANGED
|
@@ -56,8 +56,10 @@
|
|
|
56
56
|
* separate `/v1/telemetry` endpoint, fire-and-forget; the two page-level ones may
|
|
57
57
|
* share one request as a `batch` envelope, `tool_call` never does. Arrival order
|
|
58
58
|
* is NOT guaranteed, so consumers join on `sessionId` and never on ordering. A
|
|
59
|
-
* page that configures point 5's `apiKey` has it sent here as `
|
|
60
|
-
*
|
|
59
|
+
* page that configures point 5's `apiKey` has it sent here too, as the `key`
|
|
60
|
+
* query parameter rather than point 5's header (a custom header would make the
|
|
61
|
+
* beacon non-simple under CORS and cost it a preflight); it only adds tenant
|
|
62
|
+
* attribution — an unkeyed page sends anonymously to the
|
|
61
63
|
* same path. It composes with point 5: a call can emit to neither channel, either
|
|
62
64
|
* one, or both. Three page-level levers silence it entirely (no event, no
|
|
63
65
|
* network), because all three are read at emit time:
|
package/dist/index.js
CHANGED
|
@@ -181,6 +181,15 @@ function sendToCollect(event, config, scope = globalThis) {
|
|
|
181
181
|
}
|
|
182
182
|
var TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
183
183
|
var TELEMETRY_KEY_PARAM = "key";
|
|
184
|
+
function withTelemetryKey(base, apiKey) {
|
|
185
|
+
try {
|
|
186
|
+
const url = new URL(base);
|
|
187
|
+
url.searchParams.set(TELEMETRY_KEY_PARAM, apiKey);
|
|
188
|
+
return url.toString();
|
|
189
|
+
} catch {
|
|
190
|
+
return `${base}${base.includes("?") ? "&" : "?"}${TELEMETRY_KEY_PARAM}=${encodeURIComponent(apiKey)}`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
184
193
|
function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
185
194
|
try {
|
|
186
195
|
const json = JSON.stringify(event);
|
|
@@ -189,7 +198,7 @@ function sendTelemetry(event, scope = globalThis, endpoint, apiKey) {
|
|
|
189
198
|
const base = endpoint || DEFAULT_TELEMETRY_ENDPOINT;
|
|
190
199
|
const headers = { "content-type": TELEMETRY_CONTENT_TYPE };
|
|
191
200
|
const authenticated = typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
192
|
-
const url = authenticated ?
|
|
201
|
+
const url = authenticated ? withTelemetryKey(base, apiKey) : base;
|
|
193
202
|
tryFetch(scope, url, headers, json, authenticated ? (response) => {
|
|
194
203
|
if (isUnauthorized(response))
|
|
195
204
|
warnKeyRejected(scope);
|
|
@@ -1105,7 +1114,7 @@ function shapeMetrics(inputSchema) {
|
|
|
1105
1114
|
|
|
1106
1115
|
// src/telemetry.ts
|
|
1107
1116
|
var SDK_NAME = "@nekuda/webmcp-sdk";
|
|
1108
|
-
var SDK_VERSION = "0.7.0-dev.
|
|
1117
|
+
var SDK_VERSION = "0.7.0-dev.22.1";
|
|
1109
1118
|
var INSTALL_MODES = ["npm", "cdn_snippet"];
|
|
1110
1119
|
var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
|
|
1111
1120
|
function parseSampleRate(raw) {
|
package/dist/register.d.ts
CHANGED
|
@@ -33,8 +33,9 @@ export interface RegisterToolsOptions {
|
|
|
33
33
|
* event before its handler runs and a `tool_call_response` event after.
|
|
34
34
|
* `disabled: true` is this channel's consent gate; it does not narrow the
|
|
35
35
|
* default-on telemetry channel below, which resolves no identity and touches no
|
|
36
|
-
* storage. An `apiKey` set here
|
|
37
|
-
*
|
|
36
|
+
* storage. An `apiKey` set here also rides that channel's beacons — on the URL as
|
|
37
|
+
* `?key=`, not as this channel's header — which only attributes them to this
|
|
38
|
+
* tenant.
|
|
38
39
|
*/
|
|
39
40
|
tracking?: TrackingOptions;
|
|
40
41
|
/**
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Default-on usage telemetry for `@nekuda/webmcp-sdk`. Independent of the opt-in
|
|
3
3
|
* `tracking` channel (`src/tracking.ts`): it needs no `apiKey`, so it reports SDK
|
|
4
4
|
* adoption, browser mix, WebMCP availability, and tool-call reliability from every
|
|
5
|
-
* site the SDK runs on. A page that *does* configure one has it sent as
|
|
6
|
-
*
|
|
5
|
+
* site the SDK runs on. A page that *does* configure one has it sent as the `key`
|
|
6
|
+
* query parameter — never a header, which would cost every keyed beacon a CORS
|
|
7
|
+
* preflight — and it only adds tenant attribution. Opt out with `telemetry: false`
|
|
7
8
|
* on `registerTools`, with `globalThis.__WEBMCP_TELEMETRY__ = false`, or via
|
|
8
9
|
* Global Privacy Control.
|
|
9
10
|
*
|
|
@@ -388,7 +389,7 @@ export interface TelemetrySinks {
|
|
|
388
389
|
* navigation could then lose.
|
|
389
390
|
*
|
|
390
391
|
* The queue is keyed on the destination — endpoint plus key — because a batch is one
|
|
391
|
-
* request
|
|
392
|
+
* request under one key, and two tenants sharing an origin must never travel
|
|
392
393
|
* under each other's key (see {@link batchTelemetrySinks}). The init flush a batch
|
|
393
394
|
* defers (`deferInitEventForBatch`) uses that batch's sinks, so the common case lands
|
|
394
395
|
* in one queue. A `pagehide` or a hidden tab flushes everything at once, through the
|
package/dist/transport.d.ts
CHANGED
|
@@ -5,23 +5,28 @@
|
|
|
5
5
|
* v0.
|
|
6
6
|
*
|
|
7
7
|
* Transport is `fetch(..., { keepalive: true })` — unload-safe and, unlike
|
|
8
|
-
* `sendBeacon`, able to set custom headers.
|
|
9
|
-
* **header** (never a query param or body field): the deployed
|
|
10
|
-
* authorizer's identity source is that header, so a request without
|
|
11
|
-
* rejected 401 before the authorizer runs, and keeping the key out
|
|
12
|
-
* keeps it out of referrer logs and out of the canonical `payload`
|
|
13
|
-
* event JSON is the bare POST body; `org_id` is server-injected from
|
|
8
|
+
* `sendBeacon`, able to set custom headers. On **this** channel auth travels as
|
|
9
|
+
* the `x-api-key` **header** (never a query param or body field): the deployed
|
|
10
|
+
* API Gateway authorizer's identity source is that header, so a request without
|
|
11
|
+
* it is rejected 401 before the authorizer runs, and keeping the secret key out
|
|
12
|
+
* of the URL keeps it out of referrer logs and out of the canonical `payload`
|
|
13
|
+
* column. The event JSON is the bare POST body; `org_id` is server-injected from
|
|
14
|
+
* the key.
|
|
14
15
|
*
|
|
15
16
|
* The scope is injectable for tests (spec.ts pattern). Every browser global is
|
|
16
17
|
* guarded and the whole function is wrapped so telemetry never throws into the
|
|
17
18
|
* caller.
|
|
18
19
|
*
|
|
19
20
|
* `sendTelemetry` is the sibling sender for the default-on usage-telemetry
|
|
20
|
-
* channel (`src/telemetry.ts`): same keepalive/fire-and-forget discipline
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* channel (`src/telemetry.ts`): same keepalive/fire-and-forget discipline, on a
|
|
22
|
+
* distinct `/v1/telemetry` path that accepts the request with or without a key.
|
|
23
|
+
* It does **not** share the header above — it puts the *publishable* key on the
|
|
24
|
+
* URL as `?key=`, because a custom header would cost that channel a CORS
|
|
25
|
+
* preflight it exists to avoid; see {@link sendTelemetry} for the whole reason
|
|
26
|
+
* and for why the exposure that argues against a URL here does not argue against
|
|
27
|
+
* it there. That channel runs on pages that never key the authenticated one, so a
|
|
28
|
+
* keyless send is the normal case, not a failure — the backend attributes those
|
|
29
|
+
* by CORS `Origin` instead.
|
|
25
30
|
*
|
|
26
31
|
* That path has exactly one observable failure signal, and it is a `console.error`,
|
|
27
32
|
* never a throw or a retry: a *keyed* beacon whose key the backend cannot resolve
|
|
@@ -40,7 +45,7 @@ export declare const DEFAULT_COLLECT_ENDPOINT: string;
|
|
|
40
45
|
* Default ingest endpoint for the default-on usage-telemetry channel — a
|
|
41
46
|
* **separate path** from {@link DEFAULT_COLLECT_ENDPOINT} so the backend can route
|
|
42
47
|
* the two channels independently: one authorizer-protected, this one unauthenticated
|
|
43
|
-
* by default and accepting the same request with an optional
|
|
48
|
+
* by default and accepting the same request with an optional `?key=` for tenant
|
|
44
49
|
* attribution. Same per-flavor base host (see {@link INGEST_BASE}).
|
|
45
50
|
*/
|
|
46
51
|
export declare const DEFAULT_TELEMETRY_ENDPOINT: string;
|
|
@@ -62,7 +67,7 @@ export declare const TELEMETRY_CONTENT_TYPE = "text/plain";
|
|
|
62
67
|
export declare const TELEMETRY_KEY_PARAM = "key";
|
|
63
68
|
/**
|
|
64
69
|
* POST a usage-telemetry event to the telemetry endpoint via `fetch(keepalive)`.
|
|
65
|
-
* The path is the same whether or not `apiKey` is given: the
|
|
70
|
+
* The path is the same whether or not `apiKey` is given: the key only *adds*
|
|
66
71
|
* tenant attribution to an event the backend would accept anyway, so a page that
|
|
67
72
|
* never configures the authenticated channel keeps sending, and the backend falls
|
|
68
73
|
* back to the CORS `Origin` header. Never throws: any failure is swallowed and a
|
|
@@ -71,8 +76,8 @@ export declare const TELEMETRY_KEY_PARAM = "key";
|
|
|
71
76
|
* `apiKey` is `unknown` because it originates in caller-supplied options that no
|
|
72
77
|
* compiler checked; anything that is not a non-blank string sends unauthenticated
|
|
73
78
|
* rather than putting `"undefined"` (or a hostile object's `toString`) on the
|
|
74
|
-
* wire.
|
|
75
|
-
*
|
|
79
|
+
* wire. A keyed beacon differs from an anonymous one only in its URL: the request
|
|
80
|
+
* stays CORS-simple either way, so neither costs a preflight.
|
|
76
81
|
*
|
|
77
82
|
* The body is sent as `text/plain`, not `application/json`, and that is a cost
|
|
78
83
|
* decision, not a formatting one: `text/plain` is a CORS-safelisted content type,
|
|
@@ -81,7 +86,7 @@ export declare const TELEMETRY_KEY_PARAM = "key";
|
|
|
81
86
|
* per event at the edge; at 2026-09 volume that preflight was roughly half of the
|
|
82
87
|
* seven million requests a day the collect API billed. The edge maps `text/plain`
|
|
83
88
|
* to the same template as JSON (ingestion-edge/telemetry.tf), so nothing changes
|
|
84
|
-
* on the wire but the
|
|
89
|
+
* on the wire but the content type.
|
|
85
90
|
*
|
|
86
91
|
* The publishable key travels as the `key` query parameter, not as `x-api-key`. A
|
|
87
92
|
* custom header is what makes a request non-simple, so a keyed beacon with the header
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nekuda/webmcp-sdk",
|
|
3
|
-
"version": "0.7.0-dev.
|
|
3
|
+
"version": "0.7.0-dev.22.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Let AI agents use your website: define tools once and register them through WebMCP, with page-scoped tools, SPA navigation and unsupported browsers handled for you.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|