hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,538 @@
1
+ """Node management tool for hanzo-node operations."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, final, override
12
+
13
+ import httpx
14
+ from mcp.server import FastMCP
15
+ from mcp.server.fastmcp import Context as MCPContext
16
+
17
+ from hanzo_mcp.tools.common.base import BaseTool
18
+ from hanzo_mcp.tools.common.context import create_tool_context
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Default hanzo-node configuration
23
+ DEFAULT_NODE_CONFIG = {
24
+ "host": "localhost",
25
+ "port": 3690,
26
+ "db_path": "~/.hanzo/lancedb",
27
+ "models_path": "~/.hanzo/models",
28
+ "log_level": "info",
29
+ "embedding_model": "text-embedding-3-small",
30
+ "embedding_dimensions": 1536
31
+ }
32
+
33
+
34
+ @final
35
+ class NodeTool(BaseTool):
36
+ """Tool for managing local hanzo-node instance.
37
+
38
+ This tool provides management capabilities for the local hanzo-node:
39
+ - Download and install hanzo-node
40
+ - Configure node settings via ~/.hanzo
41
+ - Start/stop/restart node
42
+ - Check node status and health
43
+ - Manage vector store (LanceDB)
44
+ - Download and load models
45
+ - View logs and diagnostics
46
+ """
47
+
48
+ def __init__(self):
49
+ """Initialize node management tool."""
50
+ self.hanzo_dir = Path.home() / ".hanzo"
51
+ self.config_file = self.hanzo_dir / "node_config.json"
52
+ self.models_dir = self.hanzo_dir / "models"
53
+ self.logs_dir = self.hanzo_dir / "logs"
54
+ self.lancedb_dir = self.hanzo_dir / "lancedb"
55
+
56
+ # Ensure directories exist
57
+ for directory in [self.hanzo_dir, self.models_dir, self.logs_dir, self.lancedb_dir]:
58
+ directory.mkdir(parents=True, exist_ok=True)
59
+
60
+ # Initialize config if it doesn't exist
61
+ self._ensure_config()
62
+
63
+ def _ensure_config(self) -> None:
64
+ """Ensure node configuration exists."""
65
+ if not self.config_file.exists():
66
+ config = DEFAULT_NODE_CONFIG.copy()
67
+ # Expand paths
68
+ config["db_path"] = str(self.lancedb_dir)
69
+ config["models_path"] = str(self.models_dir)
70
+ self._save_config(config)
71
+
72
+ def _load_config(self) -> Dict[str, Any]:
73
+ """Load node configuration."""
74
+ try:
75
+ with open(self.config_file, 'r') as f:
76
+ return json.load(f)
77
+ except Exception:
78
+ return DEFAULT_NODE_CONFIG.copy()
79
+
80
+ def _save_config(self, config: Dict[str, Any]) -> None:
81
+ """Save node configuration."""
82
+ with open(self.config_file, 'w') as f:
83
+ json.dump(config, f, indent=2)
84
+
85
+ async def _is_node_running(self, host: str = "localhost", port: int = 3690) -> bool:
86
+ """Check if hanzo-node is running."""
87
+ try:
88
+ async with httpx.AsyncClient(timeout=5.0) as client:
89
+ response = await client.get(f"http://{host}:{port}/health")
90
+ return response.status_code == 200
91
+ except Exception:
92
+ return False
93
+
94
+ async def _find_node_executable(self) -> Optional[Path]:
95
+ """Find hanzo-node executable."""
96
+ # Check common locations
97
+ possible_paths = [
98
+ Path.home() / ".hanzo" / "bin" / "hanzo-node",
99
+ Path("/usr/local/bin/hanzo-node"),
100
+ Path("/opt/hanzo/bin/hanzo-node"),
101
+ # Development paths
102
+ Path.home() / "work" / "hanzo" / "node" / "apps" / "hanzo-node" / "target" / "release" / "hanzo-node",
103
+ Path.home() / "work" / "hanzo" / "target" / "release" / "hanzo-node",
104
+ ]
105
+
106
+ # Also check PATH
107
+ if shutil.which("hanzo-node"):
108
+ possible_paths.append(Path(shutil.which("hanzo-node")))
109
+
110
+ for path in possible_paths:
111
+ if path.exists() and path.is_file():
112
+ return path
113
+
114
+ return None
115
+
116
+ async def _download_node(self, tool_ctx) -> bool:
117
+ """Download hanzo-node if not available."""
118
+ await tool_ctx.info("Downloading hanzo-node...")
119
+
120
+ # For now, we'll build from source if available
121
+ source_dir = Path.home() / "work" / "hanzo" / "node" / "apps" / "hanzo-node"
122
+ if source_dir.exists():
123
+ await tool_ctx.info("Building hanzo-node from source...")
124
+ try:
125
+ # Build the project
126
+ result = subprocess.run(
127
+ ["cargo", "build", "--release"],
128
+ cwd=source_dir,
129
+ capture_output=True,
130
+ text=True,
131
+ timeout=300 # 5 minutes
132
+ )
133
+
134
+ if result.returncode == 0:
135
+ # Copy binary to hanzo directory
136
+ source_binary = source_dir / "target" / "release" / "hanzo-node"
137
+ target_binary = self.hanzo_dir / "bin" / "hanzo-node"
138
+ target_binary.parent.mkdir(parents=True, exist_ok=True)
139
+
140
+ if source_binary.exists():
141
+ shutil.copy2(source_binary, target_binary)
142
+ target_binary.chmod(0o755)
143
+ await tool_ctx.info(f"Built and installed hanzo-node to {target_binary}")
144
+ return True
145
+ else:
146
+ await tool_ctx.error("Build succeeded but binary not found")
147
+ return False
148
+ else:
149
+ await tool_ctx.error(f"Build failed: {result.stderr}")
150
+ return False
151
+
152
+ except subprocess.TimeoutExpired:
153
+ await tool_ctx.error("Build timed out after 5 minutes")
154
+ return False
155
+ except Exception as e:
156
+ await tool_ctx.error(f"Build error: {e}")
157
+ return False
158
+ else:
159
+ await tool_ctx.warning("Source code not found. Manual installation required.")
160
+ await tool_ctx.info("To install hanzo-node manually:")
161
+ await tool_ctx.info("1. Download from releases or build from source")
162
+ await tool_ctx.info("2. Place binary at ~/.hanzo/bin/hanzo-node")
163
+ await tool_ctx.info("3. Run 'node(action=\"status\")' to verify")
164
+ return False
165
+
166
+ @property
167
+ @override
168
+ def name(self) -> str:
169
+ """Get the tool name."""
170
+ return "node"
171
+
172
+ @property
173
+ @override
174
+ def description(self) -> str:
175
+ """Get the tool description."""
176
+ return """Manage local hanzo-node instance.
177
+
178
+ This tool provides comprehensive management of your local hanzo-node:
179
+
180
+ Configuration:
181
+ - Configure node settings via ~/.hanzo/node_config.json
182
+ - Set embedding models, database paths, ports
183
+ - Manage authentication and security settings
184
+
185
+ Lifecycle:
186
+ - Install/download hanzo-node if not available
187
+ - Start, stop, restart the node process
188
+ - Check node status and health
189
+ - View logs and diagnostics
190
+
191
+ Data Management:
192
+ - Manage vector store (LanceDB) location and settings
193
+ - Download and configure ML models
194
+ - Backup and restore node data
195
+ - Clear caches and temporary files
196
+
197
+ Examples:
198
+ node(action="status") # Check if node is running
199
+ node(action="start") # Start the node
200
+ node(action="stop") # Stop the node
201
+ node(action="restart") # Restart the node
202
+ node(action="config", key="port", value="3691") # Update config
203
+ node(action="logs") # View recent logs
204
+ node(action="models") # List available models
205
+ node(action="install") # Install/build hanzo-node
206
+ """
207
+
208
+ @override
209
+ async def call(
210
+ self,
211
+ ctx: MCPContext,
212
+ action: str,
213
+ key: Optional[str] = None,
214
+ value: Optional[str] = None,
215
+ force: bool = False,
216
+ **kwargs
217
+ ) -> str:
218
+ """Execute node management operations.
219
+
220
+ Args:
221
+ ctx: MCP context
222
+ action: Action to perform (status, start, stop, restart, config, logs, models, install)
223
+ key: Configuration key (for config action)
224
+ value: Configuration value (for config action)
225
+ force: Force action without confirmation
226
+ **kwargs: Additional action-specific parameters
227
+
228
+ Returns:
229
+ Operation result or status information
230
+ """
231
+ tool_ctx = create_tool_context(ctx)
232
+ await tool_ctx.set_tool_info(self.name)
233
+
234
+ if action == "status":
235
+ return await self._handle_status(tool_ctx)
236
+ elif action == "start":
237
+ return await self._handle_start(tool_ctx, force)
238
+ elif action == "stop":
239
+ return await self._handle_stop(tool_ctx, force)
240
+ elif action == "restart":
241
+ return await self._handle_restart(tool_ctx, force)
242
+ elif action == "config":
243
+ return await self._handle_config(tool_ctx, key, value)
244
+ elif action == "logs":
245
+ return await self._handle_logs(tool_ctx, kwargs.get("lines", 50))
246
+ elif action == "models":
247
+ return await self._handle_models(tool_ctx)
248
+ elif action == "install":
249
+ return await self._handle_install(tool_ctx, force)
250
+ elif action == "clean":
251
+ return await self._handle_clean(tool_ctx, force)
252
+ else:
253
+ return f"Unknown action: {action}. Available actions: status, start, stop, restart, config, logs, models, install, clean"
254
+
255
+ async def _handle_status(self, tool_ctx) -> str:
256
+ """Handle status check."""
257
+ config = self._load_config()
258
+ host = config.get("host", "localhost")
259
+ port = config.get("port", 3690)
260
+
261
+ status_info = [f"Hanzo Node Status"]
262
+ status_info.append(f"Configuration: {self.config_file}")
263
+ status_info.append(f"Expected URL: http://{host}:{port}")
264
+
265
+ # Check if node is running
266
+ is_running = await self._is_node_running(host, port)
267
+ status_info.append(f"Running: {is_running}")
268
+
269
+ # Check for executable
270
+ executable = await self._find_node_executable()
271
+ if executable:
272
+ status_info.append(f"Executable: {executable}")
273
+ else:
274
+ status_info.append("Executable: Not found")
275
+
276
+ # Check directories
277
+ status_info.append(f"Data directory: {self.hanzo_dir}")
278
+ status_info.append(f"Models directory: {self.models_dir} ({len(list(self.models_dir.glob('*')))} files)")
279
+ status_info.append(f"LanceDB directory: {self.lancedb_dir} ({len(list(self.lancedb_dir.glob('*')))} files)")
280
+
281
+ # Check config
282
+ status_info.append(f"Configured port: {port}")
283
+ status_info.append(f"Configured host: {host}")
284
+ status_info.append(f"DB path: {config.get('db_path', 'Not set')}")
285
+ status_info.append(f"Models path: {config.get('models_path', 'Not set')}")
286
+
287
+ return "\n".join(status_info)
288
+
289
+ async def _handle_start(self, tool_ctx, force: bool) -> str:
290
+ """Handle node start."""
291
+ config = self._load_config()
292
+ host = config.get("host", "localhost")
293
+ port = config.get("port", 3690)
294
+
295
+ # Check if already running
296
+ if await self._is_node_running(host, port):
297
+ return f"Hanzo node is already running on {host}:{port}"
298
+
299
+ # Find executable
300
+ executable = await self._find_node_executable()
301
+ if not executable:
302
+ return "Hanzo node executable not found. Run node(action=\"install\") first."
303
+
304
+ await tool_ctx.info(f"Starting hanzo-node on {host}:{port}...")
305
+
306
+ try:
307
+ # Prepare environment and arguments
308
+ env = os.environ.copy()
309
+ env["HANZO_CONFIG"] = str(self.config_file)
310
+
311
+ # Start the process in background
312
+ process = subprocess.Popen(
313
+ [str(executable), "--config", str(self.config_file)],
314
+ env=env,
315
+ stdout=subprocess.PIPE,
316
+ stderr=subprocess.PIPE,
317
+ start_new_session=True
318
+ )
319
+
320
+ # Wait a moment and check if it started successfully
321
+ await asyncio.sleep(2)
322
+
323
+ if await self._is_node_running(host, port):
324
+ return f"Successfully started hanzo-node on {host}:{port} (PID: {process.pid})"
325
+ else:
326
+ # Process might have failed
327
+ return_code = process.poll()
328
+ if return_code is not None:
329
+ _, stderr = process.communicate()
330
+ return f"Failed to start hanzo-node (exit code {return_code}): {stderr.decode()}"
331
+ else:
332
+ return f"Started hanzo-node process (PID: {process.pid}) but health check failed"
333
+
334
+ except Exception as e:
335
+ return f"Error starting hanzo-node: {e}"
336
+
337
+ async def _handle_stop(self, tool_ctx, force: bool) -> str:
338
+ """Handle node stop."""
339
+ config = self._load_config()
340
+ host = config.get("host", "localhost")
341
+ port = config.get("port", 3690)
342
+
343
+ # Check if running
344
+ if not await self._is_node_running(host, port):
345
+ return "Hanzo node is not running"
346
+
347
+ await tool_ctx.info("Stopping hanzo-node...")
348
+
349
+ try:
350
+ # Try graceful shutdown via API first
351
+ async with httpx.AsyncClient(timeout=10.0) as client:
352
+ try:
353
+ response = await client.post(f"http://{host}:{port}/api/shutdown")
354
+ if response.status_code == 200:
355
+ # Wait for shutdown
356
+ await asyncio.sleep(2)
357
+ if not await self._is_node_running(host, port):
358
+ return "Successfully stopped hanzo-node"
359
+ except Exception:
360
+ pass
361
+
362
+ # If graceful shutdown failed, try finding and killing the process
363
+ if force:
364
+ try:
365
+ # Find processes by name
366
+ result = subprocess.run(
367
+ ["pgrep", "-f", "hanzo-node"],
368
+ capture_output=True,
369
+ text=True
370
+ )
371
+
372
+ if result.returncode == 0 and result.stdout.strip():
373
+ pids = result.stdout.strip().split('\n')
374
+ for pid in pids:
375
+ subprocess.run(["kill", "-TERM", pid])
376
+
377
+ await asyncio.sleep(2)
378
+ if not await self._is_node_running(host, port):
379
+ return f"Force stopped hanzo-node (killed {len(pids)} processes)"
380
+ else:
381
+ # Try SIGKILL
382
+ for pid in pids:
383
+ subprocess.run(["kill", "-KILL", pid])
384
+ return f"Force killed hanzo-node processes"
385
+ except Exception as e:
386
+ return f"Error force stopping: {e}"
387
+ else:
388
+ return "Graceful shutdown failed. Use force=true for forceful shutdown."
389
+
390
+ except Exception as e:
391
+ return f"Error stopping hanzo-node: {e}"
392
+
393
+ async def _handle_restart(self, tool_ctx, force: bool) -> str:
394
+ """Handle node restart."""
395
+ stop_result = await self._handle_stop(tool_ctx, force)
396
+ await asyncio.sleep(1)
397
+ start_result = await self._handle_start(tool_ctx, force)
398
+ return f"Restart: {stop_result} -> {start_result}"
399
+
400
+ async def _handle_config(self, tool_ctx, key: Optional[str], value: Optional[str]) -> str:
401
+ """Handle configuration management."""
402
+ config = self._load_config()
403
+
404
+ if key is None:
405
+ # Show current config
406
+ formatted_config = json.dumps(config, indent=2)
407
+ return f"Current configuration:\n{formatted_config}"
408
+
409
+ if value is None:
410
+ # Show specific key
411
+ if key in config:
412
+ return f"{key}: {config[key]}"
413
+ else:
414
+ return f"Configuration key '{key}' not found"
415
+
416
+ # Update configuration
417
+ old_value = config.get(key, "Not set")
418
+ config[key] = value
419
+
420
+ # Validate important settings
421
+ if key == "port":
422
+ try:
423
+ port = int(value)
424
+ if not (1024 <= port <= 65535):
425
+ return "Error: Port must be between 1024 and 65535"
426
+ except ValueError:
427
+ return "Error: Port must be a number"
428
+
429
+ # Save updated config
430
+ self._save_config(config)
431
+
432
+ await tool_ctx.info(f"Updated {key}: {old_value} -> {value}")
433
+ return f"Configuration updated: {key} = {value}\nRestart required for changes to take effect."
434
+
435
+ async def _handle_logs(self, tool_ctx, lines: int = 50) -> str:
436
+ """Handle log viewing."""
437
+ log_file = self.logs_dir / "hanzo-node.log"
438
+
439
+ if not log_file.exists():
440
+ return "No log file found. Node may not have been started yet."
441
+
442
+ try:
443
+ # Read last N lines
444
+ with open(log_file, 'r') as f:
445
+ all_lines = f.readlines()
446
+ recent_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
447
+
448
+ if not recent_lines:
449
+ return "Log file is empty"
450
+
451
+ log_content = ''.join(recent_lines)
452
+ return f"Last {len(recent_lines)} lines from hanzo-node.log:\n{log_content}"
453
+
454
+ except Exception as e:
455
+ return f"Error reading log file: {e}"
456
+
457
+ async def _handle_models(self, tool_ctx) -> str:
458
+ """Handle model management."""
459
+ models_info = [f"Models directory: {self.models_dir}"]
460
+
461
+ # List model files
462
+ model_files = list(self.models_dir.glob("*"))
463
+ if model_files:
464
+ models_info.append(f"\nFound {len(model_files)} model files:")
465
+ for model_file in sorted(model_files):
466
+ size_mb = model_file.stat().st_size / (1024 * 1024)
467
+ models_info.append(f" {model_file.name} ({size_mb:.1f} MB)")
468
+ else:
469
+ models_info.append("\nNo model files found")
470
+
471
+ # Show config
472
+ config = self._load_config()
473
+ embedding_model = config.get("embedding_model", "Not set")
474
+ models_info.append(f"\nConfigured embedding model: {embedding_model}")
475
+
476
+ return "\n".join(models_info)
477
+
478
+ async def _handle_install(self, tool_ctx, force: bool) -> str:
479
+ """Handle node installation."""
480
+ # Check if already exists
481
+ existing = await self._find_node_executable()
482
+ if existing and not force:
483
+ return f"Hanzo node already installed at {existing}. Use force=true to reinstall."
484
+
485
+ # Attempt download/build
486
+ success = await self._download_node(tool_ctx)
487
+ if success:
488
+ return "Successfully installed hanzo-node"
489
+ else:
490
+ return "Failed to install hanzo-node. See logs above for details."
491
+
492
+ async def _handle_clean(self, tool_ctx, force: bool) -> str:
493
+ """Handle cleanup operations."""
494
+ if not force:
495
+ return "Clean operation requires force=true. This will remove logs and temporary files."
496
+
497
+ await tool_ctx.info("Cleaning hanzo-node data...")
498
+
499
+ cleaned = []
500
+
501
+ # Clean logs
502
+ if self.logs_dir.exists():
503
+ for log_file in self.logs_dir.glob("*.log"):
504
+ log_file.unlink()
505
+ cleaned.append(f"Removed log: {log_file.name}")
506
+
507
+ # Clean temporary files
508
+ temp_patterns = ["*.tmp", "*.lock", "*.pid"]
509
+ for pattern in temp_patterns:
510
+ for temp_file in self.hanzo_dir.glob(pattern):
511
+ temp_file.unlink()
512
+ cleaned.append(f"Removed temp file: {temp_file.name}")
513
+
514
+ if cleaned:
515
+ return f"Cleaned {len(cleaned)} files:\n" + "\n".join(cleaned)
516
+ else:
517
+ return "No files to clean"
518
+
519
+ @override
520
+ def register(self, mcp_server: FastMCP) -> None:
521
+ """Register this tool with the MCP server."""
522
+ tool_self = self
523
+
524
+ @mcp_server.tool(name=self.name, description=self.description)
525
+ async def node(
526
+ ctx: MCPContext,
527
+ action: str,
528
+ key: Optional[str] = None,
529
+ value: Optional[str] = None,
530
+ force: bool = False,
531
+ ) -> str:
532
+ return await tool_self.call(
533
+ ctx,
534
+ action=action,
535
+ key=key,
536
+ value=value,
537
+ force=force,
538
+ )
@@ -153,9 +153,7 @@ class ProjectVectorManager:
153
153
 
154
154
  return None
155
155
 
156
- def get_vector_store(
157
- self, project_info: Optional[ProjectInfo] = None
158
- ) -> InfinityVectorStore:
156
+ def get_vector_store(self, project_info: Optional[ProjectInfo] = None) -> InfinityVectorStore:
159
157
  """Get vector store for a project or global store.
