hanzo-mcp (0.15.1 -> 0.15.3):
- _normalize_mcp_result() collapses FastMCP list[TextContent] to parsed JSON
for ZAP MSG_RESPONSE serialization
- Closes the "TextContent not JSON serializable" bug surfaced when driving
the Hanzo extension from agent via tools/call
- Bump hanzo-tools-browser[playwright] floor to 0.5.4
hanzo-tools-browser (0.5.2 -> 0.5.4):
- mDNS-only ZAP discovery via _hanzo._tcp.local. (HIP-0069); no port pool,
no lockfile, OS-assigned ports advertised by hanzo-zap-mdns
- zap_server.py: refactor for unique server_id + retract on stop
- Test suite trimmed to the new wire contract
Patch-bump both packages.
The custom encode/decode and message-type constants in zap_server.py
were a byte-identical duplicate of zap-protocol (~/work/zap/zap-py).
Drop the duplicates, import from canonical:
from zap.protocol import (
ZAP_MAGIC, HEADER_SIZE, MAX_MESSAGE_SIZE,
MSG_HANDSHAKE, MSG_HANDSHAKE_OK,
MSG_REQUEST, MSG_RESPONSE,
MSG_PING, MSG_PONG,
ZAP_PORTS as DEFAULT_ZAP_PORTS,
encode, decode,
)
Single source of truth for the wire — any change (MSG_STREAM, frame
versioning, future Cap'n Proto migration) lands once and propagates.
zap-protocol added as a dependency. Verified at install:
ZAP encode unified: True
ZAP decode unified: True
Every ZapServer.start() now publishes its bound port + agent_label as a
`_hanzo-zap._tcp.local.` mDNS record (best-effort — silently skipped
when `hanzo-zap-mdns` isn't installed). Browser extensions and any
LAN-wide agent can discover the server via `hanzo_zap_mdns.browse()`
without hard-coded ports or fragile lockfile registries.
Publish runs on a worker thread (asyncio.to_thread) so zeroconf's
internal multicast-socket setup doesn't block our event loop and
trigger EventLoopBlocked on zeroconf >= 0.140.
stop() retracts the announcement before closing the WS.
When an extension reuses its client_id across consecutive ZAP connections
(probe followed by connectZap, both with mgr.state.extensionId), the
older websocket's finally clause was unconditionally popping the
client_id from _clients — deleting the new connection's freshly-installed
registration. The extension would then immediately disconnect because the
server-side state no longer had it, hammering reconnect loops.
Now the finally clause only removes the entry when it still points at
the closing websocket. Verified end-to-end: Firefox 1.9.x ZAP-native
connection holds steady; tabs/screenshot/evaluate round-trip in
11/73/5 ms.
Each hanzo-mcp now hosts a ZAP server directly on the lowest free port
from [9999..9995], using a POSIX flock for cross-MCP arbitration. The
browser extension discovers and connects to this server without any
node-side bridge in the critical path.
- New hanzo_tools/browser/zap_server.py: wire format, server, lifecycle,
client registry, leases, cluster-visibility config registry.
- _extension_command tries ZAP first (sub-1ms median round-trip on
loopback), falls back to legacy HTTP bridge on :9224 only when no
ZAP-connected client matches. Pinnable via BROWSER_TRANSPORT.
- New tool actions: list_mcp_instances, claim_browser, release_browser
for sub-agent coordination across multiple parallel MCPs.
- Legacy CDP HTTP bridge no longer auto-starts (opt in with
HANZO_CDP_BRIDGE_ENABLED=1). It remains available for non-ZAP MCP
clients.
Tests: 30 new unit tests cover wire format, port arbitration,
multi-MCP coexistence, lease semantics, and cluster registry.
Bench shows 0.35ms median round-trip on local loopback.
Total tests: 4 baseline -> 35 passing (30 zap_server + 1 bench + 4 imports).
The extension's bridge server (cdp-bridge-server.ts v1.9.0) added two
new actions for dynamic browser switching:
set_default_browser — persist a default-target browser
use_browser — alias of set_default_browser
Both accept the browser name (firefox / chrome / safari / edge or a
specific instance ID like 'firefox-2') as the 'browser' field. The
bridge writes the choice to ~/.hanzo/extension/config.json so it
survives bridge restarts.
This commit:
- Adds the two action names to the Action Literal so the MCP tool
accepts them.
- Adds them to extension_actions so they route through the bridge
rather than falling to Playwright.
- The 'target_browser' parameter (already exposed) carries the
browser name through to the bridge as 'browser', which is exactly
what set_default_browser expects.
Bumps hanzo-tools-browser 0.4.5 → 0.4.6.
Mirror the v1.8.3 extension bridge fix on the python side. The MCP
'browser' tool now exposes:
- tab_id — accepts the targetId returned by 'tabs' (e.g. "tab-1888868904")
or a numeric tab id. Forwards as tabId so every action operates on the
specified tab instead of the OS-active tab. Critical when the user has
many windows open and the wrong one is in focus.
- client_id — pin to a specific connected extension instance (returned
in 'status'). Use with 'list_browsers' to address one Chrome instance
vs another Firefox instance bridged to the same hanzo bridge.
- target_browser — 'chrome' | 'firefox' per call. Per-call override of
the global backend so a single MCP session can address Chrome and
Firefox at different moments.
Also expose two action names that the bridge already supports:
select_tab — bring a specific tab to focus (Target.activateTarget)
list_browsers — enumerate every connected provider with details
The "tab-NNN" prefix from getTargets is automatically stripped and the
numeric id forwarded; the bridge's parseTabId() handles both formats.
Tested against extension v1.8.3 which lands the matching bridge changes.
- H-1: guard negative abs_off in obj_bytes() (buffer read bypass)
- H-2: remove CERT_NONE from ZapClient TLS (MITM protection)
- H-3: add MAX_MESSAGE_SIZE check in ZapServer (OOM DoS)
- M-5: log exceptions instead of bare pass in server
- L-1: validate ZAP version in Message.parse()
- L-2: wrap req_id at u32 boundary in CloudClient
- Add CloudClient for luxfi/zap binary protocol (TCP + auto-TLS)
- Rewrite wire.py Builder/ObjectBuilder to match Rust single-buffer architecture
- Fix response field offsets (CLOUD_RESP_BODY=4, CLOUD_RESP_ERROR=12)
- Use signed i32 relative offsets from absolute positions
- 28 unit tests + cross-language integration test passing
Replace custom fastembed/sqlite-vec approach with Hanzo Cloud search
infrastructure. UI tool now has:
- ask: RAG chat about components via cloud-api.hanzo.ai/api/chat-docs
- semantic_search: hybrid fulltext+vector via cloud-api.hanzo.ai/api/search-docs
- rebuild_index: push components to Hanzo Cloud search index
- index_status: check search index stats
Uses publishable key (pk-hanzo-ui-search-2026) for reads, admin key for writes.
Same infrastructure as docs.hanzo.ai search.
Update RegistryClient to fetch from /api/registry/components.json and
/api/registry/components/{name}.json (pre-built static files), with
fallback to Next.js API routes. Works on CF Pages, GitHub Pages, and
server mode. Add lux registry URL (ui.lux.finance).
Add FastAPI registry server that pre-fetches all component data from
hanzoai/ui on GitHub, caches in memory, refreshes every 15min. Serves
instant cached responses. Deploy at ui.hanzo.ai.
- registry/cache.py: eager full-cache with concurrent GitHub fetching
- registry/server.py: FastAPI endpoints for components, blocks, search
- registry/client.py: RegistryClient matching GitHubAPIClient interface
- Dockerfile for containerized deployment
- UiTool now uses 3-tier fallback: local disk → registry → GitHub API
- Bump hanzo-tools-ui to 0.2.0, server deps optional (fastapi, uvicorn)
Add hanzo-tools-ui to workspace members and hanzo-mcp dependencies so
the ui tool is actually discoverable at runtime. Add LocalUIClient that
reads components directly from ~/work/hanzo/ui when available, falling
back to GitHub API. Adds list_packages and read_file actions. Fixes
GitHub config components_path to match actual repo layout (pkg/ui/primitives).
APIs.guru CDN blocks requests with Python's default User-Agent
(Python-urllib/3.x) from datacenter IPs, causing the weekly
Generate API Providers workflow to fail with HTTP 403.
Field(alias="json") doesn't translate to Python kwargs — FastMCP
passes the schema name directly. Rename json_body to json in the
registered function signature.
- Rewrite zap_server.py to use asyncio TCP (not websockets library)
- ZAP server now speaks native binary protocol over raw TCP
- Wire format: [magic:4][type:1][length:4 BE][JSON payload]
- Remove websockets from ide optional dependency
- Fix type annotations: any -> Any, dict -> dict[str, Any]
- Add proper frame reading with asyncio.StreamReader.readexactly()
- Support MAX_MESSAGE_SIZE (16MB) limit per ZAP spec
- Fix type shadowing in forgiving_edit.py (needle_lines int/list conflict)
- Add return type annotations to prompts functions
- Fix change_type indexing for None case in utils.py
- Fix commit.message bytes/str handling in utils.py
- Add proper dict typing for tree entries in utils.py
- Update shell tool count from 8 to 9 in test assertions
- Add handle_method parameter to ZapServer for full MCP protocol parity
- Server routes resources/list, resources/read, prompts/list, prompts/get through ZAP
- All MCP methods work identically over ZAP binary WebSocket transport
- Matches Node @hanzo/mcp v2.4.0 ZAP parity implementation
- Decode supports both MCP ZAP and hanzo/dev wire formats
- Fix _start_zap_server to use _tool_manager._tools (not .items())
- Persist auth token to ~/.hanzo/mcp_token instead of random per-run
Binary protocol server matching Node MCP implementation. Auto-starts
in background thread on run(). Supports multi-browser connections on
ports 9999-9995.
Wire up the three missing HIP-0300 unified tools:
- code (AST, symbols, definition, references, transform)
- git (status, diff, commit, branch, log)
- fetch (search, fetch, download, crawl)
These packages existed but weren't in hanzo-mcp dependencies.
Also updates their dep from hanzo-tools-core to hanzo-tools.
Bumps hanzo-mcp to 0.13.0.
The CDP bridge server now unwraps Runtime.evaluate results from the
nested CDP format {result: {type, value}} to just the value, matching
Playwright's evaluate return format.
Also bumps hanzo-tools-browser to 0.4.5 and hanzo-mcp to 0.12.9.
url, title, reload, go_back, go_forward, tab_info were not mapped
to their hanzo.* / Page.* CDP equivalents, causing "Unknown method"
errors when routed through the extension.