hanzo-mcp 0.8.11__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 (166) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +3 -9
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +6 -15
  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 +1 -3
  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 +2 -6
  17. hanzo_mcp/tools/__init__.py +26 -27
  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 +22 -15
  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 +75 -74
  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 +6 -18
  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 +1 -3
  101. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  102. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  103. hanzo_mcp/tools/memory/__init__.py +10 -27
  104. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  105. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  106. hanzo_mcp/tools/memory/memory_tools.py +6 -18
  107. hanzo_mcp/tools/search/find_tool.py +12 -34
  108. hanzo_mcp/tools/search/unified_search.py +24 -78
  109. hanzo_mcp/tools/shell/__init__.py +16 -4
  110. hanzo_mcp/tools/shell/auto_background.py +2 -6
  111. hanzo_mcp/tools/shell/base.py +1 -5
  112. hanzo_mcp/tools/shell/base_process.py +5 -7
  113. hanzo_mcp/tools/shell/bash_session.py +7 -24
  114. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  115. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  116. hanzo_mcp/tools/shell/command_executor.py +26 -79
  117. hanzo_mcp/tools/shell/logs.py +4 -16
  118. hanzo_mcp/tools/shell/npx.py +2 -8
  119. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  120. hanzo_mcp/tools/shell/pkill.py +4 -12
  121. hanzo_mcp/tools/shell/process_tool.py +2 -8
  122. hanzo_mcp/tools/shell/processes.py +5 -17
  123. hanzo_mcp/tools/shell/run_background.py +1 -3
  124. hanzo_mcp/tools/shell/run_command.py +1 -3
  125. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  126. hanzo_mcp/tools/shell/run_tool.py +56 -0
  127. hanzo_mcp/tools/shell/session_manager.py +2 -6
  128. hanzo_mcp/tools/shell/session_storage.py +2 -6
  129. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  130. hanzo_mcp/tools/shell/uvx.py +4 -14
  131. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  132. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  133. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  134. hanzo_mcp/tools/todo/todo.py +1 -3
  135. hanzo_mcp/tools/vector/__init__.py +97 -50
  136. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  137. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  138. hanzo_mcp/tools/vector/index_tool.py +3 -9
  139. hanzo_mcp/tools/vector/infinity_store.py +7 -27
  140. hanzo_mcp/tools/vector/mock_infinity.py +1 -3
  141. hanzo_mcp/tools/vector/node_tool.py +538 -0
  142. hanzo_mcp/tools/vector/project_manager.py +4 -12
  143. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  144. hanzo_mcp/tools/vector/vector.py +2 -6
  145. hanzo_mcp/tools/vector/vector_index.py +8 -8
  146. hanzo_mcp/tools/vector/vector_search.py +7 -21
  147. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  148. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  149. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  150. hanzo_mcp/tools/agent/swarm_tool.py +0 -718
  151. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  152. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  153. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  154. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  155. hanzo_mcp/tools/filesystem/grep.py +0 -467
  156. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  157. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  158. hanzo_mcp/tools/filesystem/tree.py +0 -270
  159. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  160. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  161. hanzo_mcp/tools/todo/todo_read.py +0 -143
  162. hanzo_mcp/tools/todo/todo_write.py +0 -374
  163. hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
  164. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  165. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  166. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
@@ -151,13 +151,9 @@ Usage:
151
151
  if not force:
152
152
  stats = await vector_store.get_stats()
153
153
  if stats and stats.get("document_count", 0) > 0:
154
- await tool_ctx.info(
155
- "Project already indexed, use --force to re-index"
156
- )
154
+ await tool_ctx.info("Project already indexed, use --force to re-index")
157
155
  if show_stats:
158
- return self._format_stats(
159
- stats, abs_path, time.time() - start_time
160
- )
156
+ return self._format_stats(stats, abs_path, time.time() - start_time)
161
157
  return "Project is already indexed. Use --force to re-index."
162
158
 
163
159
  # Prepare file patterns
@@ -268,9 +264,7 @@ Usage:
268
264
  except Exception as e:
269
265
  errors.append(f"{file_path}: {str(e)}")
270
266
 
