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
@@ -20,9 +20,7 @@ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
20
20
  class RunCommandTool(ShellBaseTool):
21
21
  """Tool for executing shell commands."""
22
22
 
23
- def __init__(
24
- self, permission_manager: Any, command_executor: CommandExecutor
25
- ) -> None:
23
+ def __init__(self, permission_manager: Any, command_executor: CommandExecutor) -> None:
26
24
  """Initialize the run command tool.
27
25
 
28
26
  Args:
@@ -0,0 +1,56 @@
1
+ """Run tool for command execution with automatic backgrounding."""
2
+
3
+ from typing import Optional, override
4
+ from mcp.server import FastMCP
5
+ from mcp.server.fastmcp import Context as MCPContext
6
+
7
+ from hanzo_mcp.tools.shell.zsh_tool import ShellTool
8
+
9
+
10
+ class RunTool(ShellTool):
11
+ """Tool for running commands with automatic backgrounding (alias of shell tool)."""
12
+
13
+ name = "run"
14
+
15
+ def register(self, server: FastMCP) -> None:
16
+ """Register the tool with the MCP server."""
17
+ tool_self = self
18
+
19
+ @server.tool(name=self.name, description=self.description)
20
+ async def run(
21
+ ctx: MCPContext,
22
+ command: str,
23
+ cwd: Optional[str] = None,
24
+ env: Optional[dict[str, str]] = None,
25
+ timeout: Optional[int] = None,
26
+ ) -> str:
27
+ return await tool_self.run(ctx, command=command, cwd=cwd, env=env, timeout=timeout)
28
+
29
+ @property
30
+ @override
31
+ def description(self) -> str:
32
+ """Get the tool description."""
33
+ return """Execute shell commands with automatic backgrounding for long-running processes.
34
+
35
+ Automatically selects the best available shell:
36
+ - Zsh if available (with .zshrc)
37
+ - User's preferred shell ($SHELL)
38
+ - Bash as fallback
39
+
40
+ Commands that run for more than 2 minutes will automatically continue in the background.
41
+ You can check their status and logs using the 'process' tool.
42
+
43
+ Usage:
44
+ run "ls -la"
45
+ run "python server.py" # Auto-backgrounds after 2 minutes
46
+ run "git status && git diff"
47
+ run "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
48
+
49
+ @override
50
+ def get_tool_name(self) -> str:
51
+ """Get the tool name."""
52
+ return "run"
53
+
54
+
55
+ # Create instance
56
+ run_tool = RunTool()
@@ -24,9 +24,7 @@ class SessionManager:
24
24
  _instance: Self | None = None
25
25
  _lock = threading.Lock()
26
26
 
27
- def __new__(
28
- cls, use_singleton: bool = True, session_storage: SessionStorage | None = None
29
- ) -> "SessionManager":
27
+ def __new__(cls, use_singleton: bool = True, session_storage: SessionStorage | None = None) -> "SessionManager":
30
28
  """Create SessionManager instance.
31
29
 
32
30
  Args:
@@ -45,9 +43,7 @@ class SessionManager:
45
43
  cls._instance._initialized = False
46
44
  return cls._instance
47
45
 
48
- def __init__(
49
- self, use_singleton: bool = True, session_storage: SessionStorage | None = None
50
- ) -> None:
46
+ def __init__(self, use_singleton: bool = True, session_storage: SessionStorage | None = None) -> None:
51
47
  """Initialize the session manager.
52
48
 
53
49
  Args:
@@ -152,9 +152,7 @@ class SessionStorageInstance:
152
152
  Returns:
153
153
  Number of sessions cleaned up
