
One array, two consumers
There are 49 page capabilities. They are defined once, in
src/lib/actions/registry.ts, and that array is compiled into both
the extension and the daemon.
Each action is a small module with a name, a description, a zod input schema and an execute():
export const clickElement = defineAction({
name: 'page.clickElement',
description: '…',
input: z.object({ target: targetSchema.describe('…') }),
execute({ target }) { /* runs in the page */ },
});
describeActions() turns that array into tool descriptors by converting each zod schema to JSON
Schema. The daemon serves those descriptors to MCP clients, so a tool cannot describe a capability
the browser does not have: both are generated from the same source.
Names
Action names are dotted (page.getPageInfo); MCP tool names are underscored
(page_getPageInfo). The mapping is mechanical.
assertToolNamesRoundTrip() runs on every tools/list to prove that every name survives the round
trip and that no two actions collide. That check is why action names must not contain
underscores: page.get_info would come back as page.get.info.
Drift detection
The extension hashes its bundled manifest and sends the hash in hello. If it differs from the
daemon's, the daemon:
- flags
manifestInSync: false, visible inbrowsentic statusandbrowsentic_status; - asks the extension for its actual descriptor list over a
describeframe and adopts it, because the browser is the authority on what the browser can do; - broadcasts
manifest-changedto control clients, which makes each MCP server emitnotifications/tools/list_changed.
So a half-rebuilt install degrades into "the tools the browser really has", loudly, rather than into tool calls that fail at the far end.
The fix is always to rebuild both halves (see guide/maintenance.md).
Reserved actions
Four names under the browsentic. prefix are not page actions:
| Action | Who calls it |
|---|---|
browsentic.saveSiteMap |
The agent, at the end of a mapping run |
browsentic.startRecording |
The intent grammar, on the user's own words |
browsentic.stopRecording |
The intent grammar |
browsentic.readSitemap |
The daemon's agent runner |
The daemon's invoke() refuses anything starting with that prefix, so they are unreachable from an
ordinary MCP client. browsentic_saveSiteMap is published as a tool only to the MCP server
spawned inside an agent run.
browsentic_status is not in the registry either: the MCP server answers it directly by combining
daemon state with a one-element page.getPageInfo and the current monitor list.
Verifying
yarn daemon:manifest
Builds the MCP server and prints every tool with its full JSON Schema. No browser needed. This is the source of truth that reference/tools.md is kept in step with.
Next
Request path →: how a tool call reaches the page.
Adding one: Contributing § Adding a capability.