feat(browser-extension): ZAP-native, kill node bridge from critical path (2.0.0)

The Python hanzo-mcp now hosts the ZAP server directly. This extension
extends the shared ZAP module so Python servers can dispatch browser
RPCs back to the extension over the same socket — closing the loop on
the 2-process architecture.

- shared/zap.ts: handle inbound MSG_REQUEST and respond with
  MSG_RESPONSE. New setZapRequestHandler(mgr, fn) installs the
  dispatcher.
- background.ts (Chrome): wire cdpBridge.dispatchMethod as the inbound
  ZAP handler.
- background-firefox.ts: expose dispatchMethod and wire it as the
  inbound ZAP handler. Reorder so discoverZapServers fires AFTER the
  handler is installed.
- cdp-bridge.ts: add public dispatchMethod() helper for ZAP-server
  initiated requests.
- cdp-bridge-server.ts: deprecation header. No longer in the critical
  path; kept only for legacy non-ZAP clients.
- mcp/src/zap-server.ts: fix wire-constant mismatches with
  shared/zap.ts (MSG_RESPONSE was 0x12, must be 0x11; PING/PONG were
  0xf0/0xf1, must be 0xFE/0xFF).

Tests: 250 vitest cases pass (was 247, +3 for setZapRequestHandler).
shared-zap.test.ts now covers 31 cases (was 28).

