Skip to content
Open
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 .changeset/calm-tools-confirm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@browserbasehq/stagehand-extension": patch
"@browserbasehq/stagehand-go": patch
"@browserbasehq/stagehand": patch
"@browserbasehq/stagehand-python": patch
---

expose the WebMCP `consequentialHint` annotation as `consequential` across all SDKs
28 changes: 21 additions & 7 deletions packages/docs/v4/basics/webmcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const tools = await page.tools({ timeout: 3000 });
for (const tool of tools) {
console.log(tool.name, tool.description);
console.log(tool.inputSchema); // JSON Schema for invoke() input
console.log(tool.annotations); // { readOnly?, untrustedContent?, autosubmit? }
console.log(tool.annotations); // { readOnly?, untrustedContent?, consequential?, autosubmit? }
console.log(tool.frameId); // the frame that registered the tool
}
```
Expand All @@ -103,7 +103,7 @@ tools = await page.tools(timeout=3000)
for tool in tools:
print(tool.name, tool.description)
print(tool.input_schema) # JSON Schema for invoke() input
print(tool.annotations) # read_only / untrusted_content / autosubmit
print(tool.annotations) # read_only / untrusted_content / consequential / autosubmit
print(tool.frame_id) # the frame that registered the tool
```
</Tab>
Expand All @@ -121,7 +121,7 @@ for _, tool := range tools {
descriptor := tool.Descriptor()
fmt.Println(descriptor.Name, descriptor.Description)
fmt.Println(descriptor.InputSchema) // JSON Schema for Invoke input
fmt.Println(descriptor.Annotations) // ReadOnly / UntrustedContent / Autosubmit
fmt.Println(descriptor.Annotations) // tool safety hints
fmt.Println(descriptor.FrameID) // the frame that registered the tool
}
```
Expand All @@ -134,13 +134,27 @@ The listing timeout defaults to 1000 ms. Tools declared in iframes are included,

| Annotation | Meaning |
|---|---|
| `readOnly` | The tool does not mutate state, so it is safe to call speculatively |
| `untrustedContent` | The output contains page-controlled text; do not feed it to a model as if it were trusted |
| `autosubmit` | Invoking the tool submits something on the user's behalf |
| `readOnly` | The tool does not change application or system state |
| `untrustedContent` | The output may contain untrusted data, such as user-generated content |
| `consequential` | The tool may perform a significant or non-reversible action |
| `autosubmit` | The declarative tool submits a form when invoked |

Annotations are hints from the page, not guarantees enforced by the browser. Treat `untrustedContent` output as data, never as instructions.
WebMCP calls the site-facing annotation `consequentialHint`. Chrome DevTools Protocol and Stagehand
expose it as `consequential`.

Annotations come from the page. They are hints, not browser-enforced guarantees. Treat
`untrustedContent` output as data, never as instructions.
</Accordion>

<Warning>
Stagehand does not ask for confirmation before it invokes a WebMCP tool. When `consequential` is
`true`, confirm the action and its inputs with the user before calling `invoke()`. If you expose
WebMCP tools to a model, enforce this check before the adapter calls `invoke()`.

A missing or `false` value does not prove that a tool is safe. The site may have omitted or
misclassified the annotation.
</Warning>

## Invoking tools

Invoking is two steps: `invoke()` hands the call to the browser and returns immediately with a handle, then `result()` waits for the terminal response. Splitting them means a long-running tool does not block you, and you can cancel while it is in flight.
Expand Down
17 changes: 13 additions & 4 deletions packages/docs/v4/reference/webmcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ icon: "plug"
| `name` | `string` | Tool name used for invocation. |
| `description` | `string` | Human-readable tool description. |
| `inputSchema` | `Record<string, unknown> \| undefined` | Optional JSON input schema. |
| `annotations` | `WebMCPAnnotation \| undefined` | Optional `readOnly`, `untrustedContent`, and `autosubmit` flags. |
| `annotations` | `WebMCPAnnotation \| undefined` | Optional `readOnly`, `untrustedContent`, `consequential`, and `autosubmit` flags. |
| `frameId` | `string` | Frame that published the tool. |
| `backendNodeId` | `number \| undefined` | Optional non-negative backend node identifier. |

Expand All @@ -33,6 +33,9 @@ invoke(options?: WebMCPInvokeOptions): Promise<WebMCPInvocation>
`options.input` is a JSON-compatible object and defaults to `{}`. The helper supplies its own page,
frame, and tool name.

`invoke()` does not ask the user for confirmation. If `annotations.consequential` is `true`, confirm
the action and input with the user before invoking the tool.

## WebMCPInvocation

### Properties
Expand Down Expand Up @@ -84,7 +87,7 @@ Requests cancellation from Chrome. It does not synthesize or overwrite a termina
| `name` | `str` | Tool name used for invocation. |
| `description` | `str` | Human-readable tool description. |
| `input_schema` | `dict[str, JsonValue] \| None` | Optional JSON input schema. |
| `annotations` | `WebMCPAnnotation \| None` | Optional `read_only`, `untrusted_content`, and `autosubmit` flags. |
| `annotations` | `WebMCPAnnotation \| None` | Optional `read_only`, `untrusted_content`, `consequential`, and `autosubmit` flags. |
| `frame_id` | `str` | Frame that published the tool. |
| `backend_node_id` | `int \| None` | Optional non-negative backend node identifier. |

Expand All @@ -99,6 +102,9 @@ async def invoke(

`input` defaults to an empty dictionary. The helper supplies its own page, frame, and tool name.

`invoke()` does not ask the user for confirmation. If `annotations.consequential` is `True`, confirm
the action and input with the user before invoking the tool.

## WebMCPInvocation

### Properties
Expand Down Expand Up @@ -151,8 +157,8 @@ func (t *WebMCPTool) Descriptor() WebMCPToolDescriptor
```

