Skip to content

Commit 83719f0

Browse files
committed
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
1 parent 8d73eaf commit 83719f0

File tree

2 files changed

+62
-28
lines changed

2 files changed

+62
-28
lines changed

pkg/hanzo-mcp/hanzo_mcp/server.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -173,18 +173,30 @@ def __init__(
173173
except Exception:
174174
self.mcp._mcp_server.version = "0.12.5"
175175

176-
# Initialize authentication token
176+
# Initialize authentication token — check env, then ~/.hanzo/mcp_token
177177
self.auth_token = auth_token or os.environ.get("HANZO_MCP_TOKEN")
178178
if not self.auth_token:
179-
# Generate a secure random token if none provided
179+
token_path = os.path.expanduser("~/.hanzo/mcp_token")
180+
try:
181+
if os.path.exists(token_path):
182+
with open(token_path) as f:
183+
self.auth_token = f.read().strip()
184+
except OSError:
185+
pass
186+
187+
if not self.auth_token:
188+
# Generate and persist to ~/.hanzo/mcp_token
180189
self.auth_token = secrets.token_urlsafe(32)
190+
token_path = os.path.expanduser("~/.hanzo/mcp_token")
191+
try:
192+
os.makedirs(os.path.dirname(token_path), exist_ok=True)
193+
with open(token_path, "w") as f:
194+
f.write(self.auth_token)
195+
os.chmod(token_path, 0o600)
196+
except OSError:
197+
pass
181198
logger = logging.getLogger(__name__)
182-
logger.warning(
183-
f"No auth token provided. Generated token: {self.auth_token}"
184-
)
185-
logger.warning(
186-
"Set HANZO_MCP_TOKEN environment variable for persistent auth"
187-
)
199+
logger.info(f"Auth token persisted to {token_path}")
188200

189201
# Initialize permissions and command executor
190202
PermissionManager = _get_permission_manager()
@@ -345,13 +357,16 @@ def _start_zap_server(self) -> None:
345357
# Collect tool manifest from registered MCP tools
346358
tool_list: list[dict] = []
347359
try:
348-
# FastMCP stores tools internally — extract names and schemas
349-
for name, tool in getattr(self.mcp, "_tool_manager", {}).items():
350-
tool_list.append({
351-
"name": name,
352-
"description": getattr(tool, "description", ""),
353-
"inputSchema": getattr(tool, "parameters", {}),
354-
})
360+
# FastMCP _tool_manager._tools is a dict[str, Tool]
361+
tm = getattr(self.mcp, "_tool_manager", None)
362+
if tm is not None:
363+
tools_dict = getattr(tm, "_tools", {})
364+
for name, tool in tools_dict.items():
365+
tool_list.append({
366+
"name": name,
367+
"description": getattr(tool, "description", ""),
368+
"inputSchema": getattr(tool, "parameters", {}),
369+
})
355370
except Exception:
356371
pass
357372

@@ -363,7 +378,7 @@ def _start_zap_server(self) -> None:
363378
tool_list.append({
364379
"name": t.name,
365380
"description": t.description or "",
366-
"inputSchema": getattr(t, "inputSchema", {}),
381+
"inputSchema": getattr(t, "parameters", {}),
367382
})
368383
except Exception:
369384
pass

pkg/hanzo-mcp/hanzo_mcp/zap_server.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,45 @@ def encode(msg_type: int, payload: Any) -> bytes:
4444

4545

4646
def decode(buf: bytes) -> tuple[int, Any] | None:
47-
"""Decode a ZAP message. Returns (type, payload) or None."""
48-
if len(buf) < HEADER_SIZE:
47+
"""Decode a ZAP message. Supports both MCP ZAP and hanzo/dev wire formats.
48+
49+
Format 1 (MCP ZAP): [magic:4 "ZAP\\x01"][type:1][length:4 BE][JSON]
50+
Format 2 (hanzo/dev): [length:4 LE][type:1][payload]
51+
Format 3: Plain JSON fallback
52+
"""
53+
if len(buf) < 5:
4954
return None
5055

51-
if buf[:4] != ZAP_MAGIC:
52-
# Not ZAP binary — try plain JSON fallback
56+
# Format 1: MCP ZAP — magic header + big-endian length
57+
if buf[:4] == ZAP_MAGIC:
58+
if len(buf) < HEADER_SIZE:
59+
return None
60+
msg_type = buf[4]
61+
length = struct.unpack("!L", buf[5:9])[0]
62+
if len(buf) < HEADER_SIZE + length:
63+
return None
5364
try:
54-
payload = json.loads(buf.decode("utf-8"))
55-
return (MSG_REQUEST, payload)
65+
payload = json.loads(buf[HEADER_SIZE : HEADER_SIZE + length].decode("utf-8"))
66+
return (msg_type, payload)
5667
except (json.JSONDecodeError, UnicodeDecodeError):
5768
return None
5869

59-
msg_type = buf[4]
60-
length = struct.unpack("!L", buf[5:9])[0]
61-
if len(buf) < HEADER_SIZE + length:
62-
return None
70+
# Format 2: hanzo/dev ZAP — little-endian length, no magic
71+
le_length = struct.unpack("<L", buf[:4])[0]
72+
if 0 < le_length <= 16 * 1024 * 1024 and len(buf) >= 5 + le_length:
73+
msg_type = buf[4]
74+
if msg_type <= 0x45 or msg_type >= 0xFE:
75+
payload_buf = buf[5 : 5 + le_length]
76+
try:
77+
payload = json.loads(payload_buf.decode("utf-8"))
78+
return (msg_type, payload)
79+
except (json.JSONDecodeError, UnicodeDecodeError):
80+
return (msg_type, {"raw": payload_buf})
6381

82+
# Format 3: Plain JSON fallback
6483
try:
65-
payload = json.loads(buf[HEADER_SIZE : HEADER_SIZE + length].decode("utf-8"))
66-
return (msg_type, payload)
84+
payload = json.loads(buf.decode("utf-8"))
85+
return (MSG_REQUEST, payload)
6786
except (json.JSONDecodeError, UnicodeDecodeError):
6887
return None
6988

0 commit comments

Comments
 (0)