# why
users need a way to subscribe to events that fire when webmcp tools were
added or removed from a page
### note:
this PR ended up a lot larger than just adding two hooks because
`WebMCP.enable` causes Chrome to emit `toolsAdded` events for tools that
already exist (docs
[here](https://chromedevtools.github.io/devtools-protocol/tot/WebMCP/#method-enable)).
ie, the `WebMCP.toolsAdded` event would fire every time someone called
`page.tools()`. therefore, we needed to manage some state, and
essentially keep a registry of tools that have been added/removed.
with the changes from this PR, `page.tools()` reads that registry
instead of enabling WebMCP again. a lot of the code here is related to
state synchronization, because the registry now needs to be kept in sync
across navigation and iframe detachment etc..
# what changed
- added `onToolsAdded()` / `onToolsRemoved()` in TypeScript,
`on_tools_added()` / `on_tools_removed()` in Python, and
`OnToolsAdded()` / `OnToolsRemoved()` in Go
- reused the existing subscription handles for `unsubscribe()` /
`Close(ctx)`
- added typed `page.event` notifications alongside the existing raw
`page.cdp_event` path, with shared protocol schemas and generated SDK
models
- made subscription setup asynchronous, with cleanup when registration
fails or the subscriber disconnects during setup
- moved WebMCP discovery onto shared page-owned tool state, so
`page.tools()` and the hooks use the same state without repeatedly
enabling WebMCP or replaying discovery results to listeners
- tracked tools across the main frame and child frames, including
out-of-process iframes, and emitted removals when their document
navigates or their frame detaches
- kept tool tracking alive independently of individual subscriptions,
while cleaning up listeners when the page or client is disposed
- also updated the WebMCP examples in all three SDKs
# test plan
- [x] Protocol tests cover typed notification payloads and the unchanged
console event contract.
- [x] Extension tests cover shared discovery state, future-only
delivery, frame navigation and detachment, independent subscribers,
registration races, & cleanup.
- [x] TypeScript, Python, and Go SDK tests cover typed callback values,
subscription routing, callable added tools, & unsubscribe behavior.
- [x] Real-browser WebMCP tests cover discovery and invocation, callback
invocation without deadlocking, same-origin and cross-origin iframe
changes, no replay, & independent unsubscribe.
- [x] Cross-SDK parity checks and example typechecks/compilation pass.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds WebMCP tool add/remove event hooks so users can subscribe when
tools appear or disappear on a page. Previously only console events were
available; the new `onToolsAdded()` / `onToolsRemoved()` (TypeScript),
`on_tools_added()` / `on_tools_removed()` (Python), and `OnToolsAdded()`
/ `OnToolsRemoved()` (Go) hooks deliver typed payloads over a new
`page.event` notification alongside the existing raw `page.cdp_event`
path.
**New Features**
- Hooks report future changes only, not tools already present on the
page.
- Added hooks deliver callable `WebMCPTool` objects; removed hooks
deliver tool identities; the generic `page.on()` / `page.On()`
subscription remains console-only and now rejects tool event names
before subscribing.
- WebMCP discovery moved onto shared page-owned tool state so
`page.tools()` and the hooks stay consistent without repeatedly enabling
WebMCP or replaying results; a failed `WebMCP.enable` retries on the
next call.
- Tools are tracked across the main frame, child frames, and
out-of-process iframes, with removals on document navigation or frame
detach and preservation on frame swap.
- Tool tracking persists independently of subscriptions and is cleaned
up on page or client disposal; subscription setup is asynchronous and
cleans up on registration failure or mid-setup disconnect.
<sup>Written for commit a4cf12a39ebd52be11a7d878e7d8538f1f369e06.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2916?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->