Files
248e5e0ea9 fix(browser-mcp): migrate CDP bridge → zapd consumer; disk authoritative (#35)
* fix(browser-mcp): migrate CDP bridge -> zapd consumer; make disk authoritative

The MCP runtime kept pulling the buggy published wheel (hanzo-tools-browser
0.5.7) from PyPI instead of the migrated on-disk source, so every restart
re-broke the `cdp` tool. Reinstate `cdp` as a zapd-native tool, finish the
bridge->zapd migration (delete dead bridge/server modules + their tests), bump
to 0.5.8 and pin hanzo-mcp >=0.5.8, add the package to the uv workspace/sources
so disk is authoritative. 9/9 browser tests pass.

Pairs with the hanzoai/extension bridge->zapd migration (separate, still
uncommitted). The ~/.claude.json MCP invocation was also pointed at the local
editable source (machine config, not in this repo).

(cherry picked from commit 11b4d4ea8a5570d6fb0e51079fa445dba3713f71)

* fix(browser): drop dead lifecycle.py (in-process zap_server/cdp_bridge are gone)

lifecycle.py only wrapped the removed in-process ZAP server + CDP bridge; nothing
imports it under the zapd-consumer model. One way: transport is zapd_consumer,
tools are browser/cdp/playwright.

---------

Co-authored-by: hanzo-dev <[email protected]>

Co-authored-by: Antje Worring <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-16 22:12:07 -07:00

106 lines
3.6 KiB
Python

"""Browser automation tools for Hanzo AI — zapd consumer model.
[Browser ext] --native host--> [zapd router] --unix sock--> [hanzo-mcp] --stdio--> [Agent]
hanzo-mcp hosts NO server. It connects to the one shared local router at
``~/.zap/run/zapd.sock`` as a *consumer*, lists providers, and routes opaque
CDP commands to a ``browser:*`` provider (the real Chrome/Firefox extension,
connected via its native-messaging host). No in-process server, no mDNS, no
well-known port pool, no :9224 HTTP bridge, no Playwright fallback in
native-browser mode. ``zapd`` is a separate always-on daemon (see ``~/work/zap``).
Three peer tools are exposed, all sharing one transport (``zapd_consumer``):
- ``browser`` — high-level, action-oriented (navigate/click/screenshot/tabs …).
Auto-routes through the extension over zapd; falls back to
Playwright only for actions the extension can't serve.
- ``cdp`` — low-level, method-oriented (send any CDP method by name).
- ``playwright`` — the same action surface as ``browser`` but pinned to the
Playwright backend (never touches the extension).
"""
import logging
from typing import Optional
from mcp.server import FastMCP
from hanzo_tools.core import BaseTool, ToolRegistry
from hanzo_tools.browser.browser_tool import (
PLAYWRIGHT_AVAILABLE,
BrowserPool,
BrowserTool,
get_backend,
_load_config,
_save_config,
browser_tool,
create_browser_tool,
launch_browser_server,
)
from hanzo_tools.browser.cdp_tool import CdpTool
from hanzo_tools.browser.playwright_tool import PlaywrightTool
from hanzo_tools.browser.zapd_consumer import ZapdConsumer, get_consumer
logger = logging.getLogger(__name__)
# Tools list for entry point discovery (see pyproject [hanzo.tools]).
TOOLS = [BrowserTool, CdpTool, PlaywrightTool]
__all__ = [
# Tools (the three peers)
"BrowserTool",
"CdpTool",
"PlaywrightTool",
# Factory + module-level instance (existing public API)
"browser_tool",
"create_browser_tool",
# Browser pool / Playwright server
"BrowserPool",
"launch_browser_server",
# zapd consumer (the one canonical transport)
"ZapdConsumer",
"get_consumer",
# Availability check
"PLAYWRIGHT_AVAILABLE",
# Backend helper
"get_backend",
# Registration
"TOOLS",
"register_browser_tools",
"register_tools",
]
def register_browser_tools(mcp_server: FastMCP, **kwargs) -> list[BaseTool]:
"""Register the browser tools with the MCP server.
hanzo-mcp is a zapd *consumer* — it connects to the shared local router at
``~/.zap/run/zapd.sock`` on demand and hosts no in-process server. zapd is a
separate always-on daemon, so there is nothing to start here.
Args:
mcp_server: The FastMCP server instance
**kwargs: Forwarded to the browser tool (headless, cdp_endpoint, backend)
Returns:
List of registered tools
"""
headless = kwargs.get("headless", True)
cdp_endpoint = kwargs.get("cdp_endpoint")
backend = kwargs.get("backend")
tools: list[BaseTool] = [
create_browser_tool(headless=headless, cdp_endpoint=cdp_endpoint, backend=backend),
CdpTool(),
PlaywrightTool(headless=headless, cdp_endpoint=cdp_endpoint),
]
for tool in tools:
ToolRegistry.register_tool(mcp_server, tool)
return tools
def register_tools(mcp_server: FastMCP, **kwargs) -> list[BaseTool]:
"""Register all browser tools with the MCP server.
Standard entry point called by the tool discovery system.
"""
return register_browser_tools(mcp_server, **kwargs)