154
154
  """
155
- max_age = (
156
- max_age_seconds if max_age_seconds is not None else self.default_ttl_seconds
157
- )
155
+ max_age = max_age_seconds if max_age_seconds is not None else self.default_ttl_seconds
158
156
  current_time = time.time()
159
157
  expired_sessions: list[str] = []
160
158
 
@@ -201,9 +199,7 @@ class SessionStorageInstance:
201
199
  return {
202
200
  "total_sessions": len(self._sessions),
203
201
  "max_sessions": self.max_sessions,
204
- "utilization": (
205
- len(self._sessions) / self.max_sessions if self.max_sessions > 0 else 0
206
- ),
202
+ "utilization": (len(self._sessions) / self.max_sessions if self.max_sessions > 0 else 0),
207
203
  "default_ttl_seconds": self.default_ttl_seconds,
208
204
  }
209
205
 
@@ -117,9 +117,7 @@ class StreamingCommandTool(BaseProcessTool):
117
117
  try:
118
118
  with open(meta_file, "r") as f:
119
119
  meta = json.load(f)
120
- last_accessed = datetime.fromisoformat(
121
- meta.get("last_accessed", "")
122
- )
120
+ last_accessed = datetime.fromisoformat(meta.get("last_accessed", ""))
123
121
  if last_accessed < cutoff:
124
122
  shutil.rmtree(session_dir)
125
123
  except Exception:
@@ -240,9 +238,7 @@ class StreamingCommandTool(BaseProcessTool):
240
238
  }
241
239
 
242
240
  # Execute new command
243
- return await self._execute_new_command(
244
- command, working_dir, timeout, chunk_size
245
- )
241
+ return await self._execute_new_command(command, working_dir, timeout, chunk_size)
246
242
 
247
243
  async def _execute_new_command(
248
244
  self,
@@ -296,18 +292,12 @@ class StreamingCommandTool(BaseProcessTool):
296
292
  f.flush() # Ensure immediate write
297
293
 
298
294
  # Start streaming tasks
299
- stdout_task = asyncio.create_task(
300
- stream_to_file(process.stdout, output_file)
301
- )
302
- stderr_task = asyncio.create_task(
303
- stream_to_file(process.stderr, error_file)
304
- )
295
+ stdout_task = asyncio.create_task(stream_to_file(process.stdout, output_file))
296
+ stderr_task = asyncio.create_task(stream_to_file(process.stderr, error_file))
305
297
 
306
298
  # Wait for initial output or timeout
307
299
  start_time = time.time()
308
- initial_timeout = min(
309
- timeout or 5, 5
310
- ) # Wait max 5 seconds for initial output
300
+ initial_timeout = min(timeout or 5, 5) # Wait max 5 seconds for initial output
311
301
 
312
302
  while time.time() - start_time < initial_timeout:
313
303
  if output_file.stat().st_size > 0 or error_file.stat().st_size > 0:
@@ -487,9 +477,7 @@ class StreamingCommandTool(BaseProcessTool):
487
477
  """Get list of recent commands for hints."""
488
478
  commands = []
489
479
 
490
- for cmd_dir in sorted(
491
- self.commands_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True
492
- )[:limit]:
480
+ for cmd_dir in sorted(self.commands_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)[:limit]:
493
481
  try:
494
482
  with open(cmd_dir / "metadata.json", "r") as f:
495
483
  meta = json.load(f)
@@ -502,11 +490,7 @@ class StreamingCommandTool(BaseProcessTool):
502
490
  commands.append(
503
491
  {
504
492
  "id": meta["command_id"][:8],
505
- "command": (
506
- meta["command"][:50] + "..."
507
- if len(meta["command"]) > 50
508
- else meta["command"]
509
- ),
493
+ "command": (meta["command"][:50] + "..." if len(meta["command"]) > 50 else meta["command"]),
510
494
  "status": meta.get("status", "unknown"),
511
495
  "output_size": output_size,
512
496
  "time": meta.get("start_time", ""),
@@ -135,9 +135,7 @@ For long-running servers, use uvx_background instead.
135
135
 
136
136
  try:
137
137
  # Run installation
138
- install_result = subprocess.run(
139
- install_cmd, shell=True, capture_output=True, text=True, timeout=60
140
- )
138
+ install_result = subprocess.run(install_cmd, shell=True, capture_output=True, text=True, timeout=60)
141
139
 
142
140
  if install_result.returncode == 0:
143
141
  await tool_ctx.info("uvx installed successfully!")
@@ -146,9 +144,7 @@ For long-running servers, use uvx_background instead.
146
144
  import os
147
145
 
148
146
  home = os.path.expanduser("~")
149
- os.environ["PATH"] = (
150
- f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
151
- )
147
+ os.environ["PATH"] = f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
152
148
 
153
149
  # Check again
154
150
  if not shutil.which("uvx"):
@@ -197,9 +193,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
197
193
 
198
194
  try:
199
195
  # Execute command
200
- result = subprocess.run(
201
- cmd, capture_output=True, text=True, timeout=timeout, check=True
202
- )
196
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=True)
203
197
 
204
198
  output = []
205
199
  if result.stdout:
@@ -207,11 +201,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh"""
207
201
  if result.stderr:
