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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to `webmcp-react` are documented here. The format is based o
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- **`consequentialHint` and `debugging` annotations.** `ToolAnnotations` now covers all four
hints in the WebMCP spec. Passing either one to `useMcpTool` used to fail to type-check.
The polyfill already forwarded annotations as-is, so nothing changes at runtime.

## 1.2.1

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ useMcpTool({

### Title and annotations

Give a tool a human-friendly display `title`, and describe its behavior to agents with `annotations`. The current WebMCP specification defines two annotations: `readOnlyHint` and `untrustedContentHint`.
Give a tool a human-friendly display `title`, and describe its behavior to agents with `annotations`. The current WebMCP specification defines four annotations: `readOnlyHint`, `untrustedContentHint`, `consequentialHint` and `debugging`. Set `consequentialHint` on tools that do something hard to undo, like placing an order, so agents know to check with the user first.

```tsx
useMcpTool({
Expand Down
8 changes: 5 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Registers a tool on `document.modelContext`. Automatically unregisters on unmoun
| `description` | `string` | Human-readable description (required, non-empty) |
| `input` | `z.ZodObject` | Zod schema for inputs. Handler receives typed args |
| `output` | `z.ZodObject` | Optional Zod schema for outputs (library extension; see below) |
| `annotations` | `ToolAnnotations` | Optional behavior hints (`readOnlyHint`, `untrustedContentHint`) |
| `annotations` | `ToolAnnotations` | Optional behavior hints (`readOnlyHint`, `untrustedContentHint`, `consequentialHint`, `debugging`) |
| `exposedTo` | `string[]` | Optional list of trustworthy origins this tool is exposed to across frames |
| `handler` | `(args, ctx) => CallToolResult \| Promise<CallToolResult>` | Tool implementation. Receives the parsed input and `ctx: { signal: AbortSignal }`; the signal aborts when the agent cancels the execution (Chrome 153+; otherwise a never-aborting substitute) |
| `onSuccess` | `(result) => void` | Optional callback on success |
Expand All @@ -57,8 +57,10 @@ Same as above, but replace `input` with `inputSchema: InputSchema` and (optional

```ts
interface ToolAnnotations {
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
readOnlyHint?: boolean; // only reads data, no side effects
untrustedContentHint?: boolean; // output may include content you don't control
consequentialHint?: boolean; // real-world or irreversible effects; agents may confirm with the user first
debugging?: boolean; // meant for debugging and developer tooling, not end users
}
```

Expand Down
6 changes: 5 additions & 1 deletion skills/webmcp-add-tool/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ useMcpTool({
annotations: {
readOnlyHint: true, // Tool only reads data, no side effects
untrustedContentHint: true, // Tool may return content from untrusted sources
consequentialHint: true, // Tool has real-world or irreversible effects
debugging: true, // Tool is for developers, not end users
},
});
```

`ToolAnnotations` is only `{ readOnlyHint?: boolean; untrustedContentHint?: boolean }`. There is no `title` inside `annotations` (it is a top-level field), and the older `destructiveHint`, `idempotentHint`, and `openWorldHint` hints no longer exist.
`ToolAnnotations` is only `{ readOnlyHint?, untrustedContentHint?, consequentialHint?, debugging? }`, all booleans. There is no `title` inside `annotations` (it is a top-level field), and the older `destructiveHint`, `idempotentHint`, and `openWorldHint` hints no longer exist.

**Guideline for choosing fields:**

Expand All @@ -111,6 +113,8 @@ useMcpTool({
| Fetches/queries data, no side effects | `annotations: { readOnlyHint: true }` |
| Returns content from untrusted sources (user input, external APIs) | `annotations: { untrustedContentHint: true }` |
| Creates/updates/deletes data | omit `readOnlyHint` (a tool with side effects should not claim it is read-only) |
| Does something hard to undo (payments, bookings, sending messages, permanent deletes) | `annotations: { consequentialHint: true }` |
| Only useful to developers (state dumps, test helpers) | `annotations: { debugging: true }` |

## Return format

Expand Down
29 changes: 29 additions & 0 deletions src/__tests__/tool-annotations.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expectTypeOf, it } from "vitest";
import type { McpToolConfigJsonSchema, RegisteredTool, ToolAnnotations } from "../index";

describe("ToolAnnotations", () => {
it("has every annotation the spec defines", () => {
expectTypeOf<ToolAnnotations>().toEqualTypeOf<{
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
consequentialHint?: boolean;
debugging?: boolean;
}>();
});

it("accepts consequentialHint and debugging in useMcpTool config", () => {
const config: McpToolConfigJsonSchema = {
name: "book_flight",
description: "Book the selected flight",
annotations: { consequentialHint: true, debugging: false },
handler: () => ({ content: [] }),
};
expectTypeOf(config.annotations).toEqualTypeOf<ToolAnnotations | undefined>();
});

it("types them on tools returned by getTools()", () => {
type Annotations = NonNullable<RegisteredTool["annotations"]>;
expectTypeOf<Annotations["consequentialHint"]>().toEqualTypeOf<boolean | undefined>();
expectTypeOf<Annotations["debugging"]>().toEqualTypeOf<boolean | undefined>();
});
});
14 changes: 12 additions & 2 deletions src/hooks/__tests__/useMcpTool.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,12 @@ describe("registration lifecycle", () => {
name: "greet",
title: "Greeter",
description,
annotations: { readOnlyHint: true, untrustedContentHint: true },
annotations: {
readOnlyHint: true,
untrustedContentHint: true,
consequentialHint: true,
debugging: true,
},
handler: () => ({ content: [{ type: "text", text: "hi" }] }),
});
return null;
Expand All @@ -263,7 +268,12 @@ describe("registration lifecycle", () => {
await waitFor(() => expect(spy).toHaveBeenCalled());
const descriptor = spy.mock.calls[0][0];
expect(descriptor.title).toBe("Greeter");
expect(descriptor.annotations).toEqual({ readOnlyHint: true, untrustedContentHint: true });
expect(descriptor.annotations).toEqual({
readOnlyHint: true,
untrustedContentHint: true,
consequentialHint: true,
debugging: true,
});
});

it("removes tool on unmount", async () => {
Expand Down
9 changes: 9 additions & 0 deletions src/polyfill/__tests__/consumer-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ describe("document.modelContext.getTools (polyfill)", () => {
expect(tool.annotations).toEqual({ readOnlyHint: true });
});

it("passes consequentialHint and debugging through to getTools()", async () => {
installPolyfill();
await mc().registerTool(
makeTool({ annotations: { consequentialHint: true, debugging: true } }),
);
const [tool] = await mc().getTools();
expect(tool.annotations).toEqual({ consequentialHint: true, debugging: true });
});

it("rejects SecurityError for an untrustworthy fromOrigins entry", async () => {
installPolyfill();
await expect(mc().getTools({ fromOrigins: ["http://evil.example"] })).rejects.toThrow(
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,17 @@ export interface CallToolResult {
}

export interface ToolAnnotations {
/** The tool only reads data and has no side effects. */
readOnlyHint?: boolean;
/** The tool's output may include content the page author doesn't control. */
untrustedContentHint?: boolean;
/**
* Running the tool has significant real-world or irreversible effects, like booking a
* flight or sending money. Agents may ask the user to confirm before calling it.
*/
consequentialHint?: boolean;
/** The tool is meant for debugging and developer tooling, not end users. */
debugging?: boolean;
}

export interface ToolDescriptor<TArgs = Record<string, unknown>> {
Expand Down
Loading