hanzo-mcp 0.5.1__py3-none-any.whl → 0.5.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (54) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/tools/__init__.py +135 -4
  3. hanzo_mcp/tools/common/base.py +7 -2
  4. hanzo_mcp/tools/common/stats.py +261 -0
  5. hanzo_mcp/tools/common/tool_disable.py +144 -0
  6. hanzo_mcp/tools/common/tool_enable.py +182 -0
  7. hanzo_mcp/tools/common/tool_list.py +263 -0
  8. hanzo_mcp/tools/database/__init__.py +71 -0
  9. hanzo_mcp/tools/database/database_manager.py +246 -0
  10. hanzo_mcp/tools/database/graph_add.py +257 -0
  11. hanzo_mcp/tools/database/graph_query.py +536 -0
  12. hanzo_mcp/tools/database/graph_remove.py +267 -0
  13. hanzo_mcp/tools/database/graph_search.py +348 -0
  14. hanzo_mcp/tools/database/graph_stats.py +345 -0
  15. hanzo_mcp/tools/database/sql_query.py +229 -0
  16. hanzo_mcp/tools/database/sql_search.py +296 -0
  17. hanzo_mcp/tools/database/sql_stats.py +254 -0
  18. hanzo_mcp/tools/editor/__init__.py +11 -0
  19. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  20. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  21. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  22. hanzo_mcp/tools/filesystem/__init__.py +15 -5
  23. hanzo_mcp/tools/filesystem/{unified_search.py → batch_search.py} +254 -131
  24. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  25. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  26. hanzo_mcp/tools/llm/__init__.py +27 -0
  27. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  28. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  29. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  30. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  31. hanzo_mcp/tools/mcp/__init__.py +11 -0
  32. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  33. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  34. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  35. hanzo_mcp/tools/shell/__init__.py +27 -7
  36. hanzo_mcp/tools/shell/logs.py +265 -0
  37. hanzo_mcp/tools/shell/npx.py +194 -0
  38. hanzo_mcp/tools/shell/npx_background.py +254 -0
  39. hanzo_mcp/tools/shell/pkill.py +262 -0
  40. hanzo_mcp/tools/shell/processes.py +279 -0
  41. hanzo_mcp/tools/shell/run_background.py +326 -0
  42. hanzo_mcp/tools/shell/uvx.py +187 -0
  43. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  44. hanzo_mcp/tools/vector/__init__.py +5 -0
  45. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  46. hanzo_mcp/tools/vector/index_tool.py +358 -0
  47. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  48. hanzo_mcp/tools/vector/vector_search.py +11 -6
  49. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +1 -1
  50. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/RECORD +54 -16
  51. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  52. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  53. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  54. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,187 @@
