---
title: "@nekuda/webmcp-sdk"
description: "The small browser SDK that generated WebMCP tools are built on: defineTool, registerTools, and the rules that keep tools stable."
sidebar:
  label: SDK
---


The generated tools are built on a small SDK rather than the raw browser API. The reason is simple: WebMCP is a draft standard that still changes month to month, and its surface moves (`document.modelContext` today, `navigator.modelContext` in older Chrome). The SDK pins one spec version, resolves whichever surface the browser exposes, and quietly does nothing in browsers with neither — so your site never breaks for regular visitors.

The package is published on npm as [`@nekuda/webmcp-sdk`](https://www.npmjs.com/package/@nekuda/webmcp-sdk).

:::note[You usually don't install this by hand]
The implement skill adds and wires the SDK for you. This page is for reading the generated code — and for writing tools yourself if you want to.
:::

## The two-module shape

Generated code always splits into two kinds of files:

- **Tool modules** define tools and export them. No side effects — importing one touches nothing.
- **One entry module** per scope calls `registerTools` and owns the tools' lifetime.

This split is what makes "connect later" cheap: adding an API key or telemetry config later touches the entry module only. Tool code never changes.

```ts
// webmcp/cart.ts — a tool module
import { defineTool } from "@nekuda/webmcp-sdk";

export const addToCart = defineTool({
  stableKey: "cart.add",            // durable identity — authored once, never changed
  name: "add_to_cart",              // wire name agents see (defaults to stableKey)
  title: "Add to cart",
  description: "Add a product to the shopping cart by SKU.",
  inputSchema: {
    type: "object",
    properties: {
      sku: { type: "string", description: "Product SKU" },
      quantity: { type: "integer", minimum: 1, default: 1 },
    },
    required: ["sku"],
    additionalProperties: false,
  },
  annotations: { readOnlyHint: false },
  async execute({ sku, quantity }: { sku: string; quantity?: number }) {
    const res = await fetch("/cart/add", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ sku, quantity: quantity ?? 1 }),
    });
    if (!res.ok) throw new Error(`add to cart failed: HTTP ${res.status}`);
    return await res.json();
  },
});
```

```ts
// webmcp/register.ts — the entry module
import { registerTools } from "@nekuda/webmcp-sdk";
import { addToCart } from "./cart";

export const registration = registerTools([addToCart]);
```

In a React app, tie the lifetime to the component instead:

```tsx
useEffect(() => {
  const reg = registerTools([addToCart]);
  return () => reg.unregister();
}, []);
```

## defineTool

Validates eagerly, freezes, and returns the tool. No side effects — an invalid definition throws a `TypeError` at module load, not at call time.

| Field | Required | What it is |
| --- | --- | --- |
| `stableKey` | yes | Durable identity, dot-namespaced `domain.action` (e.g. `cart.add`). Written once, survives renames and re-runs. Never sent to the browser. |
| `description` | yes | What the tool does, when to use it, what it returns. This is what agents read — it's the product. |
| `name` | no | Wire name agents call, 1–128 chars of `[A-Za-z0-9_.-]`. Defaults to `stableKey`. Safe to change anytime. |
| `title` | no | Human-readable display name. |
| `inputSchema` | no | Plain JSON Schema object. Set `additionalProperties: false`. |
| `annotations` | no | Hints like `{ readOnlyHint: true }`. |
| `execute` | yes | The page-owned logic. **Throw on failure** — never succeed silently on missing data. Return any JSON value; the SDK normalizes it. |

:::warning[Type the input with `type`, not `interface`]
`execute`'s input type must be a `type` alias. An `interface` has no implicit index signature and fails the SDK's generic constraint.
:::

## registerTools

```ts
const reg = registerTools(tools, {
  signal,             // optional AbortSignal — abort means unregister
  telemetry: false,   // optional — see Telemetry below
  // tracking: {...}  // optional, opt-in per-call analytics
});
```

Returns `{ ready, unregister, signal }`. `ready` resolves to one result per tool and **never rejects**:

| State | Meaning |
| --- | --- |
| `registered` | Live on the page's WebMCP surface. |
| `unsupported` | Browser has no WebMCP surface — graceful no-op, site unaffected. |
| `aborted` | The signal aborted before registration. |
| `failed` | Registration failed — e.g. a duplicate `name` on the page. |

Two rules to know:

- One batch per scope. A duplicate `name` or `stableKey` *within* a batch throws and fails the whole call.
- Unregistration happens only via `unregister()` or the abort signal — never spontaneously.

## Telemetry

The SDK emits limited usage telemetry by default — SDK adoption, browser support mix, and tool-call reliability. It does not use a persistent visitor identifier or include raw tool inputs and results. Opting out is one flag: `registerTools(tools, { telemetry: false })`; for a guaranteed page-wide opt-out, set `globalThis.__WEBMCP_TELEMETRY__ = false` before the SDK loads. It also stays silent automatically when the browser sends [Global Privacy Control](https://globalprivacycontrol.org/), and during SSR. The separate `tracking` option can include tool inputs and results but is entirely opt-in and off unless configured. See the [Privacy Policy](/privacy) for the collected data and controls.