The returned descriptor contains `Name`, `Description`, `InputSchema`, `Annotations`, `FrameID`,
and optional `BackendNodeID`. `Annotations` may contain `ReadOnly`, `UntrustedContent`, and
`Autosubmit` pointers.
and optional `BackendNodeID`. `Annotations` may contain `ReadOnly`, `UntrustedContent`,
`Consequential`, and `Autosubmit` pointers.

### Invoke()

Expand All @@ -166,6 +172,9 @@ func (t *WebMCPTool) Invoke(
`WebMCPInput` is a `map[string]any`; `nil` is sent as an empty JSON object. Values must be JSON
encodable. The helper supplies its own page, frame, and tool name.

`Invoke()` does not ask the user for confirmation. If `Annotations.Consequential` points to `true`,
confirm the action and input with the user before invoking the tool.

## WebMCPInvocation

### Descriptor()
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/tests/page-webmcp-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe("Page WebMCP tool discovery", () => {
annotations: {
readOnly: true,
untrustedContent: true,
consequential: true,
autosubmit: false,
},
frameId: "frame-1",
Expand Down Expand Up @@ -101,6 +102,7 @@ describe("Page WebMCP tool discovery", () => {
annotations: {
readOnly: true,
untrustedContent: true,
consequential: true,
autosubmit: false,
},
frameId: "frame-1",
Expand Down
1 change: 1 addition & 0 deletions packages/extension/understudy/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ function webMCPAnnotation(annotation: Protocol.WebMCP.Annotation): WebMCPAnnotat
...(annotation.untrustedContent === undefined
? {}
: { untrustedContent: annotation.untrustedContent }),
...(annotation.consequential === undefined ? {} : { consequential: annotation.consequential }),
...(annotation.autosubmit === undefined ? {} : { autosubmit: annotation.autosubmit }),
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,7 @@ export const WebMCPAnnotationSchema = z
.strictObject({
readOnly: z.boolean().optional(),
untrustedContent: z.boolean().optional(),
consequential: z.boolean().optional(),
autosubmit: z.boolean().optional(),
})
.meta({ id: "WebMCPAnnotation" });
Expand Down
3 changes: 3 additions & 0 deletions packages/protocol/stagehand.v4.json
Original file line number Diff line number Diff line change
Expand Up @@ -4060,6 +4060,9 @@
"untrusted_content": {
"type": "boolean"
},
"consequential": {
"type": "boolean"
},
"autosubmit": {
"type": "boolean"
}
Expand Down
20 changes: 20 additions & 0 deletions packages/protocol/tests/protocol/webmcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("WebMCP protocol data", () => {
annotations: {
readOnly: true,
untrustedContent: true,
consequential: true,
autosubmit: false,
},
frameId: "frame-1",
Expand All @@ -42,6 +43,7 @@ describe("WebMCP protocol data", () => {
annotations: {
readOnly: true,
untrustedContent: true,
consequential: true,
autosubmit: false,
},
frameId: "frame-1",
Expand All @@ -60,6 +62,24 @@ describe("WebMCP protocol data", () => {
).toThrow();
});

it("preserves false and omitted consequential annotations", () => {
expect(
WebMCPToolDescriptorSchema.parse({
name: "preview",
description: "Preview an action",
annotations: { consequential: false },
frameId: "frame-1",
}).annotations,
).toStrictEqual({ consequential: false });
expect(
WebMCPToolDescriptorSchema.parse({
name: "search",
description: "Search",
frameId: "frame-1",
}).annotations,
).toBeUndefined();
});

it("requires JSON-compatible input schemas", () => {
expect(() =>
WebMCPToolDescriptorSchema.parse({
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/tests/protocol/wire-casing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ describe("JSON-RPC wire casing", () => {
type: "object",
properties: { searchQuery: { type: "string" } },
},
annotations: { consequential: true },
frameId: "frame-1",
},
],
Expand All @@ -143,6 +144,7 @@ describe("JSON-RPC wire casing", () => {
type: "object",
properties: { searchQuery: { type: "string" } },
},
annotations: { consequential: true },
frame_id: "frame-1",
},
],
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk-go/models.gen.go

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

10 changes: 9 additions & 1 deletion packages/sdk-go/webmcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ func TestPageWrapsCallableWebMCPToolsWithOwnedIdentity(t *testing.T) {
t.Parallel()

readOnly := true
consequential := true
rpc := &recordingProtocolClient{responses: map[string]any{
"page.webmcp_tools": PageWebMCPToolsResult{Tools: []WebMCPToolDescriptor{{
Name: "search",
Description: "Search this site",
InputSchema: WebMCPToolDescriptorInputSchema{
"searchQuery": json.RawMessage(`{"type":"string"}`),
},
Annotations: &WebMCPAnnotation{ReadOnly: &readOnly},
Annotations: &WebMCPAnnotation{
ReadOnly: &readOnly,
Consequential: &consequential,
},
FrameID: "frame-1",
BackendNodeID: intPointer(42),
}}},
Expand Down Expand Up @@ -57,6 +61,10 @@ func TestPageWrapsCallableWebMCPToolsWithOwnedIdentity(t *testing.T) {
*descriptor.BackendNodeID != 42 {
t.Fatalf("tool descriptor = %#v", descriptor)
}
if descriptor.Annotations == nil || descriptor.Annotations.Consequential == nil ||
!*descriptor.Annotations.Consequential {
t.Fatalf("tool consequential annotation = %#v", descriptor.Annotations)
}

invocation, err := tool.Invoke(context.Background(), WebMCPInput{
"searchQuery": "Stagehand",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,7 @@ class StagehandObserveParams(TypedDict):
class WebMCPAnnotation(TypedDict):
read_only: NotRequired[bool]
untrusted_content: NotRequired[bool]
consequential: NotRequired[bool]
autosubmit: NotRequired[bool]


Expand Down
1 change: 1 addition & 0 deletions packages/sdk-python/src/stagehand/_generated/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2281,6 +2281,7 @@ class WebMCPAnnotation(WireModel):
)
read_only: Optional[StrictBool] = None
untrusted_content: Optional[StrictBool] = None
consequential: Optional[StrictBool] = None
autosubmit: Optional[StrictBool] = None


Expand Down
3 changes: 2 additions & 1 deletion packages/sdk-python/tests/test_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ async def test_page_wraps_callable_webmcp_tools_and_invocations_with_owned_ident
"type": "object",
"properties": {"searchQuery": {"type": "string"}},
},
"annotations": {"read_only": True},
"annotations": {"read_only": True, "consequential": True},
"frame_id": "frame-1",
"backend_node_id": 42,
}
Expand Down Expand Up @@ -610,6 +610,7 @@ async def test_page_wraps_callable_webmcp_tools_and_invocations_with_owned_ident
}
assert tool.annotations is not None
assert tool.annotations.read_only is True
assert tool.annotations.consequential is True
assert tool.frame_id == "frame-1"
assert tool.backend_node_id == 42

Expand Down
4 changes: 2 additions & 2 deletions packages/sdk-ts/tests/objectWrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,7 @@ describe("Stagehand TS object wrapper", () => {
type: "object",
properties: { searchQuery: { type: "string" } },
},
annotations: { readOnly: true },
annotations: { readOnly: true, consequential: true },
frameId: "frame-1",
backendNodeId: 42,
},
Expand Down Expand Up @@ -1081,7 +1081,7 @@ describe("Stagehand TS object wrapper", () => {
type: "object",
properties: { searchQuery: { type: "string" } },
},
annotations: { readOnly: true },
annotations: { readOnly: true, consequential: true },
frameId: "frame-1",
backendNodeId: 42,
});
Expand Down
Loading
Loading