1
+ """Run Python packages with uvx."""
2
+
3
+ import subprocess
4
+ import shutil
5
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
+
7
+ from fastmcp import Context as MCPContext
8
+ from pydantic import Field
9
+
10
+ from hanzo_mcp.tools.common.base import BaseTool
11
+ from hanzo_mcp.tools.common.context import create_tool_context
12
+ from hanzo_mcp.tools.common.permissions import PermissionManager
13
+
14
+
15
+ Package = Annotated[
16
+ str,
17
+ Field(
18
+ description="Package name to run (e.g., 'ruff', 'black', 'pytest')",
19
+ min_length=1,
20
+ ),
21
+ ]
22
+
23
+ Args = Annotated[
24
+ Optional[str],
25
+ Field(
26
+ description="Arguments to pass to the package",
27
+ default=None,
28
+ ),
29
+ ]
30
+
31
+ PythonVersion = Annotated[
32
+ Optional[str],
33
+ Field(
34
+ description="Python version to use (e.g., '3.11', '3.12')",
35
+ default=None,
36
+ ),
37
+ ]
38
+
39
+ Timeout = Annotated[
40
+ int,
41
+ Field(
42
+ description="Timeout in seconds (default 120)",
43
+ default=120,
44
+ ),
45
+ ]
46
+
47
+
48
+ class UvxParams(TypedDict, total=False):
49
+ """Parameters for uvx tool."""
50
+
51
+ package: str
52
+ args: Optional[str]
53
+ python_version: Optional[str]
54
+ timeout: int
55
+
56
+
57
+ @final
58
+ class UvxTool(BaseTool):
59
+ """Tool for running Python packages with uvx."""
60
+
61
+ def __init__(self, permission_manager: PermissionManager):
62
+ """Initialize the uvx tool.
63
+
64
+ Args:
65
+ permission_manager: Permission manager for access control
66
+ """
67
+ self.permission_manager = permission_manager
68
+
69
+ @property
70
+ @override
71
+ def name(self) -> str:
72
+ """Get the tool name."""
73
+ return "uvx"
74
+
75
+ @property
76
+ @override
77
+ def description(self) -> str:
78
+ """Get the tool description."""
79
+ return """Run Python packages using uvx (Python package runner).
80
+
81
+ uvx allows running Python applications in isolated environments without
82
+ installing them globally. It automatically manages dependencies and Python versions.
83
+
84
+ Common packages:
85
+ - ruff: Fast Python linter and formatter
86
+ - black: Python code formatter
87
+ - pytest: Testing framework
88
+ - mypy: Static type checker
89
+ - pipx: Install Python apps
90
+ - httpie: HTTP client
91
+ - poetry: Dependency management
92
+
93
+ Examples:
94
+ - uvx --package ruff --args "check ."
95
+ - uvx --package black --args "--check src/"
96
+ - uvx --package pytest --args "-v tests/"
97
+ - uvx --package httpie --args "GET httpbin.org/get"
98
+ - uvx --package mypy --args "--strict src/"
99
+
100
+ For long-running servers, use uvx_background instead.
101
+ """
102
+
103
+ @override
104
+ async def call(
105
+ self,
106
+ ctx: MCPContext,
107
+ **params: Unpack[UvxParams],
108
+ ) -> str:
109
+ """Execute uvx command.
110
+
111
+ Args:
112
+ ctx: MCP context
113
+ **params: Tool parameters
114
+
115
+ Returns:
116
+ Command output
117
+ """
118
+ tool_ctx = create_tool_context(ctx)
119
+ await tool_ctx.set_tool_info(self.name)
120
+
121
+ # Extract parameters
122
+ package = params.get("package")
123
+ if not package:
124
+ return "Error: package is required"
125
+
126
+ args = params.get("args", "")
127
+ python_version = params.get("python_version")
128
+ timeout = params.get("timeout", 120)
129
+
130
+ # Check if uvx is available
131
+ if not shutil.which("uvx"):
132
+ return """Error: uvx is not installed. Install it with:
133
+ curl -LsSf https://astral.sh/uv/install.sh | sh
134
+
135
+ Or on macOS:
136
+ brew install uv"""
137
+
138
+ # Build command
139
+ cmd = ["uvx"]
140
+
141
+ if python_version:
142
+ cmd.extend(["--python", python_version])
143
+
144
+ cmd.append(package)
145
+
146
+ # Add package arguments
147
+ if args:
148
+ # Split args properly (basic parsing)
149
+ import shlex
150
+ cmd.extend(shlex.split(args))
151
+
152
+ await tool_ctx.info(f"Running: {' '.join(cmd)}")
153
+
154
+ try:
155
+ # Execute command
156
+ result = subprocess.run(
157
+ cmd,
158
+ capture_output=True,
159
+ text=True,
160
+ timeout=timeout,
161
+ check=True
162
+ )
163
+
164
+ output = []
165
+ if result.stdout:
166
+ output.append(result.stdout)
167
+ if result.stderr:
168
+ output.append(f"\nSTDERR:\n{result.stderr}")
169
+
170
+ return "\n".join(output) if output else "Command completed successfully with no output."
171
+
172
+ except subprocess.TimeoutExpired:
173
+ return f"Error: Command timed out after {timeout} seconds. Use uvx_background for long-running processes."
174
+ except subprocess.CalledProcessError as e:
175
+ error_msg = [f"Error: Command failed with exit code {e.returncode}"]
176
+ if e.stdout:
177
+ error_msg.append(f"\nSTDOUT:\n{e.stdout}")
178
+ if e.stderr:
179
+ error_msg.append(f"\nSTDERR:\n{e.stderr}")
180
+ return "\n".join(error_msg)
181
+ except Exception as e:
182
+ await tool_ctx.error(f"Unexpected error: {str(e)}")
183
+ return f"Error running uvx: {str(e)}"
184
+
185
+ def register(self, mcp_server) -> None:
186
+ """Register this tool with the MCP server."""
187
+ pass
@@ -0,0 +1,249 @@
1
+ """Run Python packages in background with uvx."""
2
+
3
+ import subprocess
4
+ import shutil
5
+ import uuid
6
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
7
+
8
+ from fastmcp import Context as MCPContext
9
+ from pydantic import Field
10
+
11
+ from hanzo_mcp.tools.common.base import BaseTool
12
+ from hanzo_mcp.tools.common.context import create_tool_context
13
+ from hanzo_mcp.tools.common.permissions import PermissionManager
14
+ from hanzo_mcp.tools.shell.run_background import BackgroundProcess, RunBackgroundTool
15
+
16
+
17
+ Package = Annotated[
18
+ str,
19
+ Field(
20
+ description="Package name to run (e.g., 'streamlit', 'jupyter-lab', 'mkdocs')",
21
+ min_length=1,
22
+ ),
23
+ ]
24
+
25
+ Args = Annotated[
26
+ Optional[str],
27
+ Field(
28
+ description="Arguments to pass to the package",
29
+ default=None,
30
+ ),
31
+ ]
32
+
33
+ Name = Annotated[
34
+ Optional[str],
35
+ Field(
36
+ description="Process name for identification",
37
+ default=None,
38
+ ),
39
+ ]
40
+
41
+ PythonVersion = Annotated[
42
+ Optional[str],
43
+ Field(
44
+ description="Python version to use (e.g., '3.11', '3.12')",
45
+ default=None,
46
+ ),
47
+ ]
48
+
49
+ LogOutput = Annotated[
50
+ bool,
51
+ Field(
52
+ description="Log output to file in ~/.hanzo/logs",
53
+ default=True,
54
+ ),
55
+ ]
56
+
57
+ WorkingDir = Annotated[
58
+ Optional[str],
59
+ Field(
60
+ description="Working directory for the process",
61
+ default=None,
62
+ ),
63
+ ]
64
+
65
+
66
+ class UvxBackgroundParams(TypedDict, total=False):
67
+ """Parameters for uvx background tool."""
68
+
69
+ package: str
70
+ args: Optional[str]
71
+ name: Optional[str]
72
+ python_version: Optional[str]
73
+ log_output: bool
74
+ working_dir: Optional[str]
75
+
76
+
77
+ @final
78
+ class UvxBackgroundTool(BaseTool):
79
+ """Tool for running Python packages in background with uvx."""
80
+
81
+ def __init__(self, permission_manager: PermissionManager):
82
+ """Initialize the uvx background tool.
83
+
84
+ Args:
85
+ permission_manager: Permission manager for access control
86
+ """
87
+ self.permission_manager = permission_manager
88
+
89
+ @property
90
+ @override
91
+ def name(self) -> str:
92
+ """Get the tool name."""
93
+ return "uvx_background"
94
+
95
+ @property
96
+ @override
97
+ def description(self) -> str:
98
+ """Get the tool description."""
99
+ return """Run Python packages in the background using uvx.
100
+
101
+ Perfect for running servers and long-running Python applications.
102
+ The process continues running even after the command returns.
103
+
104
+ Common server packages:
105
+ - streamlit: Data app framework
106
+ - jupyter-lab: Jupyter Lab server
107
+ - mkdocs: Documentation server
108
+ - fastapi: FastAPI with uvicorn
109
+ - flask: Flask development server
110
+ - gradio: ML model demos
111
+ - panel: Data app framework
112
+
113
+ Examples:
114
+ - uvx_background --package streamlit --args "run app.py --port 8501" --name streamlit-app
115
+ - uvx_background --package jupyter-lab --args "--port 8888" --name jupyter
116
+ - uvx_background --package mkdocs --args "serve --dev-addr 0.0.0.0:8000" --name docs
117
+ - uvx_background --package gradio --args "app.py" --name ml-demo
118
+
119
+ Use 'processes' to list running processes and 'pkill' to stop them.
120
+ """
121
+
122
+ @override
123
+ async def call(
124
+ self,
125
+ ctx: MCPContext,
126
+ **params: Unpack[UvxBackgroundParams],
127
+ ) -> str:
128
+ """Execute uvx command in background.
129
+
130
+ Args:
131
+ ctx: MCP context
132
+ **params: Tool parameters
133
+
134
+ Returns:
135
+ Process information
136
+ """
137
+ tool_ctx = create_tool_context(ctx)
138
+ await tool_ctx.set_tool_info(self.name)
139
+
140
+ # Extract parameters
141
+ package = params.get("package")
142
+ if not package:
143
+ return "Error: package is required"
144
+
145
+ args = params.get("args", "")
146
+ name = params.get("name", f"uvx-{package}")
147
+ python_version = params.get("python_version")
148
+ log_output = params.get("log_output", True)
149
+ working_dir = params.get("working_dir")
150
+
151
+ # Check if uvx is available
152
+ if not shutil.which("uvx"):
153
+ return """Error: uvx is not installed. Install it with:
154
+ curl -LsSf https://astral.sh/uv/install.sh | sh
155
+
156
+ Or on macOS:
157
+ brew install uv"""
158
+
159
+ # Build command
160
+ cmd = ["uvx"]
161
+
162
+ if python_version:
163
+ cmd.extend(["--python", python_version])
164
+
165
+ cmd.append(package)
166
+
167
+ # Add package arguments
168
+ if args:
169
+ # Split args properly (basic parsing)
170
+ import shlex
171
+ cmd.extend(shlex.split(args))
172
+
173
+ # Generate process ID
174
+ process_id = str(uuid.uuid4())[:8]
175
+
176
+ # Prepare log file if needed
177
+ log_file = None
178
+ if log_output:
179
+ from pathlib import Path
180
+ log_dir = Path.home() / ".hanzo" / "logs"
181
+ log_dir.mkdir(parents=True, exist_ok=True)
182
+ log_file = log_dir / f"{name}_{process_id}.log"
183
+
184
+ await tool_ctx.info(f"Starting background process: {' '.join(cmd)}")
185
+
186
+ try:
187
+ # Start process
188
+ if log_output and log_file:
189
+ with open(log_file, "w") as f:
190
+ process = subprocess.Popen(
191
+ cmd,
192
+ stdout=f,
193
+ stderr=subprocess.STDOUT,
194
+ cwd=working_dir,
195
+ start_new_session=True
196
+ )
197
+ else:
198
+ process = subprocess.Popen(
199
+ cmd,
200
+ stdout=subprocess.DEVNULL,
201
+ stderr=subprocess.DEVNULL,
202
+ cwd=working_dir,
203
+ start_new_session=True
204
+ )
205
+
206
+ # Create background process object
207
+ bg_process = BackgroundProcess(
208
+ process_id=process_id,
209
+ command=" ".join(cmd),
210
+ name=name,
211
+ process=process,
212
+ log_file=str(log_file) if log_file else None,
213
+ working_dir=working_dir
214
+ )
215
+
216
+ # Register with RunBackgroundTool
217
+ RunBackgroundTool._add_process(bg_process)
218
+
219
+ output = [
220
+ f"Started uvx background process:",
221
+ f" ID: {process_id}",
222
+ f" Name: {name}",
223
+ f" Package: {package}",
224
+ f" PID: {process.pid}",
225
+ f" Command: {' '.join(cmd)}",
226
+ ]
227
+
228
+ if working_dir:
229
+ output.append(f" Working Dir: {working_dir}")
230
+
231
+ if log_file:
232
+ output.append(f" Log: {log_file}")
233
+
234
+ output.extend([
235
+ "",
236
+ "Use 'processes' to list running processes.",
237
+ f"Use 'logs --process-id {process_id}' to view output.",
238
+ f"Use 'pkill --process-id {process_id}' to stop."
239
+ ])
240
+
241
+ return "\n".join(output)
242
+
243
+ except Exception as e:
244
+ await tool_ctx.error(f"Failed to start process: {str(e)}")
245
+ return f"Error starting uvx background process: {str(e)}"
246
+
247
+ def register(self, mcp_server) -> None:
248
+ """Register this tool with the MCP server."""
249
+ pass
@@ -17,6 +17,7 @@ try:
17
17
  from .project_manager import ProjectVectorManager