160
158
 
161
159
  Args:
@@ -178,9 +176,7 @@ class ProjectVectorManager:
178
176
 
179
177
  if project_key not in self.vector_stores:
180
178
  # Get index path based on configuration
181
- index_path = self.index_config.get_index_path(
182
- "vector", str(project_info.root_path)
183
- )
179
+ index_path = self.index_config.get_index_path("vector", str(project_info.root_path))
184
180
  index_path.mkdir(parents=True, exist_ok=True)
185
181
 
186
182
  self.vector_stores[project_key] = InfinityVectorStore(
@@ -270,9 +266,7 @@ class ProjectVectorManager:
270
266
  search_tasks.append(
271
267
  asyncio.get_event_loop().run_in_executor(
272
268
  self.executor,
273
- lambda: global_store.search(
274
- query, limit_per_project, score_threshold
275
- ),
269
+ lambda: global_store.search(query, limit_per_project, score_threshold),
276
270
  )
277
271
  )
278
272
  project_names.append("global")
@@ -287,9 +281,7 @@ class ProjectVectorManager:
287
281
  search_tasks.append(
288
282
  asyncio.get_event_loop().run_in_executor(
289
283
  self.executor,
290
- lambda vs=vector_store: vs.search(
291
- query, limit_per_project, score_threshold
292
- ),
284
+ lambda vs=vector_store: vs.search(query, limit_per_project, score_threshold),
293
285
  )
294
286
  )
295
287
  project_names.append(project_info.name)