- MCP protocol compliance: ping, resources/list, prompts/list, logging/setLevel, 202 notifications - Screenshot tool: WebKit snapshot to PNG to base64 MCP image content - Session management: Mcp-Session-Id header, SSE response format, GET/DELETE endpoints - Phase 3: 70 new tools across 8 batches (extended interaction, get/state, find elements, wait/batch, cookies/storage, mouse/clipboard/settings, frames/dialogs/debug, complex) - Favicon support: enabled WebKitFaviconDatabase, notify::favicon signal handler - Tab UI: favicons in tab labels, evenly distributed tabs, immediate title from URL host - Refresh button: left-click reload, right-click hard reload menu (bypass cache, clear site data) - Webview sizing fix: gtk_widget_set_vexpand/hexpand to prevent 1px height blank page - Updated Roo Code MCP config (100 tools in alwaysAllow) - Updated plans: mcp-server.md, agent-tools.md, phase3-tools.md
17 KiB
MCP Server Implementation Plan
Overview
Add an MCP (Model Context Protocol) endpoint to sovereign_browser's existing agent server. This allows AI assistants (Roo Code, Claude Code, Cursor, etc.) to control the browser directly — no custom scripts, no CLI, no separate process. The browser exposes its tools as MCP tools, and the AI assistant calls them like any other MCP tool.
Architecture
AI Assistant (Roo Code, Claude Code, etc.)
↕ MCP Streamable HTTP (JSON-RPC over SSE)
sovereign_browser (SoupServer on port 17777)
├── /agent → WebSocket (existing, for scripts/CLI)
├── /mcp → MCP Streamable HTTP (new, for AI assistants)
└── / → HTTP status (existing)
Both /agent (WebSocket) and /mcp (MCP) use the same internal
agent_tools_dispatch() function — same code path, same tools, same
behavior. This follows the debugging principle: no parallel
implementations.
MCP Streamable HTTP transport
MCP supports a "Streamable HTTP" transport where the client sends HTTP POST requests and the server responds with Server-Sent Events (SSE). This is ideal for us — we already have a SoupServer running.
Protocol flow
-
Client sends POST to
/mcpwith JSON-RPC body:{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "roo-code", "version": "1.0"} }} -
Server responds with SSE stream containing the result:
event: message data: {"jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, "serverInfo": {"name": "sovereign-browser", "version": "0.0.10"} }} -
Client sends
tools/list:{"jsonrpc": "2.0", "id": 2, "method": "tools/list"} -
Server responds with the full tool catalog (30 tools).
-
Client sends
tools/call:{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "snapshot", "arguments": {"interactive": true, "compact": true} }} -
Server responds with the tool result.
Session management
MCP Streamable HTTP uses a session ID (returned in the Mcp-Session-Id
header from initialize). The client includes this header in subsequent
requests. We store session state (which is minimal — just the session ID
and whether the client has initialized).
Tool catalog
Each MCP tool has: name, description, and inputSchema (JSON Schema).
Login tools
{
"name": "login_status",
"description": "Check if the browser is logged in. Returns the current login state, method, and pubkey if logged in.",
"inputSchema": {"type": "object", "properties": {}}
}
{
"name": "login",
"description": "Log in to the browser with a Nostr identity. Methods: 'local' (nsec or hex privkey), 'seed' (BIP-39 mnemonic), 'readonly' (npub), 'nip46' (bunker:// URL), 'nsigner' (hardware signer). Must be called before browser tools work.",
"inputSchema": {
"type": "object",
"properties": {
"method": {"type": "string", "enum": ["local", "seed", "readonly", "nip46", "nsigner"]},
"nsec": {"type": "string", "description": "nsec1... string (method: local)"},
"privkey_hex": {"type": "string", "description": "64-char hex private key (method: local)"},
"mnemonic": {"type": "string", "description": "12 or 24 BIP-39 words (method: seed)"},
"account": {"type": "integer", "default": 0, "description": "Account index (method: seed)"},
"npub": {"type": "string", "description": "npub1... string (method: readonly)"},
"pubkey_hex": {"type": "string", "description": "64-char hex pubkey (method: readonly)"},
"bunker_url": {"type": "string", "description": "bunker:// URL (method: nip46)"},
"transport": {"type": "string", "enum": ["serial", "unix", "tcp", "qrexec"], "description": "Transport type (method: nsigner)"},
"device": {"type": "string", "description": "Device path, socket name, host:port, or qube name (method: nsigner)"},
"service": {"type": "string", "default": "qubes.NsignerRpc", "description": "Qrexec service name (method: nsigner, transport: qrexec)"},
"index": {"type": "integer", "default": 0, "description": "Key index (method: nsigner)"}
},
"required": ["method"]
}
}
{
"name": "logout",
"description": "Log out and clear the current Nostr identity.",
"inputSchema": {"type": "object", "properties": {}}
}
{
"name": "switch_identity",
"description": "Switch to a new Nostr identity. Same parameters as login. Frees the old signer first.",
"inputSchema": {"type": "object", "properties": {"method": {"type": "string"}, "nsec": {"type": "string"}, "privkey_hex": {"type": "string"}, "mnemonic": {"type": "string"}, "npub": {"type": "string"}, "bunker_url": {"type": "string"}, "transport": {"type": "string"}, "device": {"type": "string"}, "index": {"type": "integer"}}, "required": ["method"]}
}
Navigation tools
{"name": "open", "description": "Navigate the active tab to a URL.", "inputSchema": {"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]}}
{"name": "back", "description": "Go back in browser history.", "inputSchema": {"type": "object", "properties": {}}}
{"name": "forward", "description": "Go forward in browser history.", "inputSchema": {"type": "object", "properties": {}}}
{"name": "reload", "description": "Reload the current page (bypassing cache).", "inputSchema": {"type": "object", "properties": {}}}
{"name": "get_url", "description": "Get the current URL of the active tab.", "inputSchema": {"type": "object", "properties": {}}}
{"name": "get_title", "description": "Get the page title of the active tab.", "inputSchema": {"type": "object", "properties": {}}}
Snapshot & inspection tools
{
"name": "snapshot",
"description": "Get the accessibility tree of the current page with element refs. Returns a text tree and a ref map. Use refs (e.g. @e1) to interact with elements by click, fill, etc. Call this after navigation or page changes to see what's on the page.",
"inputSchema": {
"type": "object",
"properties": {
"interactive": {"type": "boolean", "default": true, "description": "Only show interactive elements (links, buttons, inputs)"},
"compact": {"type": "boolean", "default": true, "description": "Remove empty structural elements"}
}
}
}
{
"name": "get_text",
"description": "Get the text content of an element by ref or CSS selector.",
"inputSchema": {"type": "object", "properties": {"ref": {"type": "string", "description": "Element ref from snapshot (e.g. @e1)"}, "selector": {"type": "string", "description": "CSS selector"}}, "oneOf": [{"required": ["ref"]}, {"required": ["selector"]}]}
}
{
"name": "get_html",
"description": "Get the innerHTML of an element by ref or CSS selector.",
"inputSchema": {"type": "object", "properties": {"ref": {"type": "string"}, "selector": {"type": "string"}}, "oneOf": [{"required": ["ref"]}, {"required": ["selector"]}]}
}
{
"name": "get_attr",
"description": "Get an attribute of an element by ref or CSS selector.",
"inputSchema": {"type": "object", "properties": {"ref": {"type": "string"}, "selector": {"type": "string"}, "attr": {"type": "string", "description": "Attribute name (e.g. href, src, class)"}}, "required": ["attr"]}
}
{
"name": "eval",
"description": "Run JavaScript in the current page and return the result. Use for custom inspection or interaction not covered by other tools.",
"inputSchema": {"type": "object", "properties": {"script": {"type": "string", "description": "JavaScript to execute"}}, "required": ["script"]}
}
Interaction tools
{"name": "click", "description": "Click an element by ref or CSS selector.", "inputSchema": {"type": "object", "properties": {"ref": {"type": "string"}, "selector": {"type": "string"}}, "oneOf": [{"required": ["ref"]}, {"required": ["selector"]}]}}
{"name": "fill", "description": "Clear an input and fill it with a value.", "inputSchema": {"type": "object", "properties": {"ref": {"type": "string"}, "selector": {"type": "string"}, "value": {"type": "string"}}, "required": ["value"]}}
{"name": "type", "description": "Type text into an element (appends to existing value).", "inputSchema": {"type": "object", "properties": {"ref": {"type": "string"}, "selector": {"type": "string"}, "value": {"type": "string"}}, "required": ["value"]}}
{"name": "press", "description": "Press a keyboard key (e.g. Enter, Tab, Escape).", "inputSchema": {"type": "object", "properties": {"key": {"type": "string"}}, "required": ["key"]}}
{"name": "scroll", "description": "Scroll the page in a direction.", "inputSchema": {"type": "object", "properties": {"direction": {"type": "string", "enum": ["up", "down", "left", "right"]}, "amount": {"type": "integer", "default": 500}}, "required": ["direction"]}}
{"name": "hover", "description": "Hover over an element by ref or CSS selector.", "inputSchema": {"type": "object", "properties": {"ref": {"type": "string"}, "selector": {"type": "string"}}, "oneOf": [{"required": ["ref"]}, {"required": ["selector"]}]}}
{"name": "focus", "description": "Focus an element by ref or CSS selector.", "inputSchema": {"type": "object", "properties": {"ref": {"type": "string"}, "selector": {"type": "string"}}, "oneOf": [{"required": ["ref"]}, {"required": ["selector"]}]}}
{"name": "close", "description": "Close the active tab.", "inputSchema": {"type": "object", "properties": {}}}
Tab tools
{"name": "tab_list", "description": "List all open tabs with their URLs and titles.", "inputSchema": {"type": "object", "properties": {}}}
{"name": "tab_new", "description": "Open a new tab, optionally with a URL.", "inputSchema": {"type": "object", "properties": {"url": {"type": "string"}}}}
{"name": "tab_switch", "description": "Switch to a tab by index.", "inputSchema": {"type": "object", "properties": {"index": {"type": "integer"}}, "required": ["index"]}}
{"name": "tab_close", "description": "Close a tab by index. If no index, closes the active tab.", "inputSchema": {"type": "object", "properties": {"index": {"type": "integer"}}}}
Wait tools
{"name": "wait", "description": "Wait for a specified number of milliseconds.", "inputSchema": {"type": "object", "properties": {"ms": {"type": "integer", "default": 1000}}}}
{"name": "wait_for", "description": "Wait for an element to appear on the page.", "inputSchema": {"type": "object", "properties": {"selector": {"type": "string"}, "timeout": {"type": "integer", "default": 10000}}, "required": ["selector"]}}
Implementation
New file: src/agent_mcp.h / src/agent_mcp.c
/*
* MCP (Model Context Protocol) server endpoint.
* Implements the Streamable HTTP transport on top of our existing
* SoupServer. Exposes browser tools as MCP tools for AI assistants.
*/
/* Register the MCP handler at /mcp on the given SoupServer. */
void agent_mcp_register(SoupServer *server);
MCP request handling
The MCP handler at /mcp processes HTTP POST requests with JSON-RPC
bodies. It handles three methods:
initialize— returns server info and capabilities, creates a session, returnsMcp-Session-Idheadertools/list— returns the tool catalog (all 30 tools with schemas)tools/call— dispatches toagent_tools_dispatch()(same as WebSocket)ping— health check, returns empty resultresources/list/resources/templates/list— returns empty lists (no resources exposed)prompts/list— returns empty list (no prompts exposed)logging/setLevel— acknowledged, no filtering appliednotifications/*— returns 202 Accepted with no bodyGET /mcp— returns SSE content-type with: connectedcommentDELETE /mcp— terminates the session (requiresMcp-Session-Idheader)
For tools/call, the MCP handler:
- Extracts the tool name and arguments from the JSON-RPC params
- Constructs a cJSON request object (same format as WebSocket)
- Calls
agent_tools_dispatch(request, NULL)— passing NULL for the connection since MCP uses HTTP response, not WebSocket - For sync tools: returns the response immediately as JSON-RPC result
- For async tools (snapshot, eval, etc.): this is the tricky part...
Async tool handling for MCP
The async tools (snapshot, eval, get_text, etc.) send their response through a WebSocket connection callback. But MCP uses HTTP responses, not WebSocket. We need a way to capture the async result and return it in the HTTP response.
Approach: Use a GMainLoop polling approach (which works for HTTP since we're not inside a WebSocket callback). The MCP handler:
- Creates a temporary "result holder" struct
- Calls
agent_js_eval_async()with a custom callback that stores the result in the holder and quits a GMainLoop - Runs a GMainLoop with timeout
- When the JS completes, the callback stores the result
- The handler returns the result as the HTTP response
This works because the MCP HTTP handler is a regular SoupServer callback (not a WebSocket message callback), so the GMainLoop polling approach works correctly.
Tool catalog generation
The tool catalog is a static cJSON array built once at startup. Each tool entry has:
name: tool name stringdescription: human-readable descriptioninputSchema: JSON Schema object
Modified files
src/agent_mcp.h/src/agent_mcp.c— new, MCP handlersrc/agent_server.c— callagent_mcp_register()inagent_server_start()src/agent_tools.c— add a variant of dispatch that works with a result-holder instead of a WebSocket connection (for MCP async tools)Makefile— addsrc/agent_mcp.cto SRC
Roo Code configuration
To use the browser as an MCP server in Roo Code, add this to the MCP configuration:
{
"mcpServers": {
"sovereign-browser": {
"url": "http://localhost:17777/mcp",
"transport": "streamable-http"
}
}
}
Implementation order
- Create
src/agent_mcp.h/src/agent_mcp.cwith the tool catalog - Implement
initializeandtools/listhandlers - Implement
tools/callfor sync tools (login, open, tab_list, etc.) - Implement
tools/callfor async tools (snapshot, eval, get_text, etc.) using the GMainLoop polling approach - Register the MCP handler in
agent_server_start() - Test with Roo Code or a manual MCP client
- Update Makefile and docs
Implementation Status
Completed — all items in the implementation order above are done.
What's implemented
- MCP Streamable HTTP transport on
/mcp(port 17777), coexisting with the existing/agentWebSocket endpoint. - Session management:
initializecreates a session and returns aMcp-Session-Idheader. Subsequent requests are validated against the session store. Unknown/expired sessions return 404. Sessions have a 60-second idle timeout. - SSE responses: all JSON-RPC responses (requests with an
id) are wrapped inevent: message\ndata: <json>\n\nSSE format withContent-Type: text/event-stream. - GET /mcp: returns a
text/event-streamresponse with a: connectedcomment (satisfies clients probing for SSE push support). - DELETE /mcp: terminates a session by
Mcp-Session-Idheader. Returns 200 on success, 400 if header missing, 404 if session unknown. - Notifications:
notifications/*and any request without anidreturn 202 Accepted with no body. - Tool catalog: 30 tools exposed (29 original +
screenshot). Thescreenshottool returns an MCP image content block (type: "image", base64 PNG,mimeType: "image/png") instead of a text block. - Lenient mode: requests without a
Mcp-Session-Idheader are accepted with a warning (backward compatibility with older clients). - Roo Code integration: configured in
mcp_settings.jsonwithtransport: "streamable-http"and all 30 tools inalwaysAllow.
Test results (2026-07-12)
All 10 integration tests pass:
| Test | Description | Result |
|---|---|---|
| 1 | Initialize — returns Mcp-Session-Id header + SSE body |
✅ Pass |
| 2 | tools/list — 30 tools including screenshot |
✅ Pass |
| 3 | ping with session — SSE empty result |
✅ Pass |
| 4 | resources/list — empty resources array |
✅ Pass |
| 5 | prompts/list — empty prompts array |
✅ Pass |
| 6 | notifications/initialized — 202 Accepted |
✅ Pass |
| 7 | GET /mcp — text/event-stream content type |
✅ Pass |
| 8 | DELETE /mcp — 200 OK |
✅ Pass |
| 9 | ping after DELETE — 404 (session terminated) |
✅ Pass |
| 10 | login_status tool call — returns login state |
✅ Pass |
Files
src/agent_mcp.c— MCP handler (request routing, session management, SSE responses)src/agent_mcp.h— public APIsrc/agent_tools.c— tool dispatch (shared with WebSocket endpoint)src/agent_server.c— registers MCP handler on the SoupServerMakefile— includessrc/agent_mcp.cin the build