18
18
  from .vector_index import VectorIndexTool
19
19
  from .vector_search import VectorSearchTool
20
+ from .index_tool import IndexTool
20
21
 
21
22
  VECTOR_AVAILABLE = True
22
23
 
@@ -64,6 +65,9 @@ try:
64
65
  print(f"Detected {len(detected_projects)} projects with LLM.md files")
65
66
 
66
67
  # Register individual tools if enabled
68
+ if tool_enabled.get("index", True):
69
+ tools.append(IndexTool(permission_manager))
70
+
67
71
  if tool_enabled.get("vector_index", True):
68
72
  tools.append(VectorIndexTool(permission_manager, project_manager))
69
73
 
@@ -94,6 +98,7 @@ if VECTOR_AVAILABLE:
94
98
  __all__.extend([
95
99
  "InfinityVectorStore",
96
100
  "ProjectVectorManager",
101
+ "IndexTool",
97
102
  "VectorIndexTool",
98
103
  "VectorSearchTool",
99
104
  ])
@@ -89,9 +89,11 @@ class GitIngester:
89
89
  "repository": str(repo_path),
90
90
  "branch": branch,
91
91
  "commits_processed": 0,
92
+ "commits_indexed": 0,
92
93
  "files_indexed": 0,
93
94
  "symbols_extracted": 0,
94
95
  "diffs_indexed": 0,
96
+ "blame_entries": 0,
95
97
  "errors": []
96
98
  }
97
99
 
@@ -125,6 +127,7 @@ class GitIngester:
125
127
 
126
128
  for commit in commits:
127
129
  self._index_commit(commit, include_diffs=include_diffs)
130
+ results["commits_indexed"] = results.get("commits_indexed", 0) + 1
128
131
 
129
132
  if include_diffs:
130
133
  results["diffs_indexed"] += len(commit.files)