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.
- hanzo_mcp/__init__.py +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +3 -9
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +6 -15
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +1 -3
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +2 -6
- hanzo_mcp/tools/__init__.py +26 -27
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +22 -15
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +75 -74
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +6 -18
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +1 -3
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +10 -27
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +6 -18
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +24 -78
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +7 -27
- hanzo_mcp/tools/vector/mock_infinity.py +1 -3
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -718
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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", ""),
|
hanzo_mcp/tools/shell/uvx.py
CHANGED
|
@@ -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
|
-
|
|
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
|
hanzo_mcp/tools/todo/todo.py
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
"
|
|
146
|
+
"UNIFIED_TOOLS_AVAILABLE",
|
|
147
|
+
"LEGACY_VECTOR_AVAILABLE",
|
|
109
148
|
]
|
|
110
149
|
|
|
111
|
-
if
|
|
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):
|
|
@@ -153,16 +153,12 @@ class GitIngester:
|
|
|
153
153
|
)
|
|
154
154
|
return result.stdout.strip()
|
|
155
155
|
|
|
156
|
-
def _get_repository_files(
|
|
157
|
-
self, repo_path: Path, patterns: Optional[List[str]] = None
|
|
158
|
-
) -> List[Path]:
|
|
156
|
+
def _get_repository_files(self, repo_path: Path, patterns: Optional[List[str]] = None) -> List[Path]:
|
|
159
157
|
"""Get list of files in repository matching patterns."""
|
|
160
158
|
# Use git ls-files to respect .gitignore
|
|
161
159
|
cmd = ["git", "ls-files"]
|
|
162
160
|
|
|
163
|
-
result = subprocess.run(
|
|
164
|
-
cmd, cwd=repo_path, capture_output=True, text=True, check=True
|
|
165
|
-
)
|
|
161
|
+
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True, check=True)
|
|
166
162
|
|
|
167
163
|
files = []
|
|
168
164
|
for line in result.stdout.strip().split("\n"):
|
|
@@ -178,9 +174,7 @@ class GitIngester:
|
|
|
178
174
|
|
|
179
175
|
return files
|
|
180
176
|
|
|
181
|
-
def _get_commit_history(
|
|
182
|
-
self, repo_path: Path, branch: str = "HEAD", max_commits: int = 1000
|
|
183
|
-
) -> List[GitCommit]:
|
|
177
|
+
def _get_commit_history(self, repo_path: Path, branch: str = "HEAD", max_commits: int = 1000) -> List[GitCommit]:
|
|
184
178
|
"""Get commit history for the repository."""
|
|
185
179
|
# Get commit list with basic info
|
|
186
180
|
result = subprocess.run(
|
|
@@ -222,9 +216,7 @@ class GitIngester:
|
|
|
222
216
|
|
|
223
217
|
return commits
|
|
224
218
|
|
|
225
|
-
def _get_commit_files(
|
|
226
|
-
self, repo_path: Path, commit_hash: str
|
|
227
|
-
) -> List[Dict[str, str]]:
|
|
219
|
+
def _get_commit_files(self, repo_path: Path, commit_hash: str) -> List[Dict[str, str]]:
|
|
228
220
|
"""Get list of files changed in a commit."""
|
|
229
221
|
result = subprocess.run(
|
|
230
222
|
["git", "show", "--name-status", "--format=", commit_hash],
|
|
@@ -275,21 +267,15 @@ class GitIngester:
|
|
|
275
267
|
if history:
|
|
276
268
|
metadata["first_commit"] = history[-1]["hash"]
|
|
277
269
|
metadata["last_commit"] = history[0]["hash"]
|
|
278
|
-
metadata["last_modified"] = datetime.fromtimestamp(
|
|
279
|
-
history[0]["timestamp"]
|
|
280
|
-
).isoformat()
|
|
270
|
+
metadata["last_modified"] = datetime.fromtimestamp(history[0]["timestamp"]).isoformat()
|
|
281
271
|
|
|
282
272
|
# Add blame information if requested
|
|
283
273
|
if include_blame:
|
|
284
274
|
blame_data = self._get_file_blame(repo_path, relative_path)
|
|
285
|
-
metadata["unique_authors"] = len(
|
|
286
|
-
set(b["author"] for b in blame_data.values())
|
|
287
|
-
)
|
|
275
|
+
metadata["unique_authors"] = len(set(b["author"] for b in blame_data.values()))
|
|
288
276
|
|
|
289
277
|
# Index the file content
|
|
290
|
-
doc_ids = self.vector_store.add_file(
|
|
291
|
-
str(file_path), chunk_size=1000, chunk_overlap=200, metadata=metadata
|
|
292
|
-
)
|
|
278
|
+
doc_ids = self.vector_store.add_file(str(file_path), chunk_size=1000, chunk_overlap=200, metadata=metadata)
|
|
293
279
|
results["files_indexed"] += 1
|
|
294
280
|
|
|
295
281
|
# Perform AST analysis for supported languages
|
|
@@ -309,9 +295,7 @@ class GitIngester:
|
|
|
309
295
|
except Exception as e:
|
|
310
296
|
logger.warning(f"AST analysis failed for {file_path}: {e}")
|
|
311
297
|
|
|
312
|
-
def _get_file_history(
|
|
313
|
-
self, repo_path: Path, file_path: Path
|
|
314
|
-
) -> List[Dict[str, Any]]:
|
|
298
|
+
def _get_file_history(self, repo_path: Path, file_path: Path) -> List[Dict[str, Any]]:
|
|
315
299
|
"""Get commit history for a specific file."""
|
|
316
300
|
result = subprocess.run(
|
|
317
301
|
[
|
|
@@ -346,9 +330,7 @@ class GitIngester:
|
|
|
346
330
|
|
|
347
331
|
return history
|
|
348
332
|
|
|
349
|
-
def _get_file_blame(
|
|
350
|
-
self, repo_path: Path, file_path: Path
|
|
351
|
-
) -> Dict[int, Dict[str, Any]]:
|
|
333
|
+
def _get_file_blame(self, repo_path: Path, file_path: Path) -> Dict[int, Dict[str, Any]]:
|
|
352
334
|
"""Get blame information for a file."""
|
|
353
335
|
result = subprocess.run(
|
|
354
336
|
["git", "blame", "--line-porcelain", "--", str(file_path)],
|
|
@@ -448,9 +430,7 @@ Message: {commit.message}
|
|
|
448
430
|
text=True,
|
|
449
431
|
)
|
|
450
432
|
|
|
451
|
-
remote_url = (
|
|
452
|
-
remote_result.stdout.strip() if remote_result.returncode == 0 else None
|
|
453
|
-
)
|
|
433
|
+
remote_url = remote_result.stdout.strip() if remote_result.returncode == 0 else None
|
|
454
434
|
|
|
455
435
|
# Create repository summary document
|
|
456
436
|
repo_doc = f"""Repository: {repo_path.name}
|