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
4 changes: 3 additions & 1 deletion js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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? })`.

Expand Down
4 changes: 2 additions & 2 deletions js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion js/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
19 changes: 17 additions & 2 deletions js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Input = any, Output = unknown> {
Expand Down Expand Up @@ -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;
Expand All @@ -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 },
);
})
Expand Down
18 changes: 18 additions & 0 deletions js/test/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
Loading