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 are registered from an extension’s content.js. Service workers cannot register custom tools — they have no DOM access, no webfuseSession.automation.*, and the dispatch path is per-tab. If your tool needs cross-tab orchestration, register it in content.js and delegate work to the service worker via browser.runtime.sendMessage.
Quickstart
Section titled “Quickstart”Inside an extension’s content.js, 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. Registrations are scoped to the lifetime of the content script; reload the extension to drop them.
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()”Since execute() runs in the extension’s content.js, it has access to the same surface as the rest of the content script:
- 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
For work that should run once across all tabs (rather than per-tab), do it from the service worker and have execute() proxy a runtime.sendMessage to it.
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.