mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user