fix(zap): dual-protocol decode, fix tool extraction, persist auth token

- 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
This commit is contained in:
2026-03-11 13:26:46 -07:00
parent 1ff7bfc10f
commit 3aba26db16
2 changed files with 62 additions and 28 deletions
+31 -16
View File
@@ -173,18 +173,30 @@ class HanzoMCPServer:
except Exception:
self.mcp._mcp_server.version = "0.12.5"
# Initialize authentication token
# Initialize authentication token — check env, then ~/.hanzo/mcp_token
self.auth_token = auth_token or os.environ.get("HANZO_MCP_TOKEN")
if not self.auth_token:
# Generate a secure random token if none provided
token_path = os.path.expanduser("~/.hanzo/mcp_token")
try:
if os.path.exists(token_path):
with open(token_path) as f:
self.auth_token = f.read().strip()
except OSError:
pass
if not self.auth_token:
# Generate and persist to ~/.hanzo/mcp_token
self.auth_token = secrets.token_urlsafe(32)
token_path = os.path.expanduser("~/.hanzo/mcp_token")
try:
os.makedirs(os.path.dirname(token_path), exist_ok=True)
with open(token_path, "w") as f:
f.write(self.auth_token)
os.chmod(token_path, 0o600)
except OSError:
pass
logger = logging.getLogger(__name__)
logger.warning(
f"No auth token provided. Generated token: {self.auth_token}"
)
logger.warning(
"Set HANZO_MCP_TOKEN environment variable for persistent auth"
)
logger.info(f"Auth token persisted to {token_path}")
# Initialize permissions and command executor
PermissionManager = _get_permission_manager()
@@ -345,13 +357,16 @@ class HanzoMCPServer:
# Collect tool manifest from registered MCP tools
tool_list: list[dict] = []
try:
# FastMCP stores tools internally — extract names and schemas
for name, tool in getattr(self.mcp, "_tool_manager", {}).items():
tool_list.append({
"name": name,
"description": getattr(tool, "description", ""),
"inputSchema": getattr(tool, "parameters", {}),
})
# FastMCP _tool_manager._tools is a dict[str, Tool]
tm = getattr(self.mcp, "_tool_manager", None)
if tm is not None:
tools_dict = getattr(tm, "_tools", {})
for name, tool in tools_dict.items():
tool_list.append({
"name": name,
"description": getattr(tool, "description", ""),
"inputSchema": getattr(tool, "parameters", {}),
})
except Exception:
pass
@@ -363,7 +378,7 @@ class HanzoMCPServer:
tool_list.append({
"name": t.name,
"description": t.description or "",
"inputSchema": getattr(t, "inputSchema", {}),
"inputSchema": getattr(t, "parameters", {}),
})
except Exception:
pass
+31 -12
View File
@@ -44,26 +44,45 @@ def encode(msg_type: int, payload: Any) -> bytes:
def decode(buf: bytes) -> tuple[int, Any] | None:
"""Decode a ZAP message. Returns (type, payload) or None."""
if len(buf) < HEADER_SIZE:
"""Decode a ZAP message. Supports both MCP ZAP and hanzo/dev wire formats.
Format 1 (MCP ZAP): [magic:4 "ZAP\\x01"][type:1][length:4 BE][JSON]
Format 2 (hanzo/dev): [length:4 LE][type:1][payload]
Format 3: Plain JSON fallback
"""
if len(buf) < 5:
return None
if buf[:4] != ZAP_MAGIC:
# Not ZAP binary — try plain JSON fallback
# Format 1: MCP ZAP — magic header + big-endian length
if buf[:4] == ZAP_MAGIC:
if len(buf) < HEADER_SIZE:
return None
msg_type = buf[4]
length = struct.unpack("!L", buf[5:9])[0]
if len(buf) < HEADER_SIZE + length:
return None
try:
payload = json.loads(buf.decode("utf-8"))
return (MSG_REQUEST, payload)
payload = json.loads(buf[HEADER_SIZE : HEADER_SIZE + length].decode("utf-8"))
return (msg_type, payload)
except (json.JSONDecodeError, UnicodeDecodeError):
return None
msg_type = buf[4]
length = struct.unpack("!L", buf[5:9])[0]
if len(buf) < HEADER_SIZE + length:
return None
# Format 2: hanzo/dev ZAP — little-endian length, no magic
le_length = struct.unpack("<L", buf[:4])[0]
if 0 < le_length <= 16 * 1024 * 1024 and len(buf) >= 5 + le_length:
msg_type = buf[4]
if msg_type <= 0x45 or msg_type >= 0xFE:
payload_buf = buf[5 : 5 + le_length]
try:
payload = json.loads(payload_buf.decode("utf-8"))
return (msg_type, payload)
except (json.JSONDecodeError, UnicodeDecodeError):
return (msg_type, {"raw": payload_buf})
# Format 3: Plain JSON fallback
try:
payload = json.loads(buf[HEADER_SIZE : HEADER_SIZE + length].decode("utf-8"))
return (msg_type, payload)
payload = json.loads(buf.decode("utf-8"))
return (MSG_REQUEST, payload)
except (json.JSONDecodeError, UnicodeDecodeError):
return None