@nekuda/webmcp-sdk 0.7.0-dev.21.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 CHANGED
@@ -10,6 +10,13 @@ the second of them puts a request shape on the wire that no earlier backend pars
10
10
  ships with it. The events an install emits, and the fields on them, are byte for
11
11
  byte 0.6.0's.
12
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
+
13
20
  ### Telemetry beacons without a preflight
14
21
 
15
22
  - The telemetry beacon is sent as `text/plain` instead of `application/json`. The
package/README.md CHANGED
@@ -1,32 +1,275 @@
1
1
  # @nekuda/webmcp-sdk
2
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.
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.js CHANGED
@@ -1114,7 +1114,7 @@ function shapeMetrics(inputSchema) {
1114
1114
 
1115
1115
  // src/telemetry.ts
1116
1116
  var SDK_NAME = "@nekuda/webmcp-sdk";
1117
- var SDK_VERSION = "0.7.0-dev.21.1";
1117
+ var SDK_VERSION = "0.7.0-dev.22.1";
1118
1118
  var INSTALL_MODES = ["npm", "cdn_snippet"];
1119
1119
  var SDK_INSTALL_MODE = INSTALL_MODES.find((mode) => mode === (typeof __WEBMCP_INSTALL_MODE__ === "string" ? __WEBMCP_INSTALL_MODE__ : "")) ?? "npm";
1120
1120
  function parseSampleRate(raw) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nekuda/webmcp-sdk",
3
- "version": "0.7.0-dev.21.1",
3
+ "version": "0.7.0-dev.22.1",
4
4
  "type": "module",
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).",
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": {