Manifest version 2.0.0 reflects the architecture pivot; wire format
itself is unchanged from 1.9.x (still
[0x5A 0x41 0x50 0x01][type:1][length:4 BE][JSON]).
This commit is contained in:
Hanzo AI
2026-05-07 21:33:02 -07:00
parent 962bc8ce26
commit 11862defbc
11 changed files with 271 additions and 83 deletions
+36 -22
View File
@@ -1,10 +1,13 @@
# LLM.md - Hanzo Extension
## Overview
Hanzo AI Development Platform Monorepo
Hanzo AI Development Platform Monorepo. The browser extension lives in
`packages/browser` and ships to Chrome / Firefox / Safari.
## Tech Stack
- **Language**: TypeScript/JavaScript
- **Build**: esbuild via `packages/browser/src/build.js`
- **Tests**: vitest
## Build & Run
```bash
@@ -12,27 +15,38 @@ pnpm install && pnpm build
pnpm test
```
## Structure
## Architecture (since browser-extension 2.0.0)
Two-process: each Python `hanzo-mcp` hosts a ZAP server directly. The
extension discovers it on the lowest free port from
`[9999, 9998, 9997, 9996, 9995]` and dispatches commands over a binary
WebSocket frame:
```
extension/
LICENSE
LLM.md
Makefile
PUBLISHING.md
README.md
apps/
benchmark/
docs/
examples/
hanzo-ai-chrome-1.7.12.zip
hanzo-ai-firefox-1.7.12.zip
images/
package.json
packages/
pnpm-lock.yaml
[0x5A 0x41 0x50 0x01][type:1][length:4 BE][JSON payload]
```
## Key Files
- `README.md` -- Project documentation
- `package.json` -- Dependencies and scripts
- `Makefile` -- Build automation
- **shared/zap.ts** — canonical wire constants (MSG_REQUEST=0x10,
MSG_RESPONSE=0x11, MSG_PING=0xFE, MSG_PONG=0xFF). Both Chrome and
Firefox extensions use this. Includes `setZapRequestHandler(mgr, fn)`
so the Python server can dispatch RPCs back to the extension.
- **background.ts** (Chrome) — wires `cdpBridge.dispatchMethod` as the
ZAP inbound handler.
- **background-firefox.ts** — wires `HanzoFirefoxExtension.dispatchMethod`
as the ZAP inbound handler.
- **cdp-bridge-server.ts** — DEPRECATED. Legacy node bridge on
`:9223/9224`. Fallback only; not in the critical path.
Wire constants must stay locked across implementations:
- `python-sdk/pkg/hanzo-tools-browser/hanzo_tools/browser/zap_server.py`
- `extension/packages/browser/src/shared/zap.ts`
- `extension/packages/mcp/src/zap-server.ts`
If you change them, change all three and update `tests/shared-zap.test.ts`
+ `tests/test_zap_server.py::TestWireFormat::test_constants_match_extension`.
## Versioning rule
Bump patch (X.Y.Z+1) for protocol-compatible changes. Bump major (e.g.
1.x → 2.0) only when the wire format changes or the architecture pivots
(2.0.0 = ZAP becomes canonical, dropping the node bridge from default
boot). `package.json` and both `manifest*.json` must agree.
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/browser-extension",
"version": "1.8.8",
"version": "2.0.0",
"description": "Hanzo AI Browser Extension",
"main": "dist/background.js",
"scripts": {
@@ -16,14 +16,15 @@
"axios": "^1.10.0",
"chalk": "^4.1.2",
"commander": "^11.1.0",
"webextension-polyfill": "0.12.0",
"ws": "^8.18.3"
},
"devDependencies": {
"@playwright/test": "^1.52.0",
"@types/chrome": "^0.0.270",
"@types/firefox-webext-browser": "^120.0.0",
"@types/jsdom": "^21.1.7",
"@types/ws": "^8.18.1",
"@playwright/test": "^1.52.0",
"esbuild": "^0.25.6",
"jsdom": "^26.1.0",
"react": "^18.3.1",
+52 -23
View File
@@ -8,8 +8,14 @@
* - Uses `browser.*` APIs (with Promises) instead of `chrome.*` (callbacks)
* - Cannot use ES module exports at top level
* - Cannot run WebSocket servers, only clients
* - Uses `browser.tabs.executeScript` instead of `chrome.debugger`
* - MV3 109+ uses `browser.scripting.executeScript({world,func,args})`,
* NOT the removed MV2 `tabs.executeScript`. See `executeScriptWithTimeout`.
*
* The webextension-polyfill import is a no-op on Firefox (browser.* is
* native) but keeping it as the first import here mirrors the Chrome
* background's structure and avoids divergence.
*/
import 'webextension-polyfill';
import type { ControlSession, RagSnippet, RagQueryParams } from './shared/types.js';
import {
@@ -20,6 +26,7 @@ import {
zapCallTool,
hasZapTool,
handleZapMessage,
setZapRequestHandler,
type ZapManager,
} from './shared/zap.js';
import {
@@ -37,6 +44,7 @@ import {
firefoxAdapter,
type BrowserAdapter,
} from './shared/auth.js';
import { parseTabId, unwrapEvaluateResult } from './shared/tab-id.js';
// Declare browser API for TypeScript
declare const browser: typeof chrome;
@@ -66,8 +74,9 @@ const controlSession: ControlSession = {
startedAt: null,
};
// Start ZAP discovery on load (using shared module)
discoverZapServers(zapMgr, BROWSER_NAME, VERSION);
// ZAP discovery is started AFTER the extension class is instantiated
// because the inbound request handler delegates to its dispatchMethod().
// See bottom of file for the discoverZapServers() call.
// =============================================================================
@@ -124,9 +133,16 @@ class HanzoFirefoxExtension {
}
private register(): void {
// Tell the bridge server explicitly that we're Firefox so the
// `browser` parameter on multi-browser actions can route to us by
// name (e.g. `{ action: 'navigate', browser: 'firefox', url: '...' }`).
// Without this the bridge falls back to the User-Agent sniff in
// `assignClientId` which we can't observe from the SW (no navigator
// headers on a WebSocket message).
this.send({
type: 'register',
role: 'cdp-provider',
browser: BROWSER_NAME, // 'firefox'
capabilities: [
'navigate', 'screenshot', 'click', 'dblclick', 'hover', 'type',
'evaluate', 'tabs', 'fill', 'clear', 'reload', 'goBack', 'goForward',
@@ -229,7 +245,7 @@ class HanzoFirefoxExtension {
private async resolveTab(params: Record<string, unknown>): Promise<chrome.tabs.Tab> {
const rawId = params.tabId ?? params.targetId;
if (rawId !== undefined && rawId !== null) {
const id = this.parseTabId(rawId);
const id = parseTabId(rawId);
if (id !== null) {
try { return await browser.tabs.get(id); } catch { /* fall through */ }
}
@@ -244,17 +260,6 @@ class HanzoFirefoxExtension {
throw new Error('No active tab');
}
/** Fully anchored: rejects URLs / hashes / arbitrary strings even when
* they happen to end with a numeric segment. See shared/tab-id.ts. */
private parseTabId(raw: unknown): number | null {
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
if (typeof raw !== 'string' || raw === '') return null;
const m = raw.match(/^(?:tab-)?(\d+)$/);
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) ? n : null;
}
/**
* Run JS in the page via executeScript and return the serialized result.
* Wraps result to avoid undefined → {} JSON serialization issue.
@@ -293,6 +298,21 @@ class HanzoFirefoxExtension {
return result;
}
/**
* Public dispatcher for ZAP-server-initiated requests. Mirrors
* `CDPBridge.dispatchMethod` on the Chrome side so background-ZAP wiring
* is symmetric across browsers. Calls into the same canonical
* ``executeMethod`` switch already used by the legacy WS path.
*/
async dispatchMethod(method: string, params: Record<string, unknown>): Promise<any> {
const result = await this.executeMethod(method, params || {});
if (result && typeof result === 'object' && 'error' in result && result.error) {
const err: any = new Error(String(result.error));
throw err;
}
return result;
}
private async executeMethod(
method: string,
params: Record<string, unknown>
@@ -329,18 +349,16 @@ class HanzoFirefoxExtension {
}
case 'Target.closeTarget': {
const closeId = params.targetId
? parseInt((params.targetId as string).replace('tab-', ''), 10)
: params.tabId as number;
if (closeId) await browser.tabs.remove(closeId);
// Use shared parseTabId for safety: rejects URLs / strings that
// happen to end with digits (e.g. an ad URL with ?tab=123 in it).
const closeId = parseTabId(params.targetId ?? params.tabId);
if (closeId !== null) await browser.tabs.remove(closeId);
return { success: true };
}
case 'Target.activateTarget': {
const activateId = params.targetId
? parseInt((params.targetId as string).replace('tab-', ''), 10)
: params.tabId as number;
if (activateId) await browser.tabs.update(activateId, { active: true });
const activateId = parseTabId(params.targetId ?? params.tabId);
if (activateId !== null) await browser.tabs.update(activateId, { active: true });
return { success: true };
}
@@ -2333,6 +2351,17 @@ browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse:
const hanzoExtension = new HanzoFirefoxExtension();
// Install the ZAP inbound-request handler so Python hanzo-mcps can dispatch
// browser actions BACK to Firefox over the same socket. This is what
// closes the loop: the extension is no longer ZAP-client-only — it accepts
// inbound RPC and routes via the canonical executeMethod dispatcher.
setZapRequestHandler(zapMgr, async (method, params) => {
return hanzoExtension.dispatchMethod(method, (params || {}) as Record<string, unknown>);
});
// Start ZAP discovery now that the handler is installed.
discoverZapServers(zapMgr, BROWSER_NAME, VERSION);
// End active session when the controlled tab closes.
browser.tabs.onRemoved.addListener((tabId) => {
if (controlSession.active && controlSession.tabId === tabId) {
+23 -4
View File
@@ -1,10 +1,19 @@
// Background Service Worker for Browser Extension
//
// First import: webextension-polyfill. On Chrome/Edge this is a side-effect
// import that defines `globalThis.browser` as the unified Promise-returning
// WebExtension API. On Firefox/Safari `browser.*` is already native and the
// polyfill is a no-op. The shared modules (auth.ts, tab-id.ts, etc.) read
// this global so importing it once at the entrypoint is enough.
import 'webextension-polyfill';
import type { ZapState, ControlSession, RagSnippet, RagQueryParams } from './shared/types.js';
import {
createZapManager,
encodeZapMessage,
discoverZapServers,
handleZapMessage,
setZapRequestHandler,
MSG_REQUEST,
DEFAULT_ZAP_PORTS as SHARED_ZAP_PORTS,
type ZapManager,
@@ -1794,10 +1803,12 @@ function handleMCPMessage(data: any) {
// Startup
// =============================================================================
// 0. Register side panel
if (chrome.sidePanel) {
chrome.sidePanel.setOptions({ path: 'sidebar.html', enabled: true });
}
// 0. Side panel intentionally NOT registered. The user requirement is a
// right-anchored, in-page chat overlay across every browser; the native
// chrome.sidePanel surface gives an OS-managed panel which doesn't match
// that design and behaves differently from the Firefox path. Routing
// through the content-script overlay (page.overlay.* messages) is the
// single source of truth — see popup.ts.
// End active session when the controlled tab closes.
chrome.tabs.onRemoved.addListener((tabId) => {
@@ -1807,6 +1818,14 @@ chrome.tabs.onRemoved.addListener((tabId) => {
});
// 1. Primary: Discover ZAP servers (high-performance binary protocol)
// Install a request handler so Python hanzo-mcps can dispatch browser
// actions BACK to this extension over the same socket. This is what
// makes the 2-process architecture work — the extension is no longer
// a one-way client; it accepts inbound RPC and routes via the existing
// cdp-bridge dispatcher.
setZapRequestHandler(zapMgr, async (method, params) => {
return cdpBridge.dispatchMethod(method, params);
});
getPortConfig().then(({ zapPorts }) => {
discoverZapServers(zapMgr, BROWSER_NAME, VERSION, zapPorts, debugLog);
});
+13 -1
View File
@@ -1,6 +1,18 @@
#!/usr/bin/env node
/**
* CDP Bridge Server - Unified browser control interface
* CDP Bridge Server — DEPRECATED in @hanzo/browser-extension 2.0.0.
*
* The canonical 2-process architecture removes this node bridge from the
* critical path. Each Python `hanzo-mcp` now hosts a ZAP server directly
* (one of ports 9999..9995); browser extensions discover and connect to
* those MCPs without any intermediary. See
* `python-sdk/pkg/hanzo-tools-browser/hanzo_tools/browser/zap_server.py`.
*
* This file remains only as a fallback for non-ZAP MCP clients (legacy
* stdio-only consumers that still want to talk to the extension via the
* old WS+HTTP API on 9223/9224). It is no longer auto-launched. Do not
* extend it for new functionality — extend the Python ZAP server and the
* extension's `shared/zap.ts` instead.
*
* Provides hanzo.browser(action, params) interface matching hanzo-mcp pattern.
* Browser extension connects to this server for remote control.
+58 -19
View File
@@ -1,6 +1,7 @@
// CDP Bridge for hanzo-mcp Browser Tool Integration
// Enables Playwright to control browser via Chrome DevTools Protocol
import { ActionRateLimiter, debugLog, loadDebugFlagFromStorage } from './runtime-guard';
import { parseTabId, unwrapEvaluateResult } from './shared/tab-id.js';
interface CDPSession {
tabId: number;
@@ -164,27 +165,40 @@ export class CDPBridge {
debugLog(`[CDP] Page.captureScreenshot failed, using captureVisibleTab fallback: ${e}`);
}
// Fallback: use chrome.tabs.captureVisibleTab (no debugger needed)
// Fallback: chrome.tabs.captureVisibleTab. This API only works on a
// *foreground* tab in the target window — Firefox enforces this strictly,
// Chrome will sometimes succeed with a backgrounded tab but is
// inconsistent on Mac. Brief-activate the target tab if needed and
// restore focus afterwards. Always pass an explicit windowId resolved
// from the tab so multi-window automation captures the right window.
const tab = await chrome.tabs.get(tabId);
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
format: options?.format === 'jpeg' ? 'jpeg' : 'png',
quality: options?.quality,
});
let activatedFor: number | null = null;
let prevActiveId: number | undefined;
if (!tab.active) {
const prev = await new Promise<chrome.tabs.Tab | undefined>((resolve) => {
chrome.tabs.query({ active: true, windowId: tab.windowId }, (tabs) => resolve(tabs[0]));
});
prevActiveId = prev?.id;
await chrome.tabs.update(tabId, { active: true });
activatedFor = tabId;
// One animation tick so the tab actually composes.
await new Promise((r) => setTimeout(r, 50));
}
let dataUrl: string;
try {
dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
format: options?.format === 'jpeg' ? 'jpeg' : 'png',
quality: options?.quality,
});
} finally {
if (activatedFor !== null && prevActiveId) {
chrome.tabs.update(prevActiveId, { active: true }).catch(() => {});
}
}
// Strip data:image/png;base64, prefix
return dataUrl.replace(/^data:image\/\w+;base64,/, '');
}
/** Accept tabId in any of: number | "tab-NNN" | "NNN" | undefined.
* Fully anchored so "https://x.com/?tab-123" can't be coerced. */
private parseTabId(raw: unknown): number | null {
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
if (typeof raw !== 'string' || raw === '') return null;
const m = raw.match(/^(?:tab-)?(\d+)$/);
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) ? n : null;
}
async click(tabId: number, x: number, y: number): Promise<void> {
await this.send(tabId, 'Input.dispatchMouseEvent', {
type: 'mousePressed',
@@ -218,11 +232,18 @@ export class CDPBridge {
async evaluate(tabId: number, expression: string): Promise<any> {
await this.send(tabId, 'Runtime.enable');
// awaitPromise: true makes CDP wait for any async IIFE the caller passed
// to resolve before returning. Without this, expressions like
// (async () => fetch('/x').then(r => r.json()))()
// serialize to {} (a Promise placeholder) and the caller can't tell
// success from failure. unwrapEvaluateResult further normalises every
// shape (Chrome / CDP / hanzo-tools) to the bare value.
const result = await this.send(tabId, 'Runtime.evaluate', {
expression,
returnByValue: true
returnByValue: true,
awaitPromise: true,
});
return result.result?.value;
return unwrapEvaluateResult(result);
}
async getDocument(tabId: number): Promise<any> {
@@ -365,6 +386,24 @@ export class CDPBridge {
}
}
/**
* Public entry point for ZAP-server-initiated requests.
*
* Wraps {@link handleBridgeMessage} so background scripts can hand off
* ``method`` + ``params`` from a ZAP MSG_REQUEST without synthesising a
* fake numeric ``id``. Throws on bridge error so the caller can convert
* to a MSG_RESPONSE error frame.
*/
async dispatchMethod(method: string, params: any): Promise<any> {
const response = await this.handleBridgeMessage({ id: 0, method, params: params || {} });
if (response.error) {
const err: any = new Error(response.error.message);
err.code = response.error.code;
throw err;
}
return response.result;
}
private async handleBridgeMessage(message: any): Promise<CDPResponse> {
const { id, method, params } = message;
@@ -375,7 +414,7 @@ export class CDPBridge {
// getTargets, plain numeric string. Without explicit targeting fall
// back to the active tab in the focused window — which is fragile when
// the user has many windows open. Always prefer explicit ids.
const tabId = this.parseTabId(params?.tabId ?? params?.targetId)
const tabId = parseTabId(params?.tabId ?? params?.targetId)
?? await this.getActiveTabId();
switch (method) {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.8.8",
"version": "2.0.0",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
+1 -5
View File
@@ -1,12 +1,11 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.8.8",
"version": "2.0.0",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
"identity",
"sidePanel",
"storage",
"tabs",
"webNavigation",
@@ -45,9 +44,6 @@
"128": "icon128.png"
}
},
"side_panel": {
"default_path": "sidebar.html"
},
"icons": {
"16": "icon16.png",
"48": "icon48.png",
+41
View File
@@ -50,11 +50,24 @@ export function decodeZapMessage(buf: ArrayBuffer): { type: number; payload: any
// ── Connection Management ───────────────────────────────────────────────
/**
* Handler invoked when a ZAP server pushes a request TO the extension.
* Returns a result (or throws) and the framework converts that into a
* MSG_RESPONSE frame back to the server.
*
* Keeping this at the manager level means the extension's existing
* request-dispatchers (handleExtensionRequest, executeMethod) can be
* mounted once and serve every connected MCP without duplication.
*/
export type ZapRequestHandler = (method: string, params: any) => Promise<any> | any;
export interface ZapManager {
state: ZapState;
connections: Map<string, WebSocket>;
pendingRequests: Map<string, { resolve: Function; reject: Function }>;
requestIdCounter: number;
/** Optional inbound request handler (set via setZapRequestHandler). */
requestHandler?: ZapRequestHandler;
}
export function createZapManager(): ZapManager {
@@ -70,6 +83,11 @@ export function createZapManager(): ZapManager {
};
}
/** Install (or replace) the inbound request handler. */
export function setZapRequestHandler(mgr: ZapManager, handler: ZapRequestHandler | undefined): void {
mgr.requestHandler = handler;
}
/** Probe a ZAP server on given port */
export function probeZapServer(
port: number,
@@ -162,6 +180,29 @@ export async function connectZap(
}
break;
}
case MSG_REQUEST: {
// Server-initiated RPC: a Python hanzo-mcp is asking the
// extension to perform a browser action. Dispatch via the
// installed handler and reply with MSG_RESPONSE.
const { id: reqId, method: reqMethod, params: reqParams } = msg.payload || {};
const respond = (payload: object) => {
try {
ws.send(encodeZapMessage(MSG_RESPONSE, payload));
} catch (e) {
// ws.send throws if the socket is closing; nothing we can do.
}
};
const handler = mgr.requestHandler;
if (!handler) {
respond({ id: reqId, error: { code: -32601, message: `No request handler installed (method: ${reqMethod})` } });
break;
}
Promise.resolve()
.then(() => handler(reqMethod, reqParams || {}))
.then((result) => respond({ id: reqId, result: result === undefined ? null : result }))
.catch((e: any) => respond({ id: reqId, error: { code: -1, message: e?.message || String(e) } }));
break;
}
case MSG_PING:
ws.send(encodeZapMessage(MSG_PONG, {}));
break;
+33
View File
@@ -5,6 +5,7 @@ import {
createZapManager,
hasZapTool,
handleZapMessage,
setZapRequestHandler,
zapCallTool,
zapListResources,
zapReadResource,
@@ -286,3 +287,35 @@ describe('handleZapMessage', () => {
}
});
});
// ---------------------------------------------------------------------------
// setZapRequestHandler — closes the loop for ZAP-server-initiated RPC
// (the 2-process architecture where Python hanzo-mcps push browser
// actions TO the extension over the same socket).
// ---------------------------------------------------------------------------
describe('setZapRequestHandler', () => {
it('attaches the handler onto the manager', () => {
const mgr = createZapManager();
expect(mgr.requestHandler).toBeUndefined();
const handler = async () => ({ ok: true });
setZapRequestHandler(mgr, handler);
expect(mgr.requestHandler).toBe(handler);
});
it('replaces an existing handler', () => {
const mgr = createZapManager();
const a = async () => 'a';
const b = async () => 'b';
setZapRequestHandler(mgr, a);
setZapRequestHandler(mgr, b);
expect(mgr.requestHandler).toBe(b);
});
it('clears the handler when undefined', () => {
const mgr = createZapManager();
setZapRequestHandler(mgr, async () => null);
setZapRequestHandler(mgr, undefined);
expect(mgr.requestHandler).toBeUndefined();
});
});
+10 -6
View File
@@ -51,12 +51,16 @@ function zapDecode(data: Uint8Array): { type: number; payload: any } | null {
// ── Protocol Constants ────────────────────────────────────────────────
const MSG_HANDSHAKE = 0x01; // Init
const MSG_HANDSHAKE_OK = 0x02; // InitAck
const MSG_REQUEST = 0x10; // Push
const MSG_RESPONSE = 0x12; // Resolve
const MSG_PING = 0xf0;
const MSG_PONG = 0xf1;
// Wire constants must match @hanzo/browser-extension/src/shared/zap.ts.
// Earlier drafts used 0x12/0xf0/0xf1 — those were wrong and silently broke
// extension <-> mcp sessions. Canonical values live in shared/zap.ts and
// are pinned by tests.
const MSG_HANDSHAKE = 0x01;
const MSG_HANDSHAKE_OK = 0x02;
const MSG_REQUEST = 0x10;
const MSG_RESPONSE = 0x11;
const MSG_PING = 0xFE;
const MSG_PONG = 0xFF;
const ZAP_PORTS = [9999, 9998, 9997, 9996, 9995];
const SERVER_ID = `mcp-${Date.now().toString(36)}`;