Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ Agents with built-in WebMCP browser support, such as Codex, discover and call to

The extension will be deprecated when Chrome provides native bridging to desktop clients.

### Testing in Chrome

Google's [WebMCP extension](https://github.com/GoogleChromeLabs/webmcp-extension) lists a page's tools and runs an agent against them from a side panel, which is handy while building tools. It only sees tools registered through Chrome's native API, so on a page running the polyfill it finds nothing. Turn on `chrome://flags/#enable-webmcp-testing` (the extension needs Chrome 155 or newer) and `<WebMCPProvider>` will use the native API instead of the polyfill.

## Usage

### Execution state
Expand Down Expand Up @@ -188,6 +192,8 @@ useMcpTool({
});
```

`onError` fires when the handler throws. A handler that returns `{ isError: true }` on its own still counts as a success, so `onSuccess` fires for it.

### JSON Schema

Pass `inputSchema` directly to define a tool without Zod:
Expand Down
4 changes: 4 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ These are the only annotation fields supported. The classic MCP hints (`destruct

`exposedTo?: string[]` lets you control cross-frame origin visibility. Each entry must be a parseable, potentially-trustworthy origin (e.g. `https://example.com`). Changing `exposedTo` re-registers the tool. Invalid origins cause registration to reject (see below).

WebMCP is gated by the `tools` Permissions Policy feature, which defaults to `'self'`. A cross-origin iframe can't use `document.modelContext` at all unless the embedding page delegates it with `<iframe allow="tools">`.

### Return value

```tsx
Expand All @@ -85,6 +87,8 @@ const { state, execute, reset } = useMcpTool({ ... });

`execute()` (the UI/direct path) throws if validation or handler logic fails. The agent/testing-shim path returns a `CallToolResult` with `isError: true` instead. Both paths update the same reactive state and fire the same `onSuccess`/`onError` callbacks.

A handler that returns `isError: true` itself is a success as far as the hook is concerned: `state.lastResult` holds the result and `onSuccess` fires. Throw instead if you want `state.error` and `onError`.

### Cancellation

Each execution gets its own `AbortSignal`, passed to the handler as `ctx.signal`. On
Expand Down
38 changes: 33 additions & 5 deletions skills/webmcp-add-tool/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ return {
structuredContent: { key: "value", count: 42 },
};

// Error result (caught by the framework, but you can also return errors explicitly)
// Error result built by hand (usually you just throw, see Errors below)
return {
content: [{ type: "text", text: "Something went wrong" }],
isError: true,
Expand All @@ -140,6 +140,26 @@ return {
};
```

Keep results small. The agent reads all of it, `structuredContent` and base64 images included. Google's WebMCP guidance aims for about 1,500 characters. For long lists, take `page` and `page_size` inputs and return `total_count` instead of everything at once.

If the tool changes what's on screen (navigates, opens a dialog, updates a list), wait for that update before returning so the agent sees the page in its new state.

## Errors

Throw when the tool can't do what was asked. The hook catches it, sets `state.error`, calls `onError`, and the agent gets an `isError` result with your message. Write the message for the agent and say what to do next:

```tsx
handler: async ({ code }) => {
const coupon = await findCoupon(code);
if (!coupon) {
throw new Error(`Coupon "${code}" is invalid or expired. Ask the user for another code.`);
}
// ...
},
```

Returning `{ isError: true, ... }` yourself also reaches the agent as an error, but the hook treats it as a success: `state.lastResult` holds it and `onSuccess` fires, not `onError`. Use that only when you want to control the exact result, e.g. to add `structuredContent` like `{ retryable: true }`.

## Dynamic tools

Tools register on mount and unregister on unmount. Use conditional rendering to dynamically control which tools are available:
Expand Down Expand Up @@ -194,20 +214,28 @@ function ProductPage({ product }) {
}
```

## Naming conventions
## Names, descriptions and inputs

- Tool names: `snake_case` (e.g., `search_catalog`, `delete_user`, `get_order_status`)
- Tool names: `snake_case` (e.g., `search_catalog`, `delete_user`, `get_order_status`), ideally under 30 characters
- Component names: PascalCase ending in `Tool` (e.g., `SearchCatalogTool`, `DeleteUserTool`)
- Descriptions: start with a verb, be specific (e.g., "Search the product catalog by keyword" not "Search stuff")
- Name the tool for what actually happens. `book_flight` books the flight. A tool that only opens the booking form is `start_booking`, and it isn't read-only because it changes the page.
- One tool per capability. If two tools could both answer the same request, merge them or make the difference obvious.
- Descriptions say what the tool does and when to pick it: "Search the product catalog by keyword or category. Use when the user wants to find or compare products." Not "Search stuff".
- Don't list the inputs in the description, the schema already tells the agent. Leave out implementation details too (Redux, REST, cache invalidation).
- Keep descriptions under about 500 characters and each `.describe()` under about 150.
- Accept input the way the user says it. For dates, take "next Friday" as well as an ISO date and resolve it in the app; agents often get today's date or the timezone wrong.
- Prefer names over internal IDs (`shipping: "express"`, not `shipping_id: 1`). IDs the agent got from another tool's result are fine.

## Checklist

Before finishing, verify:

- [ ] Tool name is unique across the app
- [ ] Description clearly explains what the tool does (AI agents read this)
- [ ] Description says what the tool does and when to use it (AI agents read this)
- [ ] All input fields have `.describe()` for AI context
- [ ] Handler returns `CallToolResult` with `content` array
- [ ] Failures throw an error message that tells the agent what to do next
- [ ] Results stay small, and long lists are paginated
- [ ] Appropriate annotations are set
- [ ] Component is rendered inside `<WebMCPProvider>`
- [ ] For Next.js: file has `"use client"` directive
Loading