208
202
  output.append(f"\nSTDERR:\n{result.stderr}")
209
203
 
210
- return (
211
- "\n".join(output)
212
- if output
213
- else "Command completed successfully with no output."
214
- )
204
+ return "\n".join(output) if output else "Command completed successfully with no output."
215
205
 
216
206
  except subprocess.TimeoutExpired:
217
207
  return f"Error: Command timed out after {timeout} seconds. Use uvx_background for long-running processes."
@@ -156,9 +156,7 @@ Use 'processes' to list running processes and 'pkill' to stop them.
156
156
 
157
157
  try:
158
158
  # Run installation
159
- install_result = subprocess.run(
160
- install_cmd, shell=True, capture_output=True, text=True, timeout=60
161
- )
159
+ install_result = subprocess.run(install_cmd, shell=True, capture_output=True, text=True, timeout=60)
162
160
 
163
161
  if install_result.returncode == 0:
164
162
  await tool_ctx.info("uvx installed successfully!")
@@ -167,9 +165,7 @@ Use 'processes' to list running processes and 'pkill' to stop them.
167
165
  import os
168
166
 
169
167
  home = os.path.expanduser("~")
170
- os.environ["PATH"] = (
171
- f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
172
- )
168
+ os.environ["PATH"] = f"{home}/.cargo/bin:{os.environ.get('PATH', '')}"
173
169
 
174
170
  # Check again
175
171
  if not shutil.which("uvx"):
@@ -86,9 +86,7 @@ uvx jupyter lab --port 8888 # Auto-backgrounds if needed"""
86
86
  cwd: Optional[str] = None,
87
87
  python: Optional[str] = None,
88
88
  ) -> str:
89
- return await tool_self.run(
90
- ctx, package=package, args=args, cwd=cwd, python=python
91
- )
89
+ return await tool_self.run(ctx, package=package, args=args, cwd=cwd, python=python)
92
90
 
93
91
  async def call(self, ctx: MCPContext, **params) -> str:
94
92
  """Call the tool with arguments."""
@@ -29,9 +29,7 @@ class ZshTool(BaseScriptTool):
29
29
  env: Optional[dict[str, str]] = None,
30
30
  timeout: Optional[int] = None,
31
31
  ) -> str:
32
- return await tool_self.run(
33
- ctx, command=command, cwd=cwd, env=env, timeout=timeout
34
- )
32
+ return await tool_self.run(ctx, command=command, cwd=cwd, env=env, timeout=timeout)
35
33
 
36
34
  async def call(self, ctx: MCPContext, **params) -> str:
37
35
  """Call the tool with arguments."""
@@ -79,12 +77,12 @@ zsh "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
79
77
  return path
80
78
  # Fall back to bash if no zsh found
81
79
  return "bash"
82
-
80
+
83
81
  # On Unix-like systems, check for zsh
84
82
  zsh_path = shutil.which("zsh")
85
83
  if zsh_path:
86
84
  return zsh_path
87
-
85
+
88
86
  # Fall back to bash if zsh not found
89
87
  return "bash"
90
88
 
@@ -124,14 +122,12 @@ zsh "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
124
122
  # Check if zsh is available
125
123
  if not shutil.which("zsh") and platform.system() != "Windows":
126
124
  return "Error: Zsh is not installed. Please install zsh first."
127
-
125
+
128
126
  # Prepare working directory
129
127
  work_dir = Path(cwd).resolve() if cwd else Path.cwd()
130
128
 
131
129
  # Use execute_sync which has auto-backgrounding
132
- output = await self.execute_sync(
133
- command, cwd=work_dir, env=env, timeout=timeout
134
- )
130
+ output = await self.execute_sync(command, cwd=work_dir, env=env, timeout=timeout)
135
131
  return output if output else "Command completed successfully (no output)"
136
132
 
137
133
 
@@ -152,12 +148,12 @@ class ShellTool(BaseScriptTool):
152
148
  # Also check if .zshrc exists
153
149
  if (Path.home() / ".zshrc").exists():
154
150
  return "zsh"
155
-
151
+
156
152
  # Check for user's preferred shell
157
153
  user_shell = os.environ.get("SHELL", "")
158
154
  if user_shell and Path(user_shell).exists():
159
155
  return user_shell
160
-
156
+
161
157
  # Default to bash
