feat: add REPL, IDE tools and ZAP protocol

- hanzo-tools-repl: Multi-language REPL with Jupyter kernel backend
  - Supports Python, Node.js/TypeScript, Bash, Ruby, Go, Rust
  - Persistent sessions for stateful evaluation
  - History and completion support

- hanzo-tools-ide: IDE integration for VS Code, Cursor, JetBrains
  - Full editor control (open, edit, navigate, refactor)
  - Terminal integration
  - Diagnostics and quick fixes
  - WebSocket bridge to IDE extensions

- ZAP Protocol (proto/zap/):
  - MCP superset for agentic operations
  - Streaming, REPL sessions, IDE integration
  - Browser DevTools support
  - Extension bridge specification

- hanzo-mcp v0.11.7:
  - Added [repl], [ide], [interactive] extras
This commit is contained in:
2026-01-22 15:47:55 -08:00
parent 53c7597f41
commit 5636caf58c
14 changed files with 2397 additions and 1 deletions
+20 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hanzo-mcp"
version = "0.11.6"
version = "0.11.7"
description = "The Zen of Hanzo MCP: One server to rule them all. The ultimate MCP that orchestrates all others."
readme = "README.md"
requires-python = ">=3.12"
@@ -68,6 +68,25 @@ memory = [
"fastembed>=0.4.0",
]
# Interactive REPL with multi-language Jupyter kernels
repl = [
"hanzo-tools-repl>=0.1.0",
"jupyter-client>=8.6.0",
"ipykernel>=6.29.0",
]
# IDE integration (VS Code, Cursor, JetBrains)
ide = [
"hanzo-tools-ide>=0.1.0",
"websockets>=12.0",
]
# Full interactive suite (REPL + IDE + Browser)
interactive = [
"hanzo-mcp[repl,ide]",
"hanzo-tools-browser>=0.2.1",
]
# Development
dev = [
"pytest>=7.0.0",
+84
View File
@@ -0,0 +1,84 @@
# hanzo-tools-ide
IDE integration tool for VS Code, Cursor, Windsurf, JetBrains, and LSP-compatible editors.
## Features
- **Multi-IDE support**: VS Code, Cursor, Windsurf, JetBrains, Neovim
- **Full editor control**: Open, edit, navigate, refactor
- **Terminal integration**: Create and send commands to terminals
- **Diagnostics**: Access errors, warnings, and quick fixes
- **Agent-friendly**: Designed for AI agent workflows
## Installation
```bash
pip install hanzo-tools-ide
```
## Requirements
Install the Hanzo extension in your IDE:
- **VS Code/Cursor/Windsurf**: Install "Hanzo AI" extension
- **JetBrains**: Install "Hanzo AI" plugin
- **Neovim**: Install hanzo.nvim plugin
## Usage
```python
from hanzo_tools.ide import IdeTool
ide = IdeTool()
# Connect to IDE (auto-detects)
await ide.call(action="connect")
# Open file
await ide.call(action="open", path="/src/main.py", line=42)
# Insert text
await ide.call(action="insert", text="# TODO: fix", line=10)
# Go to definition
await ide.call(action="go_to_definition", line=15, column=8)
# Rename symbol
await ide.call(action="rename", new_name="betterName", line=10, column=5)
# Run in terminal
await ide.call(action="terminal", command="npm test")
# Get diagnostics
await ide.call(action="diagnostics", severity="error")
# Execute VS Code command
await ide.call(action="command", command="editor.action.formatDocument")
```
## Actions
| Action | Description | Parameters |
|--------|-------------|------------|
| connect | Connect to IDE | ide?: vscode\|cursor\|jetbrains |
| status | Check connection | - |
| open | Open file | path, line? |
| close | Close file | path? |
| save | Save file | path? |
| files | List open files | - |
| select | Set selection | line, column, end_line?, end_column? |
| insert | Insert text | text, line, column? |
| replace | Replace text | text, line, column, end_line, end_column |
| get_text | Get text | line?, end_line? |
| go_to_definition | Navigate | line, column |
| find_references | Find refs | line, column |
| rename | Rename symbol | new_name, line, column |
| format | Format doc | - |
| diagnostics | Get errors | severity? |
| quick_fix | Apply fix | line, column, index? |
| command | Execute cmd | command, args? |
| terminal | Terminal | command?, name? |
| search | Search | query, include?, exclude? |
## License
MIT - Hanzo Industries Inc
@@ -0,0 +1,3 @@
# Namespace package - see PEP 420
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
@@ -0,0 +1,24 @@
"""
hanzo-tools-ide: IDE integration for VS Code, JetBrains, and LSP editors.
Provides AI agents with direct control over IDEs:
- File operations (open, save, close)
- Editor operations (select, insert, replace)
- Navigation (go to definition, find references)
- Refactoring (rename, extract, format)
- Terminal control (create, send commands)
- Diagnostics (errors, warnings, quick fixes)
Usage:
ide(action="open", path="/path/to/file.py")
ide(action="insert", text="# comment", line=10)
ide(action="go_to_definition", line=15, column=8)
ide(action="rename", new_name="better_name", line=10, column=5)
ide(action="terminal", command="npm test")
"""
from .ide_tool import IdeTool, IdeConnection
TOOLS = [IdeTool()]
__all__ = ["IdeTool", "IdeConnection", "TOOLS"]
@@ -0,0 +1,515 @@
"""
IDE Integration Tool for VS Code, JetBrains, and LSP-compatible editors.
Architecture:
- VS Code: Connect via Hanzo extension (WebSocket on port 9225)
- JetBrains: Connect via Gateway protocol
- Neovim: Connect via RPC socket
- Generic: Connect via LSP
VS Code Extension API surfaces used:
- vscode.window: Active editor, terminals, notifications
- vscode.workspace: Files, folders, configuration
- vscode.commands: Execute any VS Code command
- vscode.languages: Diagnostics, completion, hover
The Hanzo VS Code extension acts as a bridge, exposing these APIs
via WebSocket for external AI agent control.
"""
from __future__ import annotations
import asyncio
import json
import uuid
import logging
from dataclasses import dataclass, field
from typing import Any
from enum import Enum
try:
import aiohttp
AIOHTTP_AVAILABLE = True
except ImportError:
AIOHTTP_AVAILABLE = False
try:
import websockets
WEBSOCKETS_AVAILABLE = True
except ImportError:
WEBSOCKETS_AVAILABLE = False
from hanzo_tools.core import BaseTool
logger = logging.getLogger(__name__)
class IdeType(str, Enum):
"""Supported IDE types."""
VSCODE = "vscode"
JETBRAINS = "jetbrains"
NEOVIM = "neovim"
CURSOR = "cursor"
WINDSURF = "windsurf"
GENERIC = "generic"
@dataclass
class IdeConnection:
"""Connection state for an IDE."""
ide_type: IdeType
endpoint: str
websocket: Any = None
connected: bool = False
workspace_path: str | None = None
active_file: str | None = None
pending_requests: dict[str, asyncio.Future] = field(default_factory=dict)
async def connect(self) -> bool:
"""Establish connection to IDE."""
if not WEBSOCKETS_AVAILABLE:
logger.warning("websockets not available")
return False
try:
self.websocket = await websockets.connect(
self.endpoint,
ping_interval=30,
ping_timeout=10,
)
self.connected = True
# Start message receiver
asyncio.create_task(self._receive_loop())
return True
except Exception as e:
logger.debug(f"Failed to connect to {self.ide_type}: {e}")
return False
async def disconnect(self) -> None:
"""Close connection."""
if self.websocket:
await self.websocket.close()
self.connected = False
async def send(self, action: str, **params: Any) -> dict[str, Any]:
"""Send request and wait for response."""
if not self.connected or not self.websocket:
return {"error": "Not connected to IDE"}
request_id = str(uuid.uuid4())
message = {
"id": request_id,
"action": action,
"params": params,
}
future: asyncio.Future[dict] = asyncio.get_event_loop().create_future()
self.pending_requests[request_id] = future
try:
await self.websocket.send(json.dumps(message))
result = await asyncio.wait_for(future, timeout=30.0)
return result
except asyncio.TimeoutError:
return {"error": "Request timed out"}
finally:
self.pending_requests.pop(request_id, None)
async def _receive_loop(self) -> None:
"""Receive messages from IDE."""
try:
async for message in self.websocket:
data = json.loads(message)
request_id = data.get("id")
if request_id and request_id in self.pending_requests:
self.pending_requests[request_id].set_result(data)
else:
# Event notification
await self._handle_event(data)
except Exception as e:
logger.debug(f"Receive loop error: {e}")
self.connected = False
async def _handle_event(self, data: dict) -> None:
"""Handle event notification from IDE."""
event_type = data.get("event")
if event_type == "activeEditorChanged":
self.active_file = data.get("path")
elif event_type == "workspaceChanged":
self.workspace_path = data.get("path")
class IdeManager:
"""Manages IDE connections."""
_instance: IdeManager | None = None
def __init__(self) -> None:
self.connections: dict[IdeType, IdeConnection] = {}
self.default_ide: IdeType | None = None
@classmethod
def get_instance(cls) -> IdeManager:
"""Get singleton instance."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
async def connect(self, ide_type: IdeType = IdeType.VSCODE) -> IdeConnection:
"""Connect to an IDE."""
if ide_type in self.connections and self.connections[ide_type].connected:
return self.connections[ide_type]
# Default endpoints for each IDE type
endpoints = {
IdeType.VSCODE: "ws://localhost:9225",
IdeType.CURSOR: "ws://localhost:9226",
IdeType.WINDSURF: "ws://localhost:9227",
IdeType.JETBRAINS: "ws://localhost:63342/api/hanzo",
IdeType.NEOVIM: "ws://localhost:9228",
}
endpoint = endpoints.get(ide_type, endpoints[IdeType.VSCODE])
conn = IdeConnection(ide_type=ide_type, endpoint=endpoint)
if await conn.connect():
self.connections[ide_type] = conn
if self.default_ide is None:
self.default_ide = ide_type
return conn
return conn
async def auto_connect(self) -> IdeConnection | None:
"""Auto-detect and connect to available IDE."""
for ide_type in [IdeType.VSCODE, IdeType.CURSOR, IdeType.WINDSURF]:
conn = await self.connect(ide_type)
if conn.connected:
return conn
return None
def get_connection(self, ide_type: IdeType | None = None) -> IdeConnection | None:
"""Get existing connection."""
ide_type = ide_type or self.default_ide
if ide_type:
return self.connections.get(ide_type)
return None
class IdeTool(BaseTool):
"""
IDE integration tool for controlling VS Code, JetBrains, and other editors.
Connects to the Hanzo IDE extension (VS Code, Cursor, Windsurf) via WebSocket.
Provides full control over editor operations for AI agents.
Actions:
- connect: Connect to IDE (auto-detects VS Code, Cursor, Windsurf)
- status: Check connection status
- open: Open file in editor
- close: Close file
- save: Save current file
- files: List open files
- select: Set selection in editor
- insert: Insert text at position
- replace: Replace text in range
- get_text: Get text from file/selection
- go_to_definition: Navigate to symbol definition
- find_references: Find all references to symbol
- rename: Rename symbol across workspace
- format: Format document
- diagnostics: Get errors and warnings
- quick_fix: Apply quick fix
- command: Execute VS Code command
- terminal: Create/send to terminal
- search: Search in workspace
"""
name = "ide"
@property
def description(self) -> str:
return """IDE control for VS Code, Cursor, Windsurf, and JetBrains.
ACTIONS:
- connect: Connect to IDE (ide?: vscode|cursor|windsurf|jetbrains)
- status: Check connection status
- open: Open file (path: string, line?: int)
- close: Close file (path?: string)
- save: Save file (path?: string)
- files: List open files
- select: Set selection (line: int, column: int, end_line?: int, end_column?: int)
- insert: Insert text (text: string, line: int, column?: int)
- replace: Replace text (text: string, line: int, column: int, end_line: int, end_column: int)
- get_text: Get text (line?: int, end_line?: int)
- go_to_definition: Go to definition (line: int, column: int)
- find_references: Find references (line: int, column: int)
- rename: Rename symbol (new_name: string, line: int, column: int)
- format: Format document
- diagnostics: Get diagnostics (severity?: error|warning|info)
- quick_fix: Apply quick fix (line: int, column: int, index?: int)
- command: Execute command (command: string, args?: list)
- terminal: Terminal (command?: string, name?: string)
- search: Search workspace (query: string, include?: string, exclude?: string)
EXAMPLES:
ide(action="connect")
ide(action="open", path="/src/main.py", line=42)
ide(action="insert", text="# TODO: fix this", line=10)
ide(action="rename", new_name="better_name", line=15, column=8)
ide(action="terminal", command="npm test")
ide(action="command", command="workbench.action.toggleSidebarVisibility")"""
def __init__(self) -> None:
super().__init__()
self.manager = IdeManager.get_instance()
async def call(
self,
action: str = "status",
ide: str | None = None,
path: str | None = None,
line: int | None = None,
column: int | None = None,
end_line: int | None = None,
end_column: int | None = None,
text: str | None = None,
new_name: str | None = None,
command: str | None = None,
args: list[Any] | None = None,
query: str | None = None,
include: str | None = None,
exclude: str | None = None,
name: str | None = None,
severity: str | None = None,
index: int | None = None,
**kwargs: Any,
) -> str:
"""Execute IDE action."""
action = action.lower()
# Connection management
if action == "connect":
return await self._connect(ide)
elif action == "status":
return self._status()
# Get connection
ide_type = IdeType(ide) if ide else None
conn = self.manager.get_connection(ide_type)
if not conn or not conn.connected:
# Try auto-connect
conn = await self.manager.auto_connect()
if not conn or not conn.connected:
return "Not connected to IDE. Use ide(action='connect') first or install Hanzo IDE extension."
# File operations
if action == "open":
if not path:
return "Error: path required for open action"
result = await conn.send("openFile", path=path, line=line)
return self._format_result(result, f"Opened {path}")
elif action == "close":
result = await conn.send("closeFile", path=path)
return self._format_result(result, "File closed")
elif action == "save":
result = await conn.send("saveFile", path=path)
return self._format_result(result, "File saved")
elif action == "files":
result = await conn.send("listOpenFiles")
return self._format_result(result)
# Editor operations
elif action == "select":
if line is None:
return "Error: line required for select action"
result = await conn.send(
"setSelection",
line=line,
column=column or 0,
endLine=end_line,
endColumn=end_column,
)
return self._format_result(result, "Selection set")
elif action == "insert":
if text is None or line is None:
return "Error: text and line required for insert action"
result = await conn.send(
"insertText",
text=text,
line=line,
column=column or 0,
)
return self._format_result(result, "Text inserted")
elif action == "replace":
if text is None:
return "Error: text required for replace action"
result = await conn.send(
"replaceText",
text=text,
line=line,
column=column,
endLine=end_line,
endColumn=end_column,
)
return self._format_result(result, "Text replaced")
elif action in ("get_text", "text"):
result = await conn.send(
"getText",
line=line,
endLine=end_line,
)
return self._format_result(result)
# Navigation
elif action in ("go_to_definition", "definition"):
if line is None or column is None:
return "Error: line and column required"
result = await conn.send(
"goToDefinition",
line=line,
column=column,
)
return self._format_result(result)
elif action in ("find_references", "references"):
if line is None or column is None:
return "Error: line and column required"
result = await conn.send(
"findReferences",
line=line,
column=column,
)
return self._format_result(result)
# Refactoring
elif action == "rename":
if new_name is None or line is None or column is None:
return "Error: new_name, line, and column required"
result = await conn.send(
"rename",
newName=new_name,
line=line,
column=column,
)
return self._format_result(result, f"Renamed to {new_name}")
elif action == "format":
result = await conn.send("formatDocument")
return self._format_result(result, "Document formatted")
# Diagnostics
elif action == "diagnostics":
result = await conn.send("getDiagnostics", severity=severity)
return self._format_result(result)
elif action == "quick_fix":
if line is None or column is None:
return "Error: line and column required"
result = await conn.send(
"applyQuickFix",
line=line,
column=column,
index=index or 0,
)
return self._format_result(result, "Quick fix applied")
# Commands
elif action == "command":
if not command:
return "Error: command required"
result = await conn.send(
"executeCommand",
command=command,
args=args or [],
)
return self._format_result(result)
# Terminal
elif action == "terminal":
if command:
result = await conn.send(
"sendToTerminal",
command=command,
name=name,
)
else:
result = await conn.send(
"createTerminal",
name=name,
)
return self._format_result(result)
# Search
elif action == "search":
if not query:
return "Error: query required"
result = await conn.send(
"searchWorkspace",
query=query,
include=include,
exclude=exclude,
)
return self._format_result(result)
else:
return f"Unknown action: {action}"
async def _connect(self, ide: str | None) -> str:
"""Connect to IDE."""
ide_type = IdeType(ide) if ide else None
if ide_type:
conn = await self.manager.connect(ide_type)
else:
conn = await self.manager.auto_connect()
if conn and conn.connected:
return f"Connected to {conn.ide_type.value}"
return "Failed to connect. Ensure Hanzo IDE extension is installed and running."
def _status(self) -> str:
"""Get connection status."""
if not self.manager.connections:
return "No IDE connections"
lines = ["IDE Connections:"]
for ide_type, conn in self.manager.connections.items():
status = "connected" if conn.connected else "disconnected"
default = " (default)" if ide_type == self.manager.default_ide else ""
lines.append(f" {ide_type.value}: {status}{default}")
if conn.workspace_path:
lines.append(f" Workspace: {conn.workspace_path}")
if conn.active_file:
lines.append(f" Active: {conn.active_file}")
return "\n".join(lines)
def _format_result(self, result: dict, success_msg: str | None = None) -> str:
"""Format result from IDE."""
if "error" in result:
return f"Error: {result['error']}"
if success_msg and result.get("success", True):
return success_msg
# Format result data
data = result.get("data") or result.get("result")
if data:
if isinstance(data, str):
return data
return json.dumps(data, indent=2)
return str(result)
+43
View File
@@ -0,0 +1,43 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hanzo-tools-ide"
version = "0.1.0"
description = "IDE integration tool for VS Code, JetBrains, and LSP-compatible editors"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo Industries Inc", email = "[email protected]" }]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
keywords = ["mcp", "ide", "vscode", "jetbrains", "lsp", "editor"]
dependencies = [
"hanzo-tools-core>=0.1.0",
"aiohttp>=3.9.0",
"websockets>=12.0",
]
[project.optional-dependencies]
lsp = ["pygls>=1.3.0"]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.23.0",
]
[project.urls]
"Homepage" = "https://github.com/hanzoai/python-sdk"
[project.entry-points."hanzo.tools"]
ide = "hanzo_tools.ide:TOOLS"
[tool.setuptools.packages.find]
where = ["."]
include = ["hanzo_tools*"]
[tool.setuptools.package-data]
hanzo_tools = ["py.typed"]
+62
View File
@@ -0,0 +1,62 @@
# hanzo-tools-repl
Multi-language REPL tool with Jupyter kernel backend for interactive code evaluation.
## Features
- **Multi-language support**: Python, Node.js/TypeScript, Bash, Ruby, Go, Rust
- **Persistent sessions**: Maintain state across evaluations
- **Jupyter kernels**: Uses standard Jupyter infrastructure
- **Agent-friendly**: Designed for AI agent workflows
## Installation
```bash
pip install hanzo-tools-repl
# With all kernels
pip install hanzo-tools-repl[full]
```
## Usage
```python
from hanzo_tools.repl import ReplTool
repl = ReplTool()
# Start a Python session
await repl.call(action="start", language="python")
# Evaluate code
result = await repl.call(action="eval", code="x = 1 + 1\nprint(x)")
# Output: [1] 2
# Node.js/TypeScript
await repl.call(action="start", language="node")
result = await repl.call(action="eval", code="const arr = [1,2,3].map(n => n*2)")
# List sessions
await repl.call(action="list")
# Get history
await repl.call(action="history", limit=10)
# Stop session
await repl.call(action="stop")
```
## Supported Languages
| Language | Kernel | Install |
|----------|--------|---------|
| Python | ipykernel | `pip install ipykernel` |
| Node.js/TypeScript | tslab | `npm install -g tslab && tslab install` |
| Bash | bash_kernel | `pip install bash_kernel` |
| Ruby | iruby | `gem install iruby` |
| Go | gophernotes | See gophernotes docs |
| Rust | evcxr | `cargo install evcxr_jupyter` |
## License
MIT - Hanzo Industries Inc
@@ -0,0 +1,3 @@
# Namespace package - see PEP 420
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
@@ -0,0 +1,23 @@
"""
hanzo-tools-repl: Multi-language REPL with Jupyter kernel backend.
Provides interactive code evaluation for agents across multiple languages:
- Python (ipykernel)
- Node.js/TypeScript (tslab)
- Bash (bash_kernel)
- And any language with a Jupyter kernel
Usage:
repl(action="start", language="python") # Start kernel
repl(action="eval", code="print('hello')") # Evaluate code
repl(action="eval", code="const x = 1", language="typescript")
repl(action="history") # Get history
repl(action="stop") # Stop kernel
repl(action="list") # List kernels
"""
from .repl_tool import ReplTool, KernelManager
TOOLS = [ReplTool()]
__all__ = ["ReplTool", "KernelManager", "TOOLS"]
@@ -0,0 +1,495 @@
"""
Unified REPL Tool with Multi-Language Jupyter Kernel Support.
Provides persistent interactive sessions for code evaluation across languages.
Uses Jupyter kernels as backend for consistent cross-language experience.
"""
from __future__ import annotations
import asyncio
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
# Jupyter client imports
try:
from jupyter_client import KernelManager as JupyterKernelManager
from jupyter_client.asynchronous import AsyncKernelClient
JUPYTER_AVAILABLE = True
except ImportError:
JUPYTER_AVAILABLE = False
JupyterKernelManager = None
AsyncKernelClient = None
from hanzo_tools.core import BaseTool
# Language to kernel mapping
LANGUAGE_KERNELS: dict[str, str] = {
# Python variants
"python": "python3",
"python3": "python3",
"py": "python3",
"ipython": "python3",
# JavaScript/TypeScript
"javascript": "tslab",
"js": "tslab",
"typescript": "tslab",
"ts": "tslab",
"node": "tslab",
# Shell
"bash": "bash",
"sh": "bash",
"shell": "bash",
"zsh": "bash",
# Other languages (require kernel installation)
"ruby": "ruby",
"go": "gophernotes",
"rust": "evcxr",
"julia": "julia-1.10",
"r": "ir",
}
@dataclass
class ExecutionResult:
"""Result of code execution."""
success: bool
output: str
error: str | None = None
execution_count: int = 0
data: dict[str, Any] = field(default_factory=dict)
duration_ms: float = 0
@dataclass
class KernelSession:
"""Represents an active kernel session."""
id: str
language: str
kernel_name: str
manager: Any # JupyterKernelManager
client: Any # AsyncKernelClient
created_at: datetime = field(default_factory=datetime.now)
execution_count: int = 0
history: list[tuple[str, ExecutionResult]] = field(default_factory=list)
class KernelManager:
"""Manages multiple Jupyter kernel sessions."""
_instance: KernelManager | None = None
def __init__(self) -> None:
self.sessions: dict[str, KernelSession] = {}
self.default_session: str | None = None
self._lock = asyncio.Lock()
@classmethod
def get_instance(cls) -> KernelManager:
"""Get singleton instance."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
async def start_kernel(
self,
language: str = "python",
session_id: str | None = None,
) -> KernelSession:
"""Start a new kernel session."""
if not JUPYTER_AVAILABLE:
raise RuntimeError(
"jupyter-client not installed. "
"Install with: pip install hanzo-tools-repl[full]"
)
kernel_name = LANGUAGE_KERNELS.get(language.lower(), language)
session_id = session_id or f"{language}_{uuid.uuid4().hex[:8]}"
async with self._lock:
if session_id in self.sessions:
return self.sessions[session_id]
# Create kernel manager
km = JupyterKernelManager(kernel_name=kernel_name)
km.start_kernel()
# Get async client
client = km.client()
client.start_channels()
# Wait for kernel ready
await asyncio.sleep(0.5)
session = KernelSession(
id=session_id,
language=language,
kernel_name=kernel_name,
manager=km,
client=client,
)
self.sessions[session_id] = session
if self.default_session is None:
self.default_session = session_id
return session
async def execute(
self,
code: str,
session_id: str | None = None,
timeout: float = 30.0,
) -> ExecutionResult:
"""Execute code in a kernel session."""
session_id = session_id or self.default_session
if not session_id or session_id not in self.sessions:
# Auto-start default Python kernel
session = await self.start_kernel("python")
session_id = session.id
session = self.sessions[session_id]
start_time = datetime.now()
try:
# Execute code
msg_id = session.client.execute(code)
output_parts: list[str] = []
error_output: str | None = None
data: dict[str, Any] = {}
# Collect results
while True:
try:
msg = await asyncio.wait_for(
asyncio.to_thread(
session.client.get_iopub_msg,
timeout=timeout
),
timeout=timeout
)
except asyncio.TimeoutError:
break
msg_type = msg.get("msg_type", "")
content = msg.get("content", {})
if msg_type == "stream":
output_parts.append(content.get("text", ""))
elif msg_type == "execute_result":
data = content.get("data", {})
if "text/plain" in data:
output_parts.append(data["text/plain"])
elif msg_type == "display_data":
data.update(content.get("data", {}))
if "text/plain" in data:
output_parts.append(data["text/plain"])
elif msg_type == "error":
error_output = "\n".join(content.get("traceback", []))
elif msg_type == "status":
if content.get("execution_state") == "idle":
break
duration = (datetime.now() - start_time).total_seconds() * 1000
session.execution_count += 1
result = ExecutionResult(
success=error_output is None,
output="".join(output_parts),
error=error_output,
execution_count=session.execution_count,
data=data,
duration_ms=duration,
)
# Add to history
session.history.append((code, result))
return result
except Exception as e:
return ExecutionResult(
success=False,
output="",
error=str(e),
)
async def stop_kernel(self, session_id: str | None = None) -> bool:
"""Stop a kernel session."""
session_id = session_id or self.default_session
if not session_id or session_id not in self.sessions:
return False
async with self._lock:
session = self.sessions.pop(session_id)
session.client.stop_channels()
session.manager.shutdown_kernel()
if self.default_session == session_id:
self.default_session = next(iter(self.sessions), None)
return True
async def stop_all(self) -> int:
"""Stop all kernel sessions."""
count = len(self.sessions)
for session_id in list(self.sessions.keys()):
await self.stop_kernel(session_id)
return count
def list_sessions(self) -> list[dict[str, Any]]:
"""List all active sessions."""
return [
{
"id": s.id,
"language": s.language,
"kernel": s.kernel_name,
"created": s.created_at.isoformat(),
"executions": s.execution_count,
"is_default": s.id == self.default_session,
}
for s in self.sessions.values()
]
def get_history(
self,
session_id: str | None = None,
limit: int = 20,
) -> list[dict[str, Any]]:
"""Get execution history for a session."""
session_id = session_id or self.default_session
if not session_id or session_id not in self.sessions:
return []
session = self.sessions[session_id]
history = session.history[-limit:]
return [
{
"index": i + 1,
"code": code,
"success": result.success,
"output": result.output[:500] if result.output else None,
"error": result.error[:200] if result.error else None,
}
for i, (code, result) in enumerate(history)
]
@staticmethod
def list_available_kernels() -> list[dict[str, str]]:
"""List available Jupyter kernels."""
if not JUPYTER_AVAILABLE:
return []
try:
from jupyter_client.kernelspec import find_kernel_specs
specs = find_kernel_specs()
return [
{"name": name, "path": path}
for name, path in specs.items()
]
except Exception:
return []
class ReplTool(BaseTool):
"""
Multi-language REPL with Jupyter kernel backend.
Provides interactive code evaluation for agents across languages:
- Python, Node.js/TypeScript, Bash, and any Jupyter kernel
Actions:
- start: Start a new kernel session
- eval: Execute code in a session
- stop: Stop a kernel session
- list: List active sessions
- history: Get execution history
- kernels: List available kernels
Examples:
repl(action="start", language="python")
repl(action="eval", code="x = 1 + 1; print(x)")
repl(action="eval", code="console.log('hello')", language="node")
repl(action="history", limit=10)
repl(action="stop")
"""
name = "repl"
@property
def description(self) -> str:
return """Multi-language REPL with persistent Jupyter kernel sessions.
ACTIONS:
- start: Start kernel (language: python|node|typescript|bash|ruby|go|rust)
- eval: Execute code (code: string, language?: string, session?: string)
- stop: Stop kernel (session?: string)
- list: List active sessions
- history: Get execution history (session?: string, limit?: int)
- kernels: List available Jupyter kernels
LANGUAGES: python, node/javascript/typescript, bash/shell, ruby, go, rust
EXAMPLES:
repl(action="start", language="python")
repl(action="eval", code="print('hello')")
repl(action="eval", code="const x = [1,2,3].map(n => n*2)", language="node")
repl(action="history")
repl(action="stop")"""
def __init__(self) -> None:
super().__init__()
self.manager = KernelManager.get_instance()
async def call(
self,
action: str = "eval",
code: str | None = None,
language: str = "python",
session: str | None = None,
limit: int = 20,
timeout: float = 30.0,
**kwargs: Any,
) -> str:
"""Execute REPL action."""
action = action.lower()
if action == "start":
return await self._start(language, session)
elif action in ("eval", "execute", "run"):
if not code:
return "Error: code parameter required for eval action"
return await self._eval(code, language, session, timeout)
elif action == "stop":
return await self._stop(session)
elif action == "list":
return self._list()
elif action == "history":
return self._history(session, limit)
elif action == "kernels":
return self._kernels()
else:
return f"Unknown action: {action}. Use: start, eval, stop, list, history, kernels"
async def _start(self, language: str, session_id: str | None) -> str:
"""Start a new kernel session."""
try:
session = await self.manager.start_kernel(language, session_id)
return f"""Kernel started:
Session: {session.id}
Language: {session.language}
Kernel: {session.kernel_name}
Use repl(action="eval", code="...") to execute code."""
except Exception as e:
return f"Error starting kernel: {e}"
async def _eval(
self,
code: str,
language: str,
session_id: str | None,
timeout: float,
) -> str:
"""Execute code in a session."""
# Auto-start kernel if needed
if not self.manager.sessions:
await self.manager.start_kernel(language)
elif session_id is None and language != "python":
# Start language-specific kernel if not exists
lang_sessions = [
s for s in self.manager.sessions.values()
if s.language.lower() == language.lower()
]
if not lang_sessions:
await self.manager.start_kernel(language)
result = await self.manager.execute(code, session_id, timeout)
if result.success:
output = result.output or "(no output)"
return f"""[{result.execution_count}] {output}"""
else:
return f"""[{result.execution_count}] Error:
{result.error}"""
async def _stop(self, session_id: str | None) -> str:
"""Stop a kernel session."""
if session_id == "all":
count = await self.manager.stop_all()
return f"Stopped {count} kernel(s)"
success = await self.manager.stop_kernel(session_id)
if success:
return f"Kernel stopped: {session_id or 'default'}"
return "No active kernel to stop"
def _list(self) -> str:
"""List active sessions."""
sessions = self.manager.list_sessions()
if not sessions:
return "No active REPL sessions"
lines = ["Active REPL sessions:"]
for s in sessions:
default = " (default)" if s["is_default"] else ""
lines.append(
f" {s['id']}: {s['language']} ({s['kernel']}) "
f"- {s['executions']} executions{default}"
)
return "\n".join(lines)
def _history(self, session_id: str | None, limit: int) -> str:
"""Get execution history."""
history = self.manager.get_history(session_id, limit)
if not history:
return "No execution history"
lines = ["Execution history:"]
for h in history:
status = "OK" if h["success"] else "ERR"
code_preview = h["code"][:50].replace("\n", "\\n")
if len(h["code"]) > 50:
code_preview += "..."
lines.append(f" [{h['index']}] {status}: {code_preview}")
return "\n".join(lines)
def _kernels(self) -> str:
"""List available kernels."""
kernels = self.manager.list_available_kernels()
if not kernels:
return "No Jupyter kernels found. Install with: pip install ipykernel"
lines = ["Available Jupyter kernels:"]
for k in kernels:
lines.append(f" {k['name']}")
lines.append("\nSupported language aliases:")
for lang, kernel in sorted(set((v, k) for k, v in LANGUAGE_KERNELS.items())):
aliases = [k for k, v in LANGUAGE_KERNELS.items() if v == lang]
lines.append(f" {lang}: {', '.join(aliases)}")
return "\n".join(lines)
+51
View File
@@ -0,0 +1,51 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hanzo-tools-repl"
version = "0.1.0"
description = "Multi-language REPL tool with Jupyter kernel backend for interactive code evaluation"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo Industries Inc", email = "[email protected]" }]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
keywords = ["mcp", "repl", "jupyter", "interactive", "code", "evaluation"]
dependencies = [
"hanzo-tools-core>=0.1.0",
"jupyter-client>=8.6.0",
"jupyter-core>=5.7.0",
]
[project.optional-dependencies]
kernels = [
"ipykernel>=6.29.0", # Python kernel
"bash_kernel>=0.9.0", # Bash kernel
"typescript-kernel>=0.1.0", # TypeScript (via ts-node)
]
node = ["tslab>=1.0.0"] # Node.js/TypeScript Jupyter kernel
full = [
"hanzo-tools-repl[kernels,node]",
]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.23.0",
]
[project.urls]
"Homepage" = "https://github.com/hanzoai/python-sdk"
[project.entry-points."hanzo.tools"]
repl = "hanzo_tools.repl:TOOLS"
[tool.setuptools.packages.find]
where = ["."]
include = ["hanzo_tools*"]
[tool.setuptools.package-data]
hanzo_tools = ["py.typed"]
+164
View File
@@ -0,0 +1,164 @@
# ZAP Protocol: MCP Superset for Agentic Operations
ZAP (Zero-latency Agent Protocol) extends MCP with capabilities for:
- **Interactive Sessions**: REPL/notebook execution across languages
- **Streaming**: Real-time output for long-running operations
- **IDE Integration**: Full editor control (VS Code, JetBrains, Neovim)
- **Browser DevTools**: Console, network, debugger control
- **Extension Bridge**: Browser/IDE extension communication
## Protocol Versions
| Version | MCP Compat | Features |
|---------|------------|----------|
| 1.0.0 | 2025-06-18 | Full MCP + REPL + Browser + IDE |
## Wire Formats
- **Primary**: Cap'n Proto (binary, zero-copy)
- **Fallback**: JSON-RPC 2.0 (MCP compatible)
## Message Types
### Tool Execution (MCP Compatible)
```json
{
"id": "1",
"method": "tools/call",
"params": {
"name": "read",
"arguments": {"file_path": "/path/to/file"}
}
}
```
### REPL Session
```json
{
"id": "1",
"method": "repl/eval",
"params": {
"session_id": "py_abc123",
"language": "python",
"code": "x = 1 + 1\nprint(x)",
"stream_output": true
}
}
```
### Browser Console
```json
{
"id": "1",
"method": "browser/console",
"params": {
"action": "evaluate",
"code": "document.title"
}
}
```
### IDE Control
```json
{
"id": "1",
"method": "ide/command",
"params": {
"action": "rename",
"line": 10,
"column": 5,
"new_name": "betterName"
}
}
```
### Streaming
```json
{
"id": "1",
"method": "stream/start",
"params": {
"type": "repl_output",
"session_id": "py_abc123"
}
}
```
## Capabilities
### REPL Languages
- Python (ipykernel)
- Node.js/TypeScript (tslab)
- Bash (bash_kernel)
- Ruby, Go, Rust, Julia (with kernels)
### Browser Actions
- JavaScript evaluation
- Console message capture
- Network interception
- DOM manipulation
- Debugger control
### IDE Actions
- File operations (open, save, close)
- Editor operations (select, insert, replace)
- Navigation (go to definition, find references)
- Refactoring (rename, format, extract)
- Terminal control
- Diagnostics and quick fixes
## Extension Bridge
ZAP communicates with browser/IDE extensions via WebSocket:
| Extension | Port | Protocol |
|-----------|------|----------|
| Browser | 9224 | HTTP/WS |
| VS Code | 9225 | WebSocket |
| Cursor | 9226 | WebSocket |
| Windsurf | 9227 | WebSocket |
| Neovim | 9228 | WebSocket |
## Proto Files
- `zap.proto` - Main protocol definition
- Generated code available for:
- Python: `pip install hanzo-zap`
- TypeScript: `npm install @hanzo/zap`
- Go: `go get github.com/hanzoai/zap`
- Rust: `cargo add hanzo-zap`
## Usage with hanzo-mcp
ZAP is fully integrated into hanzo-mcp tools:
```python
# REPL evaluation
repl(action="eval", code="print('hello')", language="python")
# Browser console
browser(action="console")
browser(action="evaluate", code="document.title")
# IDE control
ide(action="open", path="/src/main.py")
ide(action="rename", new_name="newFunc", line=10, column=5)
```
## Compared to MCP
| Feature | MCP | ZAP |
|---------|-----|-----|
| Tool calls | ✓ | ✓ |
| Resources | ✓ | ✓ |
| Prompts | ✓ | ✓ |
| Streaming | ✗ | ✓ |
| REPL sessions | ✗ | ✓ |
| IDE integration | ✗ | ✓ |
| Browser DevTools | ✗ | ✓ |
| Binary protocol | ✗ | ✓ (Cap'n Proto) |
| Extension bridge | ✗ | ✓ |
## License
MIT - Hanzo Industries Inc
+258
View File
@@ -0,0 +1,258 @@
# ZAP Extension Bridge Protocol
The ZAP Extension Bridge enables AI agents to control browsers and IDEs through native extensions.
## Architecture
```
┌─────────────────┐ WebSocket ┌──────────────────┐
│ hanzo-mcp │ ◄──────────────────► │ Browser Extension │
│ (AI Agent) │ Port 9224 │ (Chrome/Firefox) │
└─────────────────┘ └──────────────────┘
│
│ WebSocket
│ ◄──────────────────────────► ┌──────────────────┐
│ Port 9225 │ VS Code Extension │
│ └──────────────────┘
│
│ WebSocket
└──────────────────────────────► ┌──────────────────┐
Port 9228 │ Neovim Plugin │
└──────────────────┘
```
## Browser Extension
### Installation
- Chrome: [Chrome Web Store - Hanzo AI](https://chrome.google.com/webstore/detail/hanzo-ai)
- Firefox: [Firefox Add-ons - Hanzo AI](https://addons.mozilla.org/firefox/addon/hanzo-ai)
### Exposed APIs
#### Console
```typescript
// Subscribe to console messages
{ action: "console.subscribe" }
// Evaluate in console
{ action: "console.eval", code: "1 + 1" }
// Get console history
{ action: "console.history", level?: "error" | "warn" | "log" }
```
#### DOM
```typescript
// Get accessibility snapshot
{ action: "dom.snapshot" }
// Query selector
{ action: "dom.query", selector: ".button" }
// Click element
{ action: "dom.click", selector: "#submit" }
```
#### Network
```typescript
// Subscribe to network events
{ action: "network.subscribe" }
// Intercept requests
{ action: "network.intercept", pattern: "*/api/*" }
// Mock response
{ action: "network.mock", url: "/api/user", response: { name: "Test" } }
```
#### DevTools
```typescript
// Set breakpoint
{ action: "debugger.breakpoint", url: "app.js", line: 42 }
// Step through code
{ action: "debugger.step" }
// Get call stack
{ action: "debugger.stack" }
```
## VS Code Extension
### Installation
```bash
code --install-extension hanzo.hanzo-ai
```
### Exposed APIs
#### Files
```typescript
// Open file
{ action: "file.open", path: "/src/main.ts", line?: 42 }
// Save file
{ action: "file.save", path?: "/src/main.ts" }
// Close file
{ action: "file.close", path?: "/src/main.ts" }
// List open files
{ action: "file.list" }
```
#### Editor
```typescript
// Get selection
{ action: "editor.selection" }
// Set selection
{ action: "editor.select", line: 10, column: 5, endLine: 10, endColumn: 15 }
// Insert text
{ action: "editor.insert", text: "// TODO", line: 10 }
// Replace text
{ action: "editor.replace", text: "newCode", line: 10, column: 0, endLine: 12, endColumn: 0 }
```
#### Navigation
```typescript
// Go to definition
{ action: "nav.definition", line: 10, column: 5 }
// Find references
{ action: "nav.references", line: 10, column: 5 }
// Go to symbol
{ action: "nav.symbol", query: "MyClass" }
```
#### Refactoring
```typescript
// Rename symbol
{ action: "refactor.rename", line: 10, column: 5, newName: "betterName" }
// Format document
{ action: "refactor.format" }
// Organize imports
{ action: "refactor.imports" }
// Extract function
{ action: "refactor.extract", type: "function", line: 10, endLine: 20 }
```
#### Diagnostics
```typescript
// Get diagnostics
{ action: "diagnostics.get", severity?: "error" | "warning" }
// Apply quick fix
{ action: "diagnostics.fix", line: 10, column: 5, index?: 0 }
```
#### Terminal
```typescript
// Create terminal
{ action: "terminal.create", name?: "Build" }
// Send to terminal
{ action: "terminal.send", command: "npm test", name?: "Build" }
// Get terminal output
{ action: "terminal.output", name?: "Build" }
```
#### Commands
```typescript
// Execute VS Code command
{ action: "command.execute", command: "workbench.action.togglePanel" }
// List available commands
{ action: "command.list", filter?: "workbench" }
```
## Message Format
All messages use JSON-RPC 2.0:
### Request
```json
{
"jsonrpc": "2.0",
"id": "unique-id",
"method": "action",
"params": { ... }
}
```
### Response
```json
{
"jsonrpc": "2.0",
"id": "unique-id",
"result": { ... }
}
```
### Error
```json
{
"jsonrpc": "2.0",
"id": "unique-id",
"error": {
"code": -32600,
"message": "Invalid Request"
}
}
```
### Event (no id)
```json
{
"jsonrpc": "2.0",
"method": "event",
"params": {
"type": "console.message",
"data": { "level": "log", "text": "Hello" }
}
}
```
## Security
- Extensions only accept connections from localhost
- Optional authentication via shared secret
- Sandboxed execution (no arbitrary code without user consent)
- Rate limiting on sensitive operations
- User must enable "AI Control" in extension settings
## Development
### Building Browser Extension
```bash
cd extensions/browser
npm install
npm run build
# Load unpacked extension from dist/
```
### Building VS Code Extension
```bash
cd extensions/vscode
npm install
npm run build
code --install-extension hanzo-ai-*.vsix
```
### Testing
```bash
# Run integration tests
npm test
# Manual testing
curl -X POST http://localhost:9224 \
-H "Content-Type: application/json" \
-d '{"action": "status"}'
```
+652
View File
@@ -0,0 +1,652 @@
// ZAP Protocol: MCP Superset for Agentic Operations
// Version: 1.0.0
//
// ZAP extends MCP (Model Context Protocol) with:
// - Streaming capabilities for real-time output
// - Interactive REPL sessions across languages
// - IDE/Editor integration primitives
// - Browser DevTools integration
// - Extension messaging protocol
//
// Wire format: Cap'n Proto (primary), JSON-RPC (fallback)
syntax = "proto3";
package zap.v1;
option go_package = "github.com/hanzoai/zap/proto/zap/v1";
// ============================================================================
// Core Message Types
// ============================================================================
// Base request envelope
message ZapRequest {
string id = 1;
string method = 2;
oneof params {
ToolCallParams tool_call = 10;
ReplParams repl = 11;
BrowserParams browser = 12;
IdeParams ide = 13;
ExtensionParams extension = 14;
StreamParams stream = 15;
}
map<string, string> metadata = 100;
}
// Base response envelope
message ZapResponse {
string id = 1;
oneof result {
ToolResult tool_result = 10;
ReplResult repl_result = 11;
BrowserResult browser_result = 12;
IdeResult ide_result = 13;
ExtensionResult extension_result = 14;
StreamChunk stream_chunk = 15;
ErrorResult error = 99;
}
map<string, string> metadata = 100;
}
message ErrorResult {
int32 code = 1;
string message = 2;
string data = 3;
}
// ============================================================================
// Tool Execution (MCP Compatible)
// ============================================================================
message ToolCallParams {
string name = 1;
map<string, Value> arguments = 2;
}
message ToolResult {
bool success = 1;
repeated ContentBlock content = 2;
bool is_error = 3;
}
message ContentBlock {
oneof content {
TextContent text = 1;
ImageContent image = 2;
ResourceContent resource = 3;
BinaryContent binary = 4;
}
}
message TextContent {
string text = 1;
string mime_type = 2; // text/plain, text/markdown, application/json
}
message ImageContent {
bytes data = 1;
string mime_type = 2; // image/png, image/jpeg
string alt_text = 3;
}
message ResourceContent {
string uri = 1;
string mime_type = 2;
bytes data = 3;
}
message BinaryContent {
bytes data = 1;
string mime_type = 2;
}
// Generic value for arguments
message Value {
oneof value {
string string_value = 1;
int64 int_value = 2;
double double_value = 3;
bool bool_value = 4;
ListValue list_value = 5;
MapValue map_value = 6;
bytes bytes_value = 7;
}
}
message ListValue {
repeated Value values = 1;
}
message MapValue {
map<string, Value> fields = 1;
}
// ============================================================================
// REPL / Interactive Sessions
// ============================================================================
message ReplParams {
ReplAction action = 1;
string session_id = 2;
string language = 3;
string code = 4;
int32 timeout_ms = 5;
bool stream_output = 6;
}
enum ReplAction {
REPL_ACTION_UNSPECIFIED = 0;
REPL_ACTION_START = 1; // Start new kernel session
REPL_ACTION_EVAL = 2; // Execute code
REPL_ACTION_STOP = 3; // Stop session
REPL_ACTION_INTERRUPT = 4; // Interrupt execution
REPL_ACTION_COMPLETE = 5; // Tab completion
REPL_ACTION_INSPECT = 6; // Inspect object
REPL_ACTION_HISTORY = 7; // Get history
REPL_ACTION_LIST = 8; // List sessions
}
message ReplResult {
string session_id = 1;
bool success = 2;
int32 execution_count = 3;
string output = 4;
string error = 5;
repeated ReplOutput outputs = 6;
repeated CompletionItem completions = 7;
InspectResult inspect = 8;
}
message ReplOutput {
ReplOutputType type = 1;
string text = 2;
bytes data = 3;
string mime_type = 4;
}
enum ReplOutputType {
REPL_OUTPUT_UNSPECIFIED = 0;
REPL_OUTPUT_STDOUT = 1;
REPL_OUTPUT_STDERR = 2;
REPL_OUTPUT_RESULT = 3;
REPL_OUTPUT_DISPLAY = 4;
REPL_OUTPUT_ERROR = 5;
}
message CompletionItem {
string text = 1;
string type = 2; // function, variable, module, etc.
string signature = 3;
string docstring = 4;
}
message InspectResult {
string name = 1;
string type = 2;
string signature = 3;
string docstring = 4;
string source = 5;
}
// ============================================================================
// Browser / DevTools Integration
// ============================================================================
message BrowserParams {
BrowserAction action = 1;
string context_id = 2;
string page_id = 3;
// Action-specific params
string url = 10;
string selector = 11;
string code = 12; // JavaScript to evaluate
string expression = 13; // Console expression
// DevTools specific
bool enable_console = 20;
bool enable_network = 21;
bool enable_dom = 22;
bool enable_debugger = 23;
}
enum BrowserAction {
BROWSER_ACTION_UNSPECIFIED = 0;
// Navigation
BROWSER_ACTION_NAVIGATE = 1;
BROWSER_ACTION_RELOAD = 2;
BROWSER_ACTION_BACK = 3;
BROWSER_ACTION_FORWARD = 4;
// DOM interaction
BROWSER_ACTION_CLICK = 10;
BROWSER_ACTION_TYPE = 11;
BROWSER_ACTION_SELECT = 12;
BROWSER_ACTION_SCROLL = 13;
// JavaScript execution
BROWSER_ACTION_EVALUATE = 20;
BROWSER_ACTION_CONSOLE_EVAL = 21;
// DevTools
BROWSER_ACTION_CONSOLE_SUBSCRIBE = 30;
BROWSER_ACTION_NETWORK_SUBSCRIBE = 31;
BROWSER_ACTION_DOM_SNAPSHOT = 32;
BROWSER_ACTION_SET_BREAKPOINT = 33;
BROWSER_ACTION_REMOVE_BREAKPOINT = 34;
BROWSER_ACTION_PAUSE = 35;
BROWSER_ACTION_RESUME = 36;
BROWSER_ACTION_STEP = 37;
// Context management
BROWSER_ACTION_NEW_CONTEXT = 40;
BROWSER_ACTION_CLOSE_CONTEXT = 41;
BROWSER_ACTION_NEW_PAGE = 42;
BROWSER_ACTION_CLOSE_PAGE = 43;
BROWSER_ACTION_LIST_PAGES = 44;
// Screenshots/capture
BROWSER_ACTION_SCREENSHOT = 50;
BROWSER_ACTION_PDF = 51;
BROWSER_ACTION_ACCESSIBILITY_SNAPSHOT = 52;
}
message BrowserResult {
bool success = 1;
string page_id = 2;
string context_id = 3;
oneof result {
EvaluateResult evaluate = 10;
ConsoleMessage console_message = 11;
NetworkEvent network_event = 12;
DomSnapshot dom_snapshot = 13;
DebuggerEvent debugger_event = 14;
bytes screenshot = 15;
PageInfo page_info = 16;
repeated PageInfo pages = 17;
}
}
message EvaluateResult {
string type = 1; // undefined, string, number, object, etc.
string value = 2; // Serialized value
string preview = 3; // Short preview for objects
string class_name = 4;
string description = 5;
}
message ConsoleMessage {
ConsoleLevel level = 1;
string text = 2;
string url = 3;
int32 line = 4;
int32 column = 5;
int64 timestamp = 6;
repeated string args = 7;
}
enum ConsoleLevel {
CONSOLE_LEVEL_UNSPECIFIED = 0;
CONSOLE_LEVEL_LOG = 1;
CONSOLE_LEVEL_DEBUG = 2;
CONSOLE_LEVEL_INFO = 3;
CONSOLE_LEVEL_WARNING = 4;
CONSOLE_LEVEL_ERROR = 5;
}
message NetworkEvent {
NetworkEventType type = 1;
string request_id = 2;
string url = 3;
string method = 4;
int32 status = 5;
map<string, string> headers = 6;
bytes body = 7;
int64 timestamp = 8;
}
enum NetworkEventType {
NETWORK_EVENT_UNSPECIFIED = 0;
NETWORK_EVENT_REQUEST = 1;
NETWORK_EVENT_RESPONSE = 2;
NETWORK_EVENT_FAILED = 3;
}
message DomSnapshot {
string html = 1;
string accessibility_tree = 2;
repeated DomNode nodes = 3;
}
message DomNode {
int32 node_id = 1;
string tag = 2;
map<string, string> attributes = 3;
string text = 4;
repeated int32 children = 5;
BoundingBox bounds = 6;
}
message BoundingBox {
float x = 1;
float y = 2;
float width = 3;
float height = 4;
}
message DebuggerEvent {
DebuggerEventType type = 1;
string script_id = 2;
string url = 3;
int32 line = 4;
int32 column = 5;
repeated StackFrame call_frames = 6;
string reason = 7;
}
enum DebuggerEventType {
DEBUGGER_EVENT_UNSPECIFIED = 0;
DEBUGGER_EVENT_PAUSED = 1;
DEBUGGER_EVENT_RESUMED = 2;
DEBUGGER_EVENT_SCRIPT_PARSED = 3;
DEBUGGER_EVENT_BREAKPOINT_HIT = 4;
}
message StackFrame {
string function_name = 1;
string script_id = 2;
string url = 3;
int32 line = 4;
int32 column = 5;
repeated Scope scopes = 6;
}
message Scope {
string type = 1; // global, local, closure, etc.
map<string, string> variables = 2;
}
message PageInfo {
string page_id = 1;
string context_id = 2;
string url = 3;
string title = 4;
}
// ============================================================================
// IDE / Editor Integration
// ============================================================================
message IdeParams {
IdeAction action = 1;
string workspace_path = 2;
string file_path = 3;
// Position
int32 line = 10;
int32 column = 11;
int32 end_line = 12;
int32 end_column = 13;
// Content
string text = 20;
string new_name = 21;
// Diagnostics
DiagnosticSeverity min_severity = 30;
}
enum IdeAction {
IDE_ACTION_UNSPECIFIED = 0;
// File operations
IDE_ACTION_OPEN_FILE = 1;
IDE_ACTION_CLOSE_FILE = 2;
IDE_ACTION_SAVE_FILE = 3;
IDE_ACTION_GET_ACTIVE_FILE = 4;
IDE_ACTION_LIST_OPEN_FILES = 5;
// Editor operations
IDE_ACTION_GET_SELECTION = 10;
IDE_ACTION_SET_SELECTION = 11;
IDE_ACTION_INSERT_TEXT = 12;
IDE_ACTION_REPLACE_TEXT = 13;
IDE_ACTION_GET_LINE = 14;
IDE_ACTION_GET_RANGE = 15;
// Navigation
IDE_ACTION_GO_TO_DEFINITION = 20;
IDE_ACTION_FIND_REFERENCES = 21;
IDE_ACTION_GO_TO_LINE = 22;
IDE_ACTION_REVEAL_RANGE = 23;
// Refactoring
IDE_ACTION_RENAME = 30;
IDE_ACTION_FORMAT_DOCUMENT = 31;
IDE_ACTION_ORGANIZE_IMPORTS = 32;
IDE_ACTION_EXTRACT_FUNCTION = 33;
IDE_ACTION_EXTRACT_VARIABLE = 34;
// Diagnostics
IDE_ACTION_GET_DIAGNOSTICS = 40;
IDE_ACTION_QUICK_FIX = 41;
// Commands
IDE_ACTION_EXECUTE_COMMAND = 50;
IDE_ACTION_LIST_COMMANDS = 51;
// Workspace
IDE_ACTION_GET_WORKSPACE_FOLDERS = 60;
IDE_ACTION_FIND_FILES = 61;
IDE_ACTION_SEARCH_TEXT = 62;
// Terminal
IDE_ACTION_CREATE_TERMINAL = 70;
IDE_ACTION_SEND_TO_TERMINAL = 71;
IDE_ACTION_GET_TERMINAL_OUTPUT = 72;
}
message IdeResult {
bool success = 1;
oneof result {
FileInfo file_info = 10;
repeated FileInfo files = 11;
TextRange selection = 12;
string text = 13;
repeated Location locations = 14;
repeated Diagnostic diagnostics = 15;
repeated CodeAction code_actions = 16;
repeated string commands = 17;
WorkspaceInfo workspace = 18;
TerminalInfo terminal = 19;
}
}
message FileInfo {
string path = 1;
string uri = 2;
string language_id = 3;
bool is_dirty = 4;
int32 line_count = 5;
}
message TextRange {
int32 start_line = 1;
int32 start_column = 2;
int32 end_line = 3;
int32 end_column = 4;
string text = 5;
}
message Location {
string uri = 1;
TextRange range = 2;
}
message Diagnostic {
DiagnosticSeverity severity = 1;
TextRange range = 2;
string message = 3;
string source = 4;
string code = 5;
}
enum DiagnosticSeverity {
DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0;
DIAGNOSTIC_SEVERITY_ERROR = 1;
DIAGNOSTIC_SEVERITY_WARNING = 2;
DIAGNOSTIC_SEVERITY_INFO = 3;
DIAGNOSTIC_SEVERITY_HINT = 4;
}
message CodeAction {
string title = 1;
string kind = 2; // quickfix, refactor, source, etc.
bool is_preferred = 3;
repeated TextEdit edits = 4;
string command = 5;
}
message TextEdit {
TextRange range = 1;
string new_text = 2;
}
message WorkspaceInfo {
repeated WorkspaceFolder folders = 1;
}
message WorkspaceFolder {
string uri = 1;
string name = 2;
}
message TerminalInfo {
string id = 1;
string name = 2;
int32 pid = 3;
}
// ============================================================================
// Extension / Plugin Messaging
// ============================================================================
message ExtensionParams {
string extension_id = 1;
ExtensionAction action = 2;
string channel = 3;
bytes payload = 4;
map<string, string> headers = 5;
}
enum ExtensionAction {
EXTENSION_ACTION_UNSPECIFIED = 0;
EXTENSION_ACTION_SEND = 1; // Send message to extension
EXTENSION_ACTION_BROADCAST = 2; // Broadcast to all extensions
EXTENSION_ACTION_SUBSCRIBE = 3; // Subscribe to channel
EXTENSION_ACTION_UNSUBSCRIBE = 4;
EXTENSION_ACTION_LIST = 5; // List connected extensions
EXTENSION_ACTION_PING = 6; // Health check
}
message ExtensionResult {
bool success = 1;
string extension_id = 2;
bytes payload = 3;
repeated ExtensionInfo extensions = 4;
}
message ExtensionInfo {
string id = 1;
string name = 2;
string version = 3;
ExtensionType type = 4;
repeated string capabilities = 5;
bool connected = 6;
}
enum ExtensionType {
EXTENSION_TYPE_UNSPECIFIED = 0;
EXTENSION_TYPE_BROWSER = 1; // Browser extension
EXTENSION_TYPE_IDE = 2; // IDE plugin (VS Code, JetBrains)
EXTENSION_TYPE_TERMINAL = 3; // Terminal integration
EXTENSION_TYPE_MOBILE = 4; // Mobile app
EXTENSION_TYPE_DESKTOP = 5; // Desktop app
}
// ============================================================================
// Streaming
// ============================================================================
message StreamParams {
StreamAction action = 1;
string stream_id = 2;
StreamType type = 3;
map<string, string> filters = 4;
}
enum StreamAction {
STREAM_ACTION_UNSPECIFIED = 0;
STREAM_ACTION_START = 1;
STREAM_ACTION_STOP = 2;
STREAM_ACTION_PAUSE = 3;
STREAM_ACTION_RESUME = 4;
}
enum StreamType {
STREAM_TYPE_UNSPECIFIED = 0;
STREAM_TYPE_REPL_OUTPUT = 1;
STREAM_TYPE_CONSOLE_MESSAGES = 2;
STREAM_TYPE_NETWORK_EVENTS = 3;
STREAM_TYPE_DIAGNOSTICS = 4;
STREAM_TYPE_FILE_CHANGES = 5;
STREAM_TYPE_DEBUGGER = 6;
}
message StreamChunk {
string stream_id = 1;
int64 sequence = 2;
int64 timestamp = 3;
oneof data {
ReplOutput repl_output = 10;
ConsoleMessage console_message = 11;
NetworkEvent network_event = 12;
Diagnostic diagnostic = 13;
FileChangeEvent file_change = 14;
DebuggerEvent debugger_event = 15;
bytes raw = 99;
}
}
message FileChangeEvent {
FileChangeType type = 1;
string uri = 2;
}
enum FileChangeType {
FILE_CHANGE_UNSPECIFIED = 0;
FILE_CHANGE_CREATED = 1;
FILE_CHANGE_CHANGED = 2;
FILE_CHANGE_DELETED = 3;
}
// ============================================================================
// Service Definition
// ============================================================================
service ZapService {
// Unary RPCs (MCP compatible)
rpc Call(ZapRequest) returns (ZapResponse);
// Server streaming (real-time output)
rpc Stream(StreamParams) returns (stream StreamChunk);
// Bidirectional streaming (interactive sessions)
rpc Interactive(stream ZapRequest) returns (stream ZapResponse);
}