hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.2__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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/analytics/posthog_analytics.py +14 -1
- hanzo_mcp/cli.py +108 -4
- hanzo_mcp/server.py +11 -0
- hanzo_mcp/tools/__init__.py +3 -16
- hanzo_mcp/tools/agent/__init__.py +5 -0
- hanzo_mcp/tools/agent/agent.py +5 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -17
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
- hanzo_mcp/tools/agent/clarification_tool.py +7 -1
- hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
- hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
- hanzo_mcp/tools/agent/cli_tools.py +26 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
- hanzo_mcp/tools/agent/critic_tool.py +7 -1
- hanzo_mcp/tools/agent/iching_tool.py +5 -0
- hanzo_mcp/tools/agent/network_tool.py +5 -0
- hanzo_mcp/tools/agent/review_tool.py +7 -1
- hanzo_mcp/tools/agent/swarm_alias.py +5 -0
- hanzo_mcp/tools/agent/swarm_tool.py +701 -0
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
- hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
- hanzo_mcp/tools/common/auto_timeout.py +254 -0
- hanzo_mcp/tools/common/base.py +4 -0
- hanzo_mcp/tools/common/batch_tool.py +5 -0
- hanzo_mcp/tools/common/config_tool.py +5 -0
- hanzo_mcp/tools/common/critic_tool.py +5 -0
- hanzo_mcp/tools/common/paginated_base.py +4 -0
- hanzo_mcp/tools/common/permissions.py +38 -12
- hanzo_mcp/tools/common/personality.py +673 -980
- hanzo_mcp/tools/common/stats.py +5 -0
- hanzo_mcp/tools/common/thinking_tool.py +5 -0
- hanzo_mcp/tools/common/timeout_parser.py +103 -0
- hanzo_mcp/tools/common/tool_disable.py +5 -0
- hanzo_mcp/tools/common/tool_enable.py +5 -0
- hanzo_mcp/tools/common/tool_list.py +5 -0
- hanzo_mcp/tools/config/config_tool.py +5 -0
- hanzo_mcp/tools/config/mode_tool.py +5 -0
- hanzo_mcp/tools/database/graph.py +5 -0
- hanzo_mcp/tools/database/graph_add.py +5 -0
- hanzo_mcp/tools/database/graph_query.py +5 -0
- hanzo_mcp/tools/database/graph_remove.py +5 -0
- hanzo_mcp/tools/database/graph_search.py +5 -0
- hanzo_mcp/tools/database/graph_stats.py +5 -0
- hanzo_mcp/tools/database/sql.py +5 -0
- hanzo_mcp/tools/database/sql_query.py +2 -0
- hanzo_mcp/tools/database/sql_search.py +5 -0
- hanzo_mcp/tools/database/sql_stats.py +5 -0
- hanzo_mcp/tools/editor/neovim_command.py +5 -0
- hanzo_mcp/tools/editor/neovim_edit.py +7 -2
- hanzo_mcp/tools/editor/neovim_session.py +5 -0
- hanzo_mcp/tools/filesystem/__init__.py +23 -26
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
- hanzo_mcp/tools/filesystem/base.py +2 -18
- hanzo_mcp/tools/filesystem/batch_search.py +825 -0
- hanzo_mcp/tools/filesystem/content_replace.py +5 -3
- hanzo_mcp/tools/filesystem/diff.py +5 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
- hanzo_mcp/tools/filesystem/edit.py +6 -5
- hanzo_mcp/tools/filesystem/find.py +177 -311
- hanzo_mcp/tools/filesystem/find_files.py +370 -0
- hanzo_mcp/tools/filesystem/git_search.py +5 -3
- hanzo_mcp/tools/filesystem/grep.py +454 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
- hanzo_mcp/tools/filesystem/read.py +10 -9
- hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
- hanzo_mcp/tools/filesystem/search_tool.py +728 -0
- hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
- hanzo_mcp/tools/filesystem/tree.py +273 -0
- hanzo_mcp/tools/filesystem/watch.py +6 -1
- hanzo_mcp/tools/filesystem/write.py +13 -7
- hanzo_mcp/tools/jupyter/jupyter.py +30 -2
- hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
- hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
- hanzo_mcp/tools/llm/consensus_tool.py +8 -6
- hanzo_mcp/tools/llm/llm_manage.py +5 -0
- hanzo_mcp/tools/llm/llm_tool.py +2 -0
- hanzo_mcp/tools/llm/llm_unified.py +5 -0
- hanzo_mcp/tools/llm/provider_tools.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
- hanzo_mcp/tools/mcp/mcp_add.py +7 -2
- hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
- hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
- hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
- hanzo_mcp/tools/memory/memory_tools.py +17 -0
- hanzo_mcp/tools/search/find_tool.py +5 -3
- hanzo_mcp/tools/search/unified_search.py +3 -1
- hanzo_mcp/tools/shell/__init__.py +2 -14
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_tool.py +2 -0
- hanzo_mcp/tools/shell/command_executor.py +7 -7
- hanzo_mcp/tools/shell/logs.py +5 -0
- hanzo_mcp/tools/shell/npx.py +5 -0
- hanzo_mcp/tools/shell/npx_background.py +5 -0
- hanzo_mcp/tools/shell/npx_tool.py +5 -0
- hanzo_mcp/tools/shell/open.py +5 -0
- hanzo_mcp/tools/shell/pkill.py +5 -0
- hanzo_mcp/tools/shell/process_tool.py +5 -0
- hanzo_mcp/tools/shell/processes.py +5 -0
- hanzo_mcp/tools/shell/run_background.py +5 -0
- hanzo_mcp/tools/shell/run_command.py +2 -0
- hanzo_mcp/tools/shell/run_command_windows.py +5 -0
- hanzo_mcp/tools/shell/streaming_command.py +5 -0
- hanzo_mcp/tools/shell/uvx.py +5 -0
- hanzo_mcp/tools/shell/uvx_background.py +5 -0
- hanzo_mcp/tools/shell/uvx_tool.py +5 -0
- hanzo_mcp/tools/shell/zsh_tool.py +3 -0
- hanzo_mcp/tools/todo/todo.py +5 -0
- hanzo_mcp/tools/todo/todo_read.py +142 -0
- hanzo_mcp/tools/todo/todo_write.py +367 -0
- hanzo_mcp/tools/vector/__init__.py +42 -95
- hanzo_mcp/tools/vector/index_tool.py +5 -0
- hanzo_mcp/tools/vector/vector.py +5 -0
- hanzo_mcp/tools/vector/vector_index.py +5 -0
- hanzo_mcp/tools/vector/vector_search.py +5 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
- hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
- hanzo_mcp/tools/common/path_utils.py +0 -34
- hanzo_mcp/tools/compiler/__init__.py +0 -8
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
- hanzo_mcp/tools/environment/__init__.py +0 -8
- hanzo_mcp/tools/environment/environment_detector.py +0 -594
- hanzo_mcp/tools/filesystem/search.py +0 -1160
- hanzo_mcp/tools/framework/__init__.py +0 -8
- hanzo_mcp/tools/framework/framework_modes.py +0 -714
- hanzo_mcp/tools/memory/conversation_memory.py +0 -636
- hanzo_mcp/tools/shell/run_tool.py +0 -56
- hanzo_mcp/tools/vector/node_tool.py +0 -538
- hanzo_mcp/tools/vector/unified_vector.py +0 -384
- hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
|
@@ -1,538 +0,0 @@
|
|
|
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
|
-
)
|