162
158
  return "bash"
163
159
 
@@ -173,9 +169,7 @@ class ShellTool(BaseScriptTool):
173
169
  env: Optional[dict[str, str]] = None,
174
170
  timeout: Optional[int] = None,
175
171
  ) -> str:
176
- return await tool_self.run(
177
- ctx, command=command, cwd=cwd, env=env, timeout=timeout
178
- )
172
+ return await tool_self.run(ctx, command=command, cwd=cwd, env=env, timeout=timeout)
179
173
 
180
174
  async def call(self, ctx: MCPContext, **params) -> str:
181
175
  """Call the tool with arguments."""
@@ -249,12 +243,10 @@ shell "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
249
243
 
250
244
  # Add shell info to output if verbose
251
245
  shell_name = os.path.basename(self._best_shell)
252
-
246
+
253
247
  # Use execute_sync which has auto-backgrounding
254
- output = await self.execute_sync(
255
- command, cwd=work_dir, env=env, timeout=timeout
256
- )
257
-
248
+ output = await self.execute_sync(command, cwd=work_dir, env=env, timeout=timeout)
249
+
258
250
  if output:
259
251
  return output
260
252
  else:
@@ -263,4 +255,4 @@ shell "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
263
255
 
264
256
  # Create tool instances
265
257
  zsh_tool = ZshTool()
266
- shell_tool = ShellTool() # Smart shell that prefers zsh
258
+ shell_tool = ShellTool() # Smart shell that prefers zsh
@@ -161,9 +161,7 @@ todo --filter in_progress
161
161
  priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(
162
162
  todo.get("priority", "medium"), "⚪"
163
163
  )
164
- output.append(
165
- f"{priority_icon} [{todo['id'][:8]}] {todo['content']}"
166
- )
164
+ output.append(f"{priority_icon} [{todo['id'][:8]}] {todo['content']}")
167
165
 
168
166
  # Summary
