Skip to main content

hanzo_mcp/
lib.rs

1#![recursion_limit = "256"]
2//! Hanzo MCP Server - Rust implementation (HIP-0300)
3//!
4/// Provides full tool parity with Python hanzo-mcp:
5/// - exec: Process execution
6/// - fs: File system operations
7/// - plan: Plan tracking
8/// - think: Reasoning tools (think, critic, review)
9/// - memory: Memory and knowledge management
10/// - computer: Native OS control
11/// - browser: Playwright-based browser automation
12/// - mode: Development modes
13/// - search: Unified code search
14
15pub mod brain;
16pub mod config;
17pub mod ffi;
18pub mod hanzo_api;
19pub mod server;
20pub mod protocol;
21pub mod tools;
22pub mod search;
23
24pub use config::Config;
25pub use server::MCPServer;
26pub use tools::{
27    ExecTool, FsTool, PlanTool, ThinkTool, MemoryTool,
28    ComputerTool, BrowserTool, ModeTool,
29    CodeTool, GitTool, FetchTool, WorkspaceTool, TasksTool, HanzoTool,
30    list_tools, parity_status,
31};
32
33use anyhow::Result;
34use serde::{Deserialize, Serialize};
35use serde_json::{json, Value};
36use std::collections::HashMap;
37use std::sync::Arc;
38use tokio::sync::RwLock;
39
40/// MCP Tool trait that all tools must implement
41#[async_trait::async_trait]
42pub trait MCPTool: Send + Sync {
43    /// Get the tool's name
44    fn name(&self) -> &str;
45
46    /// Get the tool's description
47    fn description(&self) -> &str;
48
49    /// Get the tool's parameters schema
50    fn parameters(&self) -> serde_json::Value;
51
52    /// Execute the tool with given parameters
53    async fn execute(&self, params: serde_json::Value) -> Result<ToolResult>;
54}
55
56/// Result from tool execution
57#[derive(Debug, Serialize, Deserialize)]
58pub struct ToolResult {
59    pub success: bool,
60    pub content: serde_json::Value,
61    pub error: Option<String>,
62}
63
64impl ToolResult {
65    pub fn ok(content: Value) -> Self {
66        Self {
67            success: true,
68            content,
69            error: None,
70        }
71    }
72
73    pub fn err(message: &str) -> Self {
74        Self {
75            success: false,
76            content: json!(null),
77            error: Some(message.to_string()),
78        }
79    }
80}
81
82/// Tool wrapper for unified execution
83pub struct ToolWrapper<T> {
84    pub tool: Arc<RwLock<T>>,
85    pub name: String,
86    pub description: String,
87    pub schema: Value,
88}
89
90/// Tool registry for managing all available tools
91pub struct ToolRegistry {
92    tools: HashMap<String, Box<dyn MCPTool>>,
93    exec: Arc<RwLock<ExecTool>>,
94    fs: Arc<RwLock<FsTool>>,
95    code: Arc<RwLock<CodeTool>>,
96    git: Arc<RwLock<GitTool>>,
97    fetch: Arc<RwLock<FetchTool>>,
98    workspace: Arc<RwLock<WorkspaceTool>>,
99    plan: Arc<RwLock<PlanTool>>,
100    think: Arc<RwLock<ThinkTool>>,
101    memory: Arc<RwLock<MemoryTool>>,
102    computer: Arc<RwLock<ComputerTool>>,
103    browser: Arc<RwLock<BrowserTool>>,
104    mode: Arc<RwLock<ModeTool>>,
105    tasks: Arc<RwLock<TasksTool>>,
106    hanzo: Arc<RwLock<HanzoTool>>,
107}
108
109impl ToolRegistry {
110    pub fn new() -> Self {
111        Self {
112            tools: HashMap::new(),
113            exec: Arc::new(RwLock::new(ExecTool::new())),
114            fs: Arc::new(RwLock::new(FsTool::new())),
115            code: Arc::new(RwLock::new(CodeTool::new())),
116            git: Arc::new(RwLock::new(GitTool::new())),
117            fetch: Arc::new(RwLock::new(FetchTool::new())),
118            workspace: Arc::new(RwLock::new(WorkspaceTool::new())),
119            plan: Arc::new(RwLock::new(PlanTool::new())),
120            think: Arc::new(RwLock::new(ThinkTool::new())),
121            memory: Arc::new(RwLock::new(MemoryTool::new())),
122            computer: Arc::new(RwLock::new(ComputerTool::new())),
123            browser: Arc::new(RwLock::new(BrowserTool::new())),
124            mode: Arc::new(RwLock::new(ModeTool::new())),
125            tasks: Arc::new(RwLock::new(TasksTool::new())),
126            hanzo: Arc::new(RwLock::new(HanzoTool::new())),
127        }
128    }
129
130    pub fn register(&mut self, tool: Box<dyn MCPTool>) {
131        self.tools.insert(tool.name().to_string(), tool);
132    }
133
134    pub fn get(&self, name: &str) -> Option<&Box<dyn MCPTool>> {
135        self.tools.get(name)
136    }
137
138    pub fn list(&self) -> Vec<String> {
139        let mut names: Vec<String> = self.tools.keys().cloned().collect();
140        // Add built-in tools (all 13 HIP-0300 canonical + search alias + browser extension)
141        names.extend(vec![
142            "exec".into(), "fs".into(), "code".into(), "git".into(),
143            "fetch".into(), "workspace".into(), "computer".into(),
144            "think".into(), "memory".into(), "hanzo".into(),
145            "plan".into(), "tasks".into(), "mode".into(),
146            "search".into(), "browser".into(),
147        ]);
148        names.sort();
149        names.dedup();
150        names
151    }
152
153    /// Execute a tool by name
154    pub async fn execute(&self, name: &str, params: Value) -> Result<ToolResult> {
155        match name {
156            "exec" => {
157                let args: tools::ExecToolArgs = serde_json::from_value(params)?;
158                let result = self.exec.read().await.execute(args).await?;
159                Ok(ToolResult::ok(serde_json::from_str(&result)?))
160            }
161            "fs" => {
162                let args: tools::FsToolArgs = serde_json::from_value(params)?;
163                let result = self.fs.read().await.execute(args).await?;
164                Ok(ToolResult::ok(serde_json::from_str(&result)?))
165            }
166            "search" => {
167                let mut args: tools::FsToolArgs = serde_json::from_value(params)?;
168                if args.action.is_empty() {
169                    args.action = "search".to_string();
170                }
171                let result = self.fs.read().await.execute(args).await?;
172                Ok(ToolResult::ok(serde_json::from_str(&result)?))
173            }
174            "plan" => {
175                let args: tools::PlanToolArgs = serde_json::from_value(params)?;
176                let result = self.plan.read().await.execute(args).await?;
177                Ok(ToolResult::ok(serde_json::from_str(&result)?))
178            }
179            "think" => {
180                let args: tools::ThinkToolArgs = serde_json::from_value(params)?;
181                let result = self.think.read().await.execute(args).await?;
182                Ok(ToolResult::ok(result))
183            }
184            "memory" => {
185                let args: tools::MemoryToolArgs = serde_json::from_value(params)?;
186                let result = self.memory.read().await.execute(args).await?;
187                Ok(ToolResult::ok(serde_json::from_str(&result)?))
188            }
189            "computer" => {
190                let args: tools::ComputerToolArgs = serde_json::from_value(params)?;
191                let mut computer = self.computer.write().await;
192                let result = computer.execute(args).await?;
193                Ok(ToolResult::ok(serde_json::from_str(&result)?))
194            }
195            "browser" => {
196                let args: tools::BrowserToolArgs = serde_json::from_value(params)?;
197                let result = self.browser.read().await.execute(args).await?;
198                Ok(ToolResult::ok(serde_json::from_str(&result)?))
199            }
200            "mode" => {
201                let args: tools::ModeToolArgs = serde_json::from_value(params)?;
202                let result = self.mode.read().await.execute(args).await?;
203                Ok(ToolResult::ok(serde_json::from_str(&result)?))
204            }
205            "code" => {
206                let args: tools::CodeToolArgs = serde_json::from_value(params)?;
207                let result = self.code.read().await.execute(args).await?;
208                Ok(ToolResult::ok(result))
209            }
210            "git" => {
211                let args: tools::GitToolArgs = serde_json::from_value(params)?;
212                let result = self.git.read().await.execute(args).await?;
213                Ok(ToolResult::ok(result))
214            }
215            "fetch" => {
216                let args: tools::FetchToolArgs = serde_json::from_value(params)?;
217                let result = self.fetch.read().await.execute(args).await?;
218                Ok(ToolResult::ok(result))
219            }
220            "workspace" => {
221                let args: tools::WorkspaceToolArgs = serde_json::from_value(params)?;
222                let result = self.workspace.read().await.execute(args).await?;
223                Ok(ToolResult::ok(result))
224            }
225            "tasks" => {
226                let args: tools::TasksToolArgs = serde_json::from_value(params)?;
227                let result = self.tasks.read().await.execute(args).await?;
228                Ok(ToolResult::ok(result))
229            }
230            "hanzo" => {
231                let args: tools::HanzoToolArgs = serde_json::from_value(params)?;
232                let result = self.hanzo.read().await.execute(args).await?;
233                Ok(ToolResult::ok(result))
234            }
235            _ => {
236                if let Some(tool) = self.tools.get(name) {
237                    tool.execute(params).await
238                } else {
239                    Ok(ToolResult::err(&format!("Unknown tool: {}", name)))
240                }
241            }
242        }
243    }
244
245    /// Get tool definitions for MCP protocol
246    pub fn get_definitions(&self) -> Vec<Value> {
247        let mut definitions = vec![
248            json!({
249                "name": "exec",
250                "description": tools::ExecToolDefinition::new().description,
251                "inputSchema": tools::ExecToolDefinition::new().input_schema
252            }),
253            json!({
254                "name": "fs",
255                "description": tools::FsToolDefinition::new().description,
256                "inputSchema": tools::FsToolDefinition::new().input_schema
257            }),
258            json!({
259                "name": "search",
260                "description": "Search file contents (alias of fs with action=search)",
261                "inputSchema": tools::FsToolDefinition::new().input_schema
262            }),
263            json!({
264                "name": "plan",
265                "description": tools::PlanToolDefinition::new().description,
266                "inputSchema": tools::PlanToolDefinition::new().input_schema
267            }),
268            json!({
269                "name": "think",
270                "description": tools::ThinkToolDefinition::new().description,
271                "inputSchema": tools::ThinkToolDefinition::new().input_schema
272            }),
273            json!({
274                "name": "memory",
275                "description": tools::MemoryToolDefinition::new().description,
276                "inputSchema": tools::MemoryToolDefinition::new().input_schema
277            }),
278            json!({
279                "name": "computer",
280                "description": tools::ComputerToolDefinition::new().description,
281                "inputSchema": tools::ComputerToolDefinition::new().input_schema
282            }),
283            json!({
284                "name": "browser",
285                "description": tools::BrowserToolDefinition::new().description,
286                "inputSchema": tools::BrowserToolDefinition::new().input_schema
287            }),
288            json!({
289                "name": "mode",
290                "description": tools::ModeToolDefinition::new().description,
291                "inputSchema": tools::ModeToolDefinition::new().input_schema
292            }),
293            tools::CodeToolDefinition::schema(),
294            tools::GitToolDefinition::schema(),
295            tools::FetchToolDefinition::schema(),
296            tools::WorkspaceToolDefinition::schema(),
297            tools::TasksToolDefinition::schema(),
298            tools::HanzoToolDefinition::schema(),
299        ];
300
301        // Add custom registered tools
302        for tool in self.tools.values() {
303            definitions.push(json!({
304                "name": tool.name(),
305                "description": tool.description(),
306                "inputSchema": tool.parameters()
307            }));
308        }
309
310        definitions
311    }
312
313    /// Initialize with all default tools
314    pub fn with_defaults() -> Self {
315        let mut registry = Self::new();
316
317        // Cloud-backed tools (api.hanzo.ai) via the generic MCPTool seam.
318        // These complement the local tools: code_* is cross-repo RAG next to the
319        // local tree-sitter `code` tool; web_* and vision reach the platform.
320        registry.register(Box::new(tools::CodeSearchTool::new()));
321        registry.register(Box::new(tools::CodeContextTool::new()));
322        registry.register(Box::new(tools::CodeAskTool::new()));
323        registry.register(Box::new(tools::CodeIndexTool::new()));
324        registry.register(Box::new(tools::WebSearchTool::new()));
325        registry.register(Box::new(tools::WebReadTool::new()));
326        registry.register(Box::new(tools::ResearchTool::new()));
327        registry.register(Box::new(tools::VisionTool::new()));
328
329        // Ported tool surface (HIP-0300 parity with python-sdk): config, llm, ui,
330        // agent, lsp, refactor, system — all dyn-trait tools via the MCPTool seam.
331        registry.register(Box::new(tools::ConfigTool::new()));
332        registry.register(Box::new(tools::LlmTool::new()));
333        registry.register(Box::new(tools::UiTool::new()));
334        registry.register(Box::new(tools::AgentTool::new()));
335        registry.register(Box::new(tools::LspTool::new()));
336        registry.register(Box::new(tools::RefactorTool::new()));
337        registry.register(Box::new(tools::SystemTool::new()));
338
339        #[cfg(feature = "computer-control")]
340        {
341            // Additional computer control features
342        }
343
344        #[cfg(feature = "vector-store")]
345        {
346            // Vector store integration
347        }
348
349        registry
350    }
351}
352
353impl Default for ToolRegistry {
354    fn default() -> Self {
355        Self::with_defaults()
356    }
357}
358
359/// Get version information
360pub fn version() -> Value {
361    json!({
362        "name": "hanzo-mcp",
363        "version": env!("CARGO_PKG_VERSION"),
364        "rust_version": "1.75+",
365        "tools": list_tools().len(),
366        "parity": parity_status()
367    })
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn test_tool_registry() {
376        let registry = ToolRegistry::new();
377        let tools = registry.list();
378        assert!(tools.contains(&"exec".to_string()));
379        assert!(tools.contains(&"fs".to_string()));
380        assert!(tools.contains(&"search".to_string()));
381        assert!(tools.contains(&"plan".to_string()));
382        assert!(tools.contains(&"think".to_string()));
383        assert!(tools.contains(&"memory".to_string()));
384        assert!(tools.contains(&"computer".to_string()));
385        assert!(tools.contains(&"browser".to_string()));
386        assert!(tools.contains(&"mode".to_string()));
387    }
388
389    #[test]
390    fn test_tool_definitions() {
391        let registry = ToolRegistry::new();
392        let definitions = registry.get_definitions();
393        assert!(definitions.len() >= 9);
394    }
395
396    #[test]
397    fn test_cloud_tools_registered_and_discoverable() {
398        let registry = ToolRegistry::with_defaults();
399        let names = registry.list();
400        for t in ["code_search", "code_context", "code_ask", "code_index", "web_search", "web_read", "research", "vision"] {
401            assert!(names.contains(&t.to_string()), "{t} missing from registry.list()");
402        }
403        // Each cloud tool must also expose a definition (name + inputSchema) for MCP tools/list.
404        let defs = registry.get_definitions();
405        for t in ["code_search", "code_context", "code_ask", "code_index", "web_search", "web_read", "research", "vision"] {
406            assert!(
407                defs.iter().any(|d| d["name"] == t && d["inputSchema"].is_object()),
408                "{t} missing a definition"
409            );
410        }
411    }
412
413    #[tokio::test]
414    async fn test_proc_execute() {
415        let registry = ToolRegistry::new();
416        let result = registry.execute("exec", json!({
417            "action": "help"
418        })).await;
419        assert!(result.is_ok());
420    }
421
422    #[tokio::test]
423    async fn test_fs_execute() {
424        let registry = ToolRegistry::new();
425        let result = registry.execute("fs", json!({
426            "action": "help"
427        })).await;
428        assert!(result.is_ok());
429    }
430
431    #[test]
432    fn test_version() {
433        let v = version();
434        assert!(v.get("name").is_some());
435        assert!(v.get("version").is_some());
436        assert!(v.get("tools").is_some());
437    }
438}