hanzo-tools-browser: vendor ZAP wire constants, advertise on bind host

Two fixes for the canonical ZAP server:

1. Vendor the wire format (ZAP_MAGIC, MSG_*, MAX_MESSAGE_SIZE,
   encode/decode) inside hanzo_tools/browser/zap_server.py instead of
   importing from the external ``zap.protocol`` module — the published
   ``zap-protocol`` package on PyPI is now a ``zap-schema`` stub that
   doesn't expose those symbols, so the import chain breaks for any
   fresh install. Inlining ~40 lines keeps the spec self-contained and
   matches the TS reference at extension/packages/browser/src/shared/zap.ts.

2. Pass ``host=self.host`` to ``zap_mdns.publish`` so the advertised
   address matches the bound address. Default behaviour advertised the
   outbound LAN IP via ``_local_ip()`` while the WebSocket was bound to
   127.0.0.1; clients dialled the LAN IP and got ECONNREFUSED.

Drops the broken ``zap-protocol>=0.2.1`` dependency from both
hanzo-mcp and hanzo-tools-browser pyprojects — no longer needed.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
Antje Worring
2026-05-18 22:24:47 -07:00
co-authored by Hanzo Dev
parent 8637133b22
commit ffc7159b45
3 changed files with 47 additions and 19 deletions
-2
View File
@@ -19,8 +19,6 @@ keywords = ["mcp", "claude", "hanzo", "code", "agent"]
dependencies = [
# Hanzo AI core SDK (canonical types, config, session, MCP client)
"hanzoai>=2.2.0",
# ZAP native protocol
"zap-protocol>=0.2.1",
# Core MCP dependencies
"mcp>=1.25.0",
"fastmcp>=2.14.4",
@@ -24,22 +24,48 @@ from typing import Any, Awaitable, Callable, Optional
logger = logging.getLogger(__name__)
# Wire format — single source of truth is the `zap-protocol` package
# (~/work/zap/zap-py). Import the constants instead of redefining them so
# any future change (e.g. MSG_STREAM, MAX_MESSAGE_SIZE) lands here for free.
from zap.protocol import (
ZAP_MAGIC,
HEADER_SIZE,
MAX_MESSAGE_SIZE,
MSG_HANDSHAKE,
MSG_HANDSHAKE_OK,
MSG_REQUEST,
MSG_RESPONSE,
MSG_PING,
MSG_PONG,
encode,
decode,
)
# Wire format — vendored here. Mirrors the TypeScript reference at
# extension/packages/browser/src/shared/zap.ts. Kept self-contained so a
# vanilla `pip install hanzo-tools-browser` works without external state
# (an earlier `zap-protocol` PyPI package was a `zap-schema` stub that
# doesn't expose `zap.protocol`; rather than chase that, the wire spec is
# small enough to inline). If you change anything here, also update the
# TS reference and `extension/packages/mcp/src/zap-server.ts`.
import json as _json
import struct as _struct
from typing import Tuple as _Tuple
ZAP_MAGIC = b"\x5a\x41\x50\x01"
HEADER_SIZE = 9 # 4 magic + 1 type + 4 length BE
MAX_MESSAGE_SIZE = 16 * 1024 * 1024 # 16 MiB
MSG_HANDSHAKE = 0x01
MSG_HANDSHAKE_OK = 0x02
MSG_REQUEST = 0x10
MSG_RESPONSE = 0x11
MSG_PING = 0xFE
MSG_PONG = 0xFF
def encode(msg_type: int, payload) -> bytes:
body = _json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
if len(body) > MAX_MESSAGE_SIZE:
raise ValueError(f"ZAP payload exceeds MAX_MESSAGE_SIZE ({len(body)} > {MAX_MESSAGE_SIZE})")
return ZAP_MAGIC + _struct.pack("!BL", msg_type & 0xFF, len(body)) + body
def decode(frame: bytes):
if len(frame) < HEADER_SIZE or frame[:4] != ZAP_MAGIC:
return None
msg_type = frame[4]
(length,) = _struct.unpack("!L", frame[5:9])
if length > MAX_MESSAGE_SIZE or len(frame) < HEADER_SIZE + length:
return None
try:
payload = _json.loads(frame[HEADER_SIZE : HEADER_SIZE + length].decode("utf-8"))
except (_json.JSONDecodeError, UnicodeDecodeError):
return None
return msg_type, payload
# Browser-resolution preference (mirror cdp-bridge-server.ts)
DEFAULT_BROWSER_PREFERENCE: list[str] = ["firefox", "safari", "edge", "chrome"]
@@ -195,6 +221,11 @@ class ZapServer:
agent_label=self.agent_label or "",
version="zap/1",
capabilities=["mcp", "browser-bridge"],
# Advertise the bind address so the URL clients receive
# actually reaches this server. zap-mdns defaults to the
# outbound LAN IP via _local_ip(), which is wrong when we
# bind loopback (browser extension dials LAN IP → ECONNREFUSED).
host=self.host,
)
logger.info("mDNS published %s on :%d", zap_mdns.SERVICE_TYPE, self._port)
except Exception as e:
-1
View File
@@ -18,7 +18,6 @@ dependencies = [
"aiohttp>=3.9.0",
"websockets>=12.0",
"zap-mdns>=0.1.0",
"zap-protocol>=0.2.1",
]
[project.optional-dependencies]