169
167
  output.append(
@@ -4,15 +4,26 @@ This package provides tools for working with local vector databases for semantic
4
4
  document indexing, and retrieval-augmented generation (RAG) workflows.
5
5
 
6
6
  Supported backends:
7
- - Infinity database (default) - High-performance local vector database
7
+ - LanceDB (primary) - High-performance embedded vector database
8
+ - Hanzo-node (optional) - Distributed vector processing node
9
+ - Infinity database (legacy) - High-performance local vector database
8
10
  """
9
11
 
10
12
  from mcp.server import FastMCP
11
13
 
12
- from hanzo_mcp.tools.common.base import BaseTool
14
+ from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
13
15
  from hanzo_mcp.tools.common.permissions import PermissionManager
14
16
 
15
- # Try to import vector dependencies
17
+ # Try to import unified vector tools first
18
+ try:
19
+ from .unified_vector import UnifiedVectorTool
20
+ from .node_tool import NodeTool
21
+
22
+ UNIFIED_TOOLS_AVAILABLE = True
23
+ except ImportError:
24
+ UNIFIED_TOOLS_AVAILABLE = False
25
+
26
+ # Try to import legacy vector dependencies
16
27
  try:
17
28
  from .index_tool import IndexTool
18
29
  from .vector_index import VectorIndexTool
@@ -20,35 +31,73 @@ try:
20
31
  from .infinity_store import InfinityVectorStore
21
32
  from .project_manager import ProjectVectorManager
22
33
 
23
- VECTOR_AVAILABLE = True
24
-
25
- def register_vector_tools(
26
- mcp_server: FastMCP,
27
- permission_manager: PermissionManager,
28
- vector_config: dict | None = None,
29
- enabled_tools: dict[str, bool] | None = None,
30
- search_paths: list[str] | None = None,
31
- project_manager: "ProjectVectorManager | None" = None,
32
- ) -> list[BaseTool]:
33
- """Register vector database tools with the MCP server.
34
-
35
- Args:
36
- mcp_server: The FastMCP server instance
37
- permission_manager: Permission manager for access control
38
- vector_config: Vector store configuration
39
- enabled_tools: Dictionary of individual tool enable states
40
- search_paths: Paths to search for projects (default: None, uses allowed paths)
41
- project_manager: Optional existing project manager to reuse
42
-
43
- Returns:
44
- List of registered tools
45
- """
34
+ LEGACY_VECTOR_AVAILABLE = True
35
+ except ImportError:
36
+ LEGACY_VECTOR_AVAILABLE = False
37
+
38
+
39
+ def register_vector_tools(
40
+ mcp_server: FastMCP,
41
+ permission_manager: PermissionManager | None = None,
42
+ vector_config: dict | None = None,
43
+ enabled_tools: dict[str, bool] | None = None,
44
+ search_paths: list[str] | None = None,
45
+ project_manager: "ProjectVectorManager | None" = None,
46
+ user_id: str = "default",
47
+ project_id: str = "default",
48
+ use_unified: bool = True,
49
+ ) -> list[BaseTool]:
50
+ """Register vector database tools with the MCP server.
51
+
52
+ Args:
53
+ mcp_server: The FastMCP server instance
54
+ permission_manager: Permission manager for access control (optional for unified tools)
55
+ vector_config: Vector store configuration
56
+ enabled_tools: Dictionary of individual tool enable states
57
+ search_paths: Paths to search for projects (default: None, uses allowed paths)
58
+ project_manager: Optional existing project manager to reuse
59
+ user_id: User ID for unified tools
60
+ project_id: Project ID for unified tools
61
+ use_unified: Whether to use unified tools (default: True)
62
+
63
+ Returns:
64
+ List of registered tools
65
+ """
66
+ tools = []
67
+
68
+ # Prefer unified tools if available and enabled
69
+ if use_unified and UNIFIED_TOOLS_AVAILABLE:
70
+ tool_enabled = enabled_tools or {}
71
+
72
+ # Register unified vector tool (consolidates vector_search, vector_index, memory ops)
73
+ if tool_enabled.get("vector", True):
74
+ unified_vector = UnifiedVectorTool(user_id=user_id, project_id=project_id)
75
+ ToolRegistry.register_tool(mcp_server, unified_vector)
76
+ tools.append(unified_vector)
77
+
78
+ # Register node management tool
79
+ if tool_enabled.get("node", True):
80
+ node_tool = NodeTool()
81
+ ToolRegistry.register_tool(mcp_server, node_tool)
82
+ tools.append(node_tool)
83
+
84
+ import logging
85
+ logger = logging.getLogger(__name__)
86
+ logger.info(f"Registered {len(tools)} unified vector tools")
87
+
88
+ # Fall back to legacy tools if unified not available or disabled
89
+ elif not use_unified and LEGACY_VECTOR_AVAILABLE:
46
90
  if not vector_config or not vector_config.get("enabled", False):
47
91
  return []
48
92
 
93
+ if not permission_manager:
94
+ import logging
95
+ logger = logging.getLogger(__name__)
96
+ logger.warning("Permission manager required for legacy vector tools")
97
+ return []
98
+
49
99
  # Check individual tool enablement
50
100
  tool_enabled = enabled_tools or {}
51
- tools = []
52
101
 
53
102
  # Use provided project manager or create new one
54
103
  if project_manager is None:
@@ -56,9 +105,7 @@ try:
56
105
  store_config = vector_config.copy()
57
106
  project_manager = ProjectVectorManager(
58
107
  global_db_path=store_config.get("data_path"),
59
- embedding_model=store_config.get(
60
- "embedding_model", "text-embedding-3-small"
61
- ),
108
+ embedding_model=store_config.get("embedding_model", "text-embedding-3-small"),
62
109
  dimension=store_config.get("dimension", 1536),
63
110
  )
64
111
 
@@ -68,9 +115,7 @@ try:
68
115
  import logging
69
116
 
70
117
  logger = logging.getLogger(__name__)
71
- logger.info(
72
- f"Detected {len(detected_projects)} projects with LLM.md files"
73
- )
118
+ logger.info(f"Detected {len(detected_projects)} projects with LLM.md files")
74
119
 
75
120
  # Register individual tools if enabled
76
121
  if tool_enabled.get("index", True):
@@ -83,32 +128,34 @@ try:
83
128
  tools.append(VectorSearchTool(permission_manager, project_manager))
84
129
 
85
130
  # Register with MCP server
86
- from hanzo_mcp.tools.common.base import ToolRegistry
87
-
88
131
  ToolRegistry.register_tools(mcp_server, tools)
89
-
90
- return tools
91
-
92
- except ImportError:
93
- VECTOR_AVAILABLE = False
94
-
95
- def register_vector_tools(*args, **kwargs) -> list[BaseTool]:
96
- """Vector tools not available - missing dependencies."""
132
+
133
+ else:
97
134
  import logging
98
-
99
135
  logger = logging.getLogger(__name__)
100
- logger.warning(
101
- "Vector tools not available. Install infinity-embedded: pip install infinity-embedded"
102
- )
103
- return []
136
+ if not UNIFIED_TOOLS_AVAILABLE and not LEGACY_VECTOR_AVAILABLE:
137
+ logger.warning("No vector tools available. Install hanzo-memory package or infinity-embedded.")
138
+ elif not use_unified:
139
+ logger.info("Unified vector tools disabled, legacy tools not available")
140
+
141
+ return tools
104
142
 
105
143
 
106
144
  __all__ = [
107
145
  "register_vector_tools",
108
- "VECTOR_AVAILABLE",
146
+ "UNIFIED_TOOLS_AVAILABLE",
147
+ "LEGACY_VECTOR_AVAILABLE",
109
148
  ]
110
149
 
111
- if VECTOR_AVAILABLE:
150
+ if UNIFIED_TOOLS_AVAILABLE:
151
+ __all__.extend(
152
+ [
153
+ "UnifiedVectorTool",
154
+ "NodeTool",
155
+ ]
156
+ )
157
+
158
+ if LEGACY_VECTOR_AVAILABLE:
112
159
  __all__.extend(
113
160
  [
114
161
  "InfinityVectorStore",
@@ -130,9 +130,7 @@ class ASTAnalyzer:
130
130
  return self._analyze_python_file(file_path, content, file_hash)
131
131
  else:
132
132
  # Generic analysis for other languages
133
- return self._analyze_generic_file(
134
- file_path, content, file_hash, language
135
- )
133
+ return self._analyze_generic_file(file_path, content, file_hash, language)
136
134
 
137
135
  except Exception as e:
138
136
  import logging
@@ -177,9 +175,7 @@ class ASTAnalyzer:
177
175
 
178
176
  return language_map.get(extension)
179
177
 
180
- def _analyze_python_file(
181
- self, file_path: str, content: str, file_hash: str
182
- ) -> FileAST:
178
+ def _analyze_python_file(self, file_path: str, content: str, file_hash: str) -> FileAST:
183
179
  """Analyze Python file using both AST and tree-sitter."""
184
180
  symbols = []
185
181
  ast_nodes = []
@@ -228,9 +224,7 @@ class ASTAnalyzer:
228
224
  dependencies=dependencies,
229
225
  )
230
226
 
231
- def _analyze_generic_file(
232
- self, file_path: str, content: str, file_hash: str, language: str
233
- ) -> FileAST:
227
+ def _analyze_generic_file(self, file_path: str, content: str, file_hash: str, language: str) -> FileAST:
234
228
  """Generic analysis for non-Python files."""
235
229
  # For now, just basic line-based analysis
236
230
  # Could be enhanced with language-specific parsers
@@ -248,11 +242,7 @@ class ASTAnalyzer:
248
242
 
249
243
  # Basic function detection (works for many C-style languages)
250
244
  if language in ["javascript", "typescript", "java", "cpp", "c"]:
251
- if (
252
- "function " in line
253
- or line.startswith("def ")
254
- or " function(" in line
255
- ):
245
+ if "function " in line or line.startswith("def ") or " function(" in line:
256
246
  # Extract function name
257
247
  parts = line.split()
258
248
  for j, part in enumerate(parts):
@@ -399,9 +389,7 @@ class PythonSymbolExtractor(ast.NodeVisitor):
399
389
 
400
390
  # Extract base classes
401
391
  bases = [self._get_name(base) for base in node.bases]
402
- signature = (
403
- f"class {node.name}({', '.join(bases)})" if bases else f"class {node.name}"
404
- )
392
+ signature = f"class {node.name}({', '.join(bases)})" if bases else f"class {node.name}"
405
393
 
406
394
  symbol = Symbol(
407
395
  name=node.name,
@@ -441,9 +429,7 @@ class PythonSymbolExtractor(ast.NodeVisitor):
441
429
 
442
430
  for alias in node.names:
443
431
  if alias.name != "*":
444
- import_item = (
445
- f"{node.module}.{alias.name}" if node.module else alias.name
446
- )
432
+ import_item = f"{node.module}.{alias.name}" if node.module else alias.name
447
433
  self.imports.append(import_item)
448
434
 
449
435
  def visit_Assign(self, node):