feat(zap-server): auto-publish via hanzo-zap-mdns + retract on stop

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.
This commit is contained in:
2026-05-08 12:10:26 -07:00
parent 41bdfa3644
commit 65cb749fa6
@@ -327,6 +327,29 @@ class ZapServer:
self.server_id,
self.agent_label or "?",
)
# Best-effort mDNS publish so LAN-wide consumers can find us
# without hard-coded ports. No-op if `hanzo-zap-mdns` (which
# owns the zeroconf dep) isn't installed.
self._mdns_handle = None
try:
import hanzo_zap_mdns
# zeroconf's register_service blocks the event loop briefly
# while it sets up the multicast socket; from inside an
# asyncio coroutine that triggers EventLoopBlocked. Run on
# a worker thread so it never sees our loop.
self._mdns_handle = await asyncio.to_thread(
hanzo_zap_mdns.publish,
port=port,
server_id=self.server_id,
agent_label=self.agent_label or "",
version="zap/1",
capabilities=["mcp", "browser-bridge"],
)
logger.info("mDNS published _hanzo-zap._tcp.local. on :%d", port)
except ImportError:
logger.debug("hanzo-zap-mdns not installed; mDNS publish skipped")
except Exception as _e:
logger.warning("mDNS publish failed: %s: %s", type(_e).__name__, _e)
return port
logger.warning("ZAP: no free port in %s", self.ports)
@@ -334,6 +357,13 @@ class ZapServer:
async def stop(self) -> None:
"""Gracefully shut down: close clients, release lock, drop registry entry."""
# Retract mDNS announcement first so consumers see us go away.
try:
if getattr(self, "_mdns_handle", None) is not None:
self._mdns_handle.close()
self._mdns_handle = None
except Exception:
pass
if self._server is not None:
self._server.close()
try: