# webmcp-kit

[![npm version](https://img.shields.io/npm/v/webmcp-kit)](https://www.npmjs.com/package/webmcp-kit)
[![npm downloads](https://img.shields.io/npm/dm/webmcp-kit)](https://www.npmjs.com/package/webmcp-kit)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/victorhuangwq/webmcp-kit/blob/main/LICENSE)
[![types: TypeScript](https://img.shields.io/badge/types-TypeScript-3178C6.svg)](https://www.typescriptlang.org/)

Type-safe WebMCP tools with Zod.

## What is WebMCP?

[WebMCP](https://webmachinelearning.github.io/webmcp/) is a browser API that lets websites expose tools to AI agents, developed under the WebML working group.

The API adds `navigator.modelContext`, which websites use to register tools that agents can discover and call. Think of it like making your site's functionality available to AI assistants.

The current spec exposes a single method — `navigator.modelContext.registerTool(tool, { signal })` — and tools are unregistered by aborting the `AbortSignal` you passed at registration time. webmcp-kit handles that controller lifecycle for you.

Major browsers are starting to experiment with implementations.
- Chrome:
  - [WebMCP is available for early preview](https://developer.chrome.com/blog/webmcp-epp)
  - [WebMCP early preview](https://docs.google.com/document/d/1rtU1fRPS0bMqd9abMG_hc6K9OAI6soUy3Kh00toAgyk/edit?tab=t.0)
  - TLDR: Download Chrome Canary (146+), go to `about:flags` and set ` WebMCP for testing` to enabled

## What does webmcp-kit do?

webmcp-kit wraps the raw WebMCP API to make building tools easier:

- **Zod schemas**: Define inputs once, get JSON Schema conversion and TypeScript inference
- **Built-in validation**: Inputs are validated against your schema before `execute` runs
- **Less boilerplate**: Feature detection, response formatting, and registration handled for you
- **Automatic feature detection**: Works when the API exists, falls back gracefully when it doesn't
- **Dev panel**: Test and debug tools in the browser without needing a real agent

```typescript
import { defineTool } from 'webmcp-kit';
import { z } from 'zod';

const addToCart = defineTool({
  name: 'addToCart',
  description: 'Add a product to cart',
  inputSchema: z.object({
    productId: z.string(),
    quantity: z.number().min(1),
  }),
  execute: async ({ productId, quantity }) => {
    // productId and quantity are typed
    return `Added ${quantity}x ${productId}`;
  },
});

addToCart.register();
```

## Install

```bash
npm install webmcp-kit zod
```

Requires Zod v4.

## Install and Use the `add-webmcp-tools` Skill

Install the skill from this repository using Vercel's Skills CLI:

```bash
npx skills add victorhuangwq/webmcp-kit
```

Then invoke it with requests like:
- "add a webmcp tool for search"
- "debug why my tool is not in dev panel"
- "update tool schema and validation"

References:
- Skill definition: [`skills/add-webmcp-tools/SKILL.md`](./skills/add-webmcp-tools/SKILL.md)
- Working demos:
  - [`examples/pizza-shop/README.md`](./examples/pizza-shop/README.md)
  - [`examples/flight-booking/README.md`](./examples/flight-booking/README.md)

## Usage

### Define a Tool

```typescript
import { defineTool } from 'webmcp-kit';
import { z } from 'zod';

const searchProducts = defineTool({
  name: 'searchProducts',
  title: 'Search Products', // optional human-friendly label
  description: 'Search the product catalog',
  inputSchema: z.object({
    query: z.string().describe('Search query'),
    limit: z.number().optional().default(10),
  }),
  execute: async ({ query, limit }) => {
    const results = await db.products.search(query, limit);
    return JSON.stringify(results);
  },
});

searchProducts.register();
```

The schema is converted to JSON Schema for the WebMCP API. Your `execute` function receives typed inputs. Inputs that fail Zod validation never reach `execute` — the kit returns a validation error response instead. Exceptions thrown inside `execute` are caught and surfaced as error responses (`isError: true`), so a single tool blowing up won't crash the page.

### Annotations

Use `annotations` to hint at tool behavior, matching the spec's `ToolAnnotations`:

```typescript
defineTool({
  name: 'searchProducts',
  description: 'Search the product catalog',
  inputSchema: z.object({ query: z.string() }),
  annotations: {
    readOnlyHint: true,         // tool does not modify state
    untrustedContentHint: true, // surfaces third-party content the model should treat as untrusted
  },
  execute: async ({ query }) => { /* ... */ },
});
```

Only set `readOnlyHint: true` for tools that genuinely make no state changes. For sensitive mutations, prompt for confirmation inline via `client.requestUserInteraction(...)` (see below).

### Testing tools

Call a tool directly without registering it. Input is validated against the Zod schema, and you always get back a `ToolResponse`:

```typescript
const response = await searchProducts.execute({ query: 'shoes', limit: 5 });
// response: { content: [...], isError?: boolean }
```

Between tests, use `unregisterAll()` to tear down kit-managed registrations and `resetMockModelContext()` (from `webmcp-kit`) to reset the in-process mock registry.

### Response Helpers

```typescript
import { textContent, jsonContent, errorContent } from 'webmcp-kit';

// String responses are auto-wrapped, but you can be explicit:
return textContent('Done');
return jsonContent({ status: 'ok' });
return errorContent('Something went wrong');
```

### Dev Panel

Test tools without a real agent:

```typescript
import { enableDevMode } from 'webmcp-kit/devtools';

enableDevMode();
// or with a custom position:
enableDevMode({ position: { bottom: 24, right: 24 } });
```

This injects a panel that lists your tools, generates input forms from schemas, and lets you execute them. If the native `navigator.modelContextTesting` API is available (Chrome 146+ EPP), the panel uses it; otherwise it falls back to the kit's mock registry.

For more control, `injectDevPanel(options?)` and `removeDevPanel()` are also exported from `webmcp-kit/devtools`.

### Testing tools directly

`tool.execute(input, client?)` runs the same validation + execute pipeline used by the WebMCP API, so unit tests don't need to register anything:

```typescript
import { createMockClient } from 'webmcp-kit';

const result = await checkout.execute(
  { cartId: 'abc' },
  createMockClient({
    onUserInteraction: async () => ({ confirmed: true }),
  })
);

expect(result.isError).toBeUndefined();
```

The `client` argument is optional — omit it and a default mock client is used.

## API

### `defineTool(options)`

```typescript
const tool = defineTool({
  name: string,
  title?: string,
  description: string,
  inputSchema: ZodSchema,
  execute: (input, client) => Promise<string | ToolResponse>,
  annotations?: ToolAnnotations,
});

tool.register();     // Add to navigator.modelContext (mints an AbortController internally)
tool.unregister();   // Aborts the signal — removes the tool
tool.execute(input); // Validates input, returns Promise<ToolResponse> — useful in unit tests
tool.schema;         // The original Zod schema (readonly)
```

`execute` can return a `string` (auto-wrapped via `textContent`) or a full `ToolResponse` when you need multi-content output or `isError: true`.

`annotations` accepts two hints from the WebMCP spec:
- `readOnlyHint` — the tool does not modify external state.
- `untrustedContentHint` — the tool may surface untrusted third-party content to the model.

Internally, `tool.register()` calls `navigator.modelContext.registerTool(tool, { signal })` with a kit-owned `AbortController`. `tool.unregister()` aborts it. You don't need to manage controllers yourself.

### `unregisterAll()` / `getRegisteredTools()`

The kit tracks every tool you register via `defineTool().register()` in a process-wide set. This is handy for HMR, SPA route teardown, and test cleanup:

```typescript
import { unregisterAll, getRegisteredTools } from 'webmcp-kit';

// e.g. on Vite HMR dispose
if (import.meta.hot) {
  import.meta.hot.dispose(() => unregisterAll());
}

console.log(getRegisteredTools().map((t) => t.name));
```

### `enableDevMode()`

```typescript
import { enableDevMode } from 'webmcp-kit/devtools';

enableDevMode();
// or, with custom positioning:
enableDevMode({ position: { bottom: 16, right: 16 } });
```

`webmcp-kit/devtools` also exports `injectDevPanel(options?)` and `removeDevPanel()` if you want explicit control over when the panel mounts and unmounts. `injectDevPanel` returns a cleanup function.

## How It Works

`defineTool()` creates a tool object. When you call `.register()`:

1. It mints a fresh `AbortController` for this registration.
2. It checks `isWebMCPSupported()` (i.e. whether `navigator.modelContext` exists).
3. If yes, it calls `navigator.modelContext.registerTool(tool, { signal })` with that controller's signal.
4. If no, it registers with an internal mock so the dev panel still works.
5. It adds the tool to the kit's process-wide registration tracker (used by `unregisterAll()` / `getRegisteredTools()`).

`.unregister()` simply aborts the controller — both the native API and the mock listen for that signal, so the tool disappears from wherever it was registered. The tracker untracks itself via the abort listener, so it stays in sync even if the signal fires from elsewhere.

Your code doesn't change based on environment. When browsers ship WebMCP support, the same code will use the real API. `isWebMCPSupported()` is also exported if you want to gate behavior on the native API yourself.

If you need to gate UI on capability, `isWebMCPSupported()` and `isBrowser()` are exported from `webmcp-kit`.

## Examples

- [`examples/pizza-shop`](./examples/pizza-shop): pizza ordering flow with multiple tools (`getMenu`, `addToCart`, `getCart`, `clearCart`, `checkout`).
- [`examples/flight-booking`](./examples/flight-booking): multi-step flight purchase flow (`searchFlights`, `selectFlight`, `addTraveler`, `addExtras`, `reviewBooking`, `purchaseFlight`).

Both examples include setup instructions for testing with native WebMCP and mock mode.

## Support

If you find webmcp-kit useful:

- **Star the repo**: It helps others discover the project
- **Report issues**: Found a bug or have a feature request? [Open an issue](https://github.com/victorhuangwq/webmcp-kit/issues)

## License

MIT