271
- await tool_ctx.info(
272
- f"Indexed {indexed_files} files ({total_size / 1024 / 1024:.1f} MB)"
273
- )
267
+ await tool_ctx.info(f"Indexed {indexed_files} files ({total_size / 1024 / 1024:.1f} MB)")
274
268
 
275
269
  # Index git history if requested
276
270
  git_stats = {}
@@ -79,9 +79,7 @@ class InfinityVectorStore:
79
79
  dimension: Vector dimension (must match embedding model)
80
80
  """
81
81
  if not INFINITY_AVAILABLE:
82
- raise ImportError(
83
- "infinity_embedded is required for vector store functionality"
84
- )
82
+ raise ImportError("infinity_embedded is required for vector store functionality")
85
83
 
86
84
  # Set up data path
87
85
  if data_path:
@@ -172,17 +170,13 @@ class InfinityVectorStore:
172
170
  "source_file": {"type": "varchar"},
173
171
  "target_file": {"type": "varchar"},
174
172
  "symbol_name": {"type": "varchar"},
175
- "reference_type": {
176
- "type": "varchar"
177
- }, # import, call, inheritance, etc.
173
+ "reference_type": {"type": "varchar"}, # import, call, inheritance, etc.
178
174
  "line_number": {"type": "integer"},
179
175
  "metadata": {"type": "varchar"}, # JSON string
180
176
  },
181
177
  )
182
178
 
183
- def _generate_doc_id(
184
- self, content: str, file_path: str = "", chunk_index: int = 0
185
- ) -> str:
179
+ def _generate_doc_id(self, content: str, file_path: str = "", chunk_index: int = 0) -> str:
186
180
  """Generate a unique document ID."""
187
181
  content_hash = hashlib.sha256(content.encode()).hexdigest()[:16]
188
182
  path_hash = hashlib.sha256(file_path.encode()).hexdigest()[:8]
@@ -534,11 +528,7 @@ class InfinityVectorStore:
534
528
  FileAST object if file found, None otherwise
535
529
  """
536
530
  try:
537
- results = (
538
- self.ast_table.output(["*"])
539
- .filter(f"file_path = '{file_path}'")
540
- .to_pl()
541
- )
531
+ results = self.ast_table.output(["*"]).filter(f"file_path = '{file_path}'").to_pl()
542
532
 
543
533
  if len(results) == 0:
544
534
  return None
@@ -577,11 +567,7 @@ class InfinityVectorStore:
577
567
  List of reference information
578
568
  """
579
569
  try:
580
- results = (
581
- self.references_table.output(["*"])
582
- .filter(f"target_file = '{file_path}'")
583
- .to_pl()
584
- )
570
+ results = self.references_table.output(["*"]).filter(f"target_file = '{file_path}'").to_pl()
585
571
 
586
572
  references = []
587
573
  for row in results.iter_rows(named=True):
@@ -696,11 +682,7 @@ class InfinityVectorStore:
696
682
  """
697
683
  try:
698
684
  # Get count first
699
- results = (
700
- self.documents_table.output(["id"])
701
- .filter(f"file_path = '{file_path}'")
702
- .to_pl()
703
- )
685
+ results = self.documents_table.output(["id"]).filter(f"file_path = '{file_path}'").to_pl()
704
686
  count = len(results)
705
687
 
706
688
  # Delete all documents for this file
@@ -726,9 +708,7 @@ class InfinityVectorStore:
726
708
  metadata = json.loads(row["metadata"])
727
709
  files[file_path] = {
728
710
  "file_path": file_path,
729
- "file_name": metadata.get(
730
- "file_name", Path(file_path).name
731
- ),
711
+ "file_name": metadata.get("file_name", Path(file_path).name),
732
712
  "file_size": metadata.get("file_size", 0),
733
713
  "total_chunks": metadata.get("total_chunks", 1),
734
714
  }
@@ -58,9 +58,7 @@ class MockQuery:
58
58
  self.filters.append(condition)
59
59
  return self
60
60
 
61
- def match_dense(
62
- self, column: str, vector: List[float], dtype: str, metric: str, limit: int
63
- ):
61
+ def match_dense(self, column: str, vector: List[float], dtype: str, metric: str, limit: int):
64
62
  """Add vector search."""
65
63
  self.vector_search = {
66
64
  "column": column,
@@ -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
+ )