From 256d48f32db21e381286692f0a1bccfec245ff9e Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Sun, 1 Nov 2026 18:51:37 +0000 Subject: [PATCH 1/2] Copy destructiveHint to WebMCP's consequentialHint when it is not set --- js/README.md | 4 +++- js/src/index.ts | 19 +++++++++++++++++-- js/test/core.test.ts | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/js/README.md b/js/README.md index fb03e5b6..2e713af8 100644 --- a/js/README.md +++ b/js/README.md @@ -245,7 +245,9 @@ point of view. A full page load ends the call before it can return. | `xwebmcp` | `registerTool(tool, { signal })`, `refresh()`, `getModelContext()`, `availableToolNames()`, `subscribe(listener)`; no React | `useWebMCPTool({ name, description, inputSchema?, outputSchema?, annotations?, title?, execute, enabled? })` -takes the fields of a WebMCP tool plus `enabled`. +takes the fields of a WebMCP tool plus `enabled`. Chrome reads `consequentialHint` rather than +MCP's `destructiveHint`; when only the latter is set it is copied over, so a destructive tool is +marked as needing confirmation. `useWebMCPRoute({ path, description, enabled? })`. diff --git a/js/src/index.ts b/js/src/index.ts index 4114058c..97683570 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -6,6 +6,11 @@ export interface ToolAnnotations { idempotentHint?: boolean; openWorldHint?: boolean; untrustedContentHint?: boolean; + /** + * WebMCP's name for an action with effects the user would want to confirm. + * Defaults to `destructiveHint`, so MCP-style annotations carry over. + */ + consequentialHint?: boolean; } export interface WebMCPTool { @@ -121,6 +126,11 @@ function notify() { for (const listener of listeners) listener(); } +function withConsequentialHint(annotations: ToolAnnotations): ToolAnnotations { + if (annotations.consequentialHint !== undefined || annotations.destructiveHint === undefined) return annotations; + return { ...annotations, consequentialHint: annotations.destructiveHint }; +} + function sync(name: string) { const ctx = context(); if (!ctx) return; @@ -134,14 +144,19 @@ function sync(name: string) { if (top) { const controller = new AbortController(); active.set(name, { tool: top, controller }); - const { execute, ...definition } = top; + const { execute, annotations, ...definition } = top; // Deferred so that replacing a registration within one task only // registers the final one with the browser. Promise.resolve() .then(() => { if (controller.signal.aborted) return; return ctx.registerTool( - { inputSchema: { type: 'object', properties: {} }, ...definition, execute }, + { + inputSchema: { type: 'object', properties: {} }, + ...definition, + ...(annotations ? { annotations: withConsequentialHint(annotations) } : {}), + execute, + }, { signal: controller.signal }, ); }) diff --git a/js/test/core.test.ts b/js/test/core.test.ts index 3e8a2352..b64e6831 100644 --- a/js/test/core.test.ts +++ b/js/test/core.test.ts @@ -114,6 +114,24 @@ describe('with WebMCP', () => { expect(ctx.tools.has('a')).toBe(true); }); + it('derives consequentialHint from destructiveHint', async () => { + const ctx = installFakeModelContext(); + core.registerTool({ ...tool('a'), annotations: { destructiveHint: true } }, { signal: new AbortController().signal }); + core.registerTool( + { ...tool('b'), annotations: { destructiveHint: true, consequentialHint: false } }, + { signal: new AbortController().signal }, + ); + await flush(); + expect((ctx.tools.get('a') as { annotations?: object }).annotations).toEqual({ + destructiveHint: true, + consequentialHint: true, + }); + expect((ctx.tools.get('b') as { annotations?: object }).annotations).toEqual({ + destructiveHint: true, + consequentialHint: false, + }); + }); + it('lists tools registered outside this package', async () => { const ctx = installFakeModelContext(); ctx.registerTool({ ...tool('native'), inputSchema: {} }, { signal: new AbortController().signal }); From 607260bfba0bcfcf89e1f09fb858e18209d51288 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:05:12 +0000 Subject: [PATCH 2/2] xwebmcp 0.1.2 --- js/package-lock.json | 4 ++-- js/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/package-lock.json b/js/package-lock.json index 7f36777a..564bc5c1 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "xwebmcp", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xwebmcp", - "version": "0.1.1", + "version": "0.1.2", "license": "UNLICENSED", "devDependencies": { "@testing-library/react": "^16.3.3", diff --git a/js/package.json b/js/package.json index 63d5187f..232a1641 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "xwebmcp", - "version": "0.1.1", + "version": "0.1.2", "description": "Register WebMCP tools from React and Next.js components, with distributed get_page_context and navigate_to_page tools.", "license": "MIT", "repository": {