Custom MCP Tools
A Webfuse Extension can register its own MCP automation tools that any MCP client connected to the Session MCP Server will list and can call. The customer-facing API matches the WebMCP spec exactly, so customer code (and developer mental model) port between the two cleanly.
Custom tools can be registered from any extension component — content script,
service worker, popup, side panel, or new tab — by calling
browser.webfuseSession.registerTool({...}). The service worker is the natural
home for tools that must outlive page navigations. When a tool is called, the
request is delivered to every live component of the extension; the one that
registered the handler runs it. Registering the same tool name in two
components is undefined — pick one.
A tool registered in an ephemeral component (popup, side panel) stops responding once that component closes, but remains listed until the extension is reloaded.
Quickstart
Section titled “Quickstart”From any extension component, call browser.webfuseSession.registerTool({...}):
browser.webfuseSession.registerTool({ name: 'addTodo', description: 'Add a new item to the todo list', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'], }, execute: async ({ text }) => { return `Added todo: ${text}`; }, annotations: { readOnlyHint: false, untrustedContentHint: true },});That’s it — the tool is now exposed over MCP. A tools/list_changed notification fires automatically. Reload the extension to drop a registration.
How execute() is called
Section titled “How execute() is called”execute() receives (args, ctx):
| Parameter | Description |
|---|---|
args | The parsed tool arguments matching inputSchema. |
ctx.eventId | The in-flight MCP call id. Use it with sendAutomationProgress (see below). Ignore it if you don’t need progress. |
The return value becomes the MCP response:
- A string is wrapped into a single text content.
- An MCP-shaped
{ content, isError }object passes through unchanged. undefinedis coerced to'ok'.- A thrown error becomes
isError: truewith the error message as the text content.
Reporting progress for long-running tools
Section titled “Reporting progress for long-running tools”The Session MCP Server kills tool calls that go silent past its idle timeout. For tools whose execute() takes longer than that, emit progress via browser.webfuseSession.sendAutomationProgress(eventId, { progress, total, message }). Each call resets the idle timer and is forwarded to the MCP client as a notifications/progress (when the originating call carried a progressToken).
The eventId you pass is the one handed to your execute() as ctx.eventId:
browser.webfuseSession.registerTool({ name: 'longRunning', description: 'Slow operation with progress', inputSchema: { type: 'object' }, execute: async (_args, ctx) => { for (let i = 0; i < 5; i++) { await doStep(i); browser.webfuseSession.sendAutomationProgress(ctx.eventId, { progress: i + 1, total: 5, message: `step ${i + 1}/5`, }); } return 'done'; },});Static declaration in manifest.json
Section titled “Static declaration in manifest.json”A tool can also be declared in the manifest so the MCP server knows about it before the page loads. This is useful when an MCP client connects and lists tools before any matched URL has been navigated to. The declaration is metadata-only — the execute() handler is still supplied at runtime via registerTool.
{ "manifest_version": 3, "name": "todo-app", "tools": [ { "name": "addTodo", "description": "Add a new item to the todo list", "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] } } ]}session_id is automatically merged into every tool’s input schema so the MCP client can route the call to the right Webfuse Session — extensions never declare it themselves.
Capabilities inside execute()
Section titled “Capabilities inside execute()”The surface available to execute() depends on which component it runs in. A content-script-registered tool has access to:
- DOM (
document,window) of the automated page browser.webfuseSession.automation.*— the built-in automation primitives (hierarchical:act.click(),see.domSnapshot(),navigate(), …). Note: the Automation API uses a dot separator (act.click), while the Session MCP Server exposes the same primitives with an underscore separator (act_click) for broader client and LLM compatibility.browser.webfuseSession.apiRequest({cmd, ...})for session-level commands (takeScreenshot,GET_SESSION_INFO,transfer_tab_control, …)browser.runtime.sendMessage/onMessageto talk to the extension’s service worker, popup, or side panelfetch()for external APIs
A component without DOM access (service worker, popup, side panel) still drives the page via browser.webfuseSession.automation.* and apiRequest — it just cannot read document or window directly.
For work that should run once across all tabs (rather than per-tab), register the tool in the service worker where it naturally has that scope.
Replaying recorded MCP steps
Section titled “Replaying recorded MCP steps”A custom tool’s execute() can drive Webfuse’s automation primitives directly. browser.webfuseSession.automation is the same object the built-in MCP tools dispatch against — the Automation API uses dot-separated method names (automation.act.click()) while the MCP tool names use underscores (act_click) for compatibility with more MCP clients and LLMs:
browser.webfuseSession.registerTool({ name: 'login_flow', description: 'Log a user in', inputSchema: { type: 'object', properties: {} }, execute: async () => { const automation = browser.webfuseSession.automation; await automation.navigate({ url: '/login' }); await automation.act.type({ target: '#user', text: 'alice' }); await automation.act.click({ target: '#submit' }); return 'logged in'; },});Conflict rules
Section titled “Conflict rules”- A tool name registered by an extension shadows any built-in tool with the same name (built-in tool names are reserved by the manifest validator to prevent accidental shadowing).
- A second extension trying to register an already-claimed name is rejected client-side with a console warning; the first registration wins.
- When an extension is uninstalled or reloaded, all its registrations are dropped and the registry is rebroadcast (
tools/list_changed). - The per-session built-in tool allowlist (
automation_available_tools) does NOT apply to extension-registered tools — installing the extension IS the allow decision.