hanzo-mcp 0.5.0__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 (60) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/config/settings.py +61 -0
  3. hanzo_mcp/tools/__init__.py +158 -12
  4. hanzo_mcp/tools/common/base.py +7 -2
  5. hanzo_mcp/tools/common/config_tool.py +396 -0
  6. hanzo_mcp/tools/common/stats.py +261 -0
  7. hanzo_mcp/tools/common/tool_disable.py +144 -0
  8. hanzo_mcp/tools/common/tool_enable.py +182 -0
  9. hanzo_mcp/tools/common/tool_list.py +263 -0
  10. hanzo_mcp/tools/database/__init__.py +71 -0
  11. hanzo_mcp/tools/database/database_manager.py +246 -0
  12. hanzo_mcp/tools/database/graph_add.py +257 -0
  13. hanzo_mcp/tools/database/graph_query.py +536 -0
  14. hanzo_mcp/tools/database/graph_remove.py +267 -0
  15. hanzo_mcp/tools/database/graph_search.py +348 -0
  16. hanzo_mcp/tools/database/graph_stats.py +345 -0
  17. hanzo_mcp/tools/database/sql_query.py +229 -0
  18. hanzo_mcp/tools/database/sql_search.py +296 -0
  19. hanzo_mcp/tools/database/sql_stats.py +254 -0
  20. hanzo_mcp/tools/editor/__init__.py +11 -0
  21. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  22. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  23. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  24. hanzo_mcp/tools/filesystem/__init__.py +20 -1
  25. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  26. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  27. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  28. hanzo_mcp/tools/llm/__init__.py +27 -0
  29. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  30. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  31. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  32. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  33. hanzo_mcp/tools/mcp/__init__.py +11 -0
  34. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  35. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  36. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  37. hanzo_mcp/tools/shell/__init__.py +27 -7
  38. hanzo_mcp/tools/shell/logs.py +265 -0
  39. hanzo_mcp/tools/shell/npx.py +194 -0
  40. hanzo_mcp/tools/shell/npx_background.py +254 -0
  41. hanzo_mcp/tools/shell/pkill.py +262 -0
  42. hanzo_mcp/tools/shell/processes.py +279 -0
  43. hanzo_mcp/tools/shell/run_background.py +326 -0
  44. hanzo_mcp/tools/shell/uvx.py +187 -0
  45. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  46. hanzo_mcp/tools/vector/__init__.py +21 -12
  47. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  48. hanzo_mcp/tools/vector/git_ingester.py +485 -0
  49. hanzo_mcp/tools/vector/index_tool.py +358 -0
  50. hanzo_mcp/tools/vector/infinity_store.py +465 -1
  51. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  52. hanzo_mcp/tools/vector/vector_index.py +7 -6
  53. hanzo_mcp/tools/vector/vector_search.py +22 -7
  54. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
  55. hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
  56. hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
  57. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  58. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  59. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  60. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,272 @@
1
+ """Execute Neovim commands and macros."""
2
+
3
+ import os
4
+ import subprocess
5
+ import shutil
6
+ import tempfile
7
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override, List
8
+
9
+ from fastmcp import Context as MCPContext
10
+ from pydantic import Field
11
+
12
+ from hanzo_mcp.tools.common.base import BaseTool
13
+ from hanzo_mcp.tools.common.context import create_tool_context
14
+ from hanzo_mcp.tools.common.permissions import PermissionManager
15
+
16
+
17
+ Command = Annotated[
18
+ Optional[str],
19
+ Field(
20
+ description="Neovim command to execute (Ex commands like :w, :q, etc.)",
21
+ default=None,
22
+ ),
23
+ ]
24
+
25
+ Commands = Annotated[
26
+ Optional[List[str]],
27
+ Field(
28
+ description="List of Neovim commands to execute in sequence",
29
+ default=None,
30
+ ),
31
+ ]
32
+
33
+ Macro = Annotated[
34
+ Optional[str],
35
+ Field(
36
+ description="Vim macro to execute (e.g., 'dd' to delete line)",
37
+ default=None,
38
+ ),
39
+ ]
40
+
41
+ FilePath = Annotated[
42
+ Optional[str],
43
+ Field(
44
+ description="File to operate on (optional, uses current buffer if not specified)",
45
+ default=None,
46
+ ),
47
+ ]
48
+
49
+ SaveAfter = Annotated[
50
+ bool,
51
+ Field(
52
+ description="Save file after executing commands",
53
+ default=True,
54
+ ),
55
+ ]
56
+
57
+ ReturnOutput = Annotated[
58
+ bool,
59
+ Field(
60
+ description="Return output/messages from Neovim",
61
+ default=True,
62
+ ),
63
+ ]
64
+
65
+
66
+ class NeovimCommandParams(TypedDict, total=False):
67
+ """Parameters for Neovim command tool."""
68
+
69
+ command: Optional[str]
70
+ commands: Optional[List[str]]
71
+ macro: Optional[str]
72
+ file_path: Optional[str]
73
+ save_after: bool
74
+ return_output: bool
75
+
76
+
77
+ @final
78
+ class NeovimCommandTool(BaseTool):
79
+ """Tool for executing Neovim commands and macros."""
80
+
81
+ def __init__(self, permission_manager: PermissionManager):
82
+ """Initialize the Neovim command 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 "neovim_command"
94
+
95
+ @property
96
+ @override
97
+ def description(self) -> str:
98
+ """Get the tool description."""
99
+ return """Execute Neovim commands and macros programmatically.
100
+
101
+ Run Ex commands, normal mode commands, or complex macros in Neovim.
102
+ Can operate on files without opening the editor interface.
103
+
104
+ Examples:
105
+ - neovim_command --command ":set number" --file-path main.py
106
+ - neovim_command --command ":%s/old/new/g" --file-path config.json
107
+ - neovim_command --commands ":set expandtab" ":retab" --file-path script.sh
108
+ - neovim_command --macro "ggVG=" --file-path messy.py # Format entire file
109
+ - neovim_command --macro "dd10j" --file-path list.txt # Delete line and go down 10
110
+
111
+ Common commands:
112
+ - :w - Save file
113
+ - :q - Quit
114
+ - :%s/old/new/g - Replace all occurrences
115
+ - :set number - Show line numbers
116
+ - :set expandtab - Use spaces instead of tabs
117
+ - :retab - Convert tabs to spaces
118
+
119
+ Common macros:
120
+ - gg - Go to beginning of file
121
+ - G - Go to end of file
122
+ - dd - Delete line
123
+ - yy - Yank (copy) line
124
+ - p - Paste
125
+ - V - Visual line mode
126
+ - = - Format/indent
127
+
128
+ Note: Requires Neovim to be installed.
129
+ """
130
+
131
+ @override
132
+ async def call(
133
+ self,
134
+ ctx: MCPContext,
135
+ **params: Unpack[NeovimCommandParams],
136
+ ) -> str:
137
+ """Execute Neovim command.
138
+
139
+ Args:
140
+ ctx: MCP context
141
+ **params: Tool parameters
142
+
143
+ Returns:
144
+ Result of the command execution
145
+ """
146
+ tool_ctx = create_tool_context(ctx)
147
+ await tool_ctx.set_tool_info(self.name)
148
+
149
+ # Extract parameters
150
+ command = params.get("command")
151
+ commands = params.get("commands")
152
+ macro = params.get("macro")
153
+ file_path = params.get("file_path")
154
+ save_after = params.get("save_after", True)
155
+ return_output = params.get("return_output", True)
156
+
157
+ # Validate inputs
158
+ if not any([command, commands, macro]):
159
+ return "Error: Must provide either 'command', 'commands', or 'macro'"
160
+
161
+ if sum(bool(x) for x in [command, commands, macro]) > 1:
162
+ return "Error: Can only use one of 'command', 'commands', or 'macro' at a time"
163
+
164
+ # Check if Neovim is available
165
+ nvim_cmd = shutil.which("nvim")
166
+ if not nvim_cmd:
167
+ return "Error: Neovim (nvim) not found. Install it first."
168
+
169
+ # Prepare commands list
170
+ nvim_commands = []
171
+
172
+ if command:
173
+ nvim_commands.append(command)
174
+ elif commands:
175
+ nvim_commands.extend(commands)
176
+ elif macro:
177
+ # Convert macro to normal mode command
178
+ # Escape special characters
179
+ escaped_macro = macro.replace('"', '\\"')
180
+ nvim_commands.append(f':normal "{escaped_macro}"')
181
+
182
+ # Add save command if requested
183
+ if save_after:
184
+ nvim_commands.append(":w")
185
+
186
+ # Always quit at the end
187
+ nvim_commands.append(":q")
188
+
189
+ # Build Neovim command line
190
+ cmd = [nvim_cmd, "-n", "-i", "NONE"] # No swap file, no shada file
191
+
192
+ # Add commands
193
+ for vim_cmd in nvim_commands:
194
+ cmd.extend(["-c", vim_cmd])
195
+
196
+ # Add file if specified
197
+ if file_path:
198
+ file_path = os.path.abspath(file_path)
199
+
200
+ # Check permissions
201
+ if not self.permission_manager.has_permission(file_path):
202
+ return f"Error: No permission to access {file_path}"
203
+
204
+ if not os.path.exists(file_path):
205
+ return f"Error: File not found: {file_path}"
206
+
207
+ cmd.append(file_path)
208
+ else:
209
+ # Create empty buffer
210
+ cmd.append("-")
211
+
212
+ await tool_ctx.info(f"Executing Neovim commands: {nvim_commands}")
213
+
214
+ try:
215
+ # Execute Neovim
216
+ if return_output:
217
+ # Capture output by redirecting messages
218
+ output_file = tempfile.NamedTemporaryFile(mode='w+', delete=False)
219
+ output_file.close()
220
+
221
+ # Add command to redirect messages
222
+ cmd.insert(3, "-c")
223
+ cmd.insert(4, f":redir! > {output_file.name}")
224
+
225
+ # Execute
226
+ result = subprocess.run(
227
+ cmd,
228
+ capture_output=True,
229
+ text=True
230
+ )
231
+
232
+ # Read output
233
+ output_content = ""
234
+ try:
235
+ with open(output_file.name, 'r') as f:
236
+ output_content = f.read().strip()
237
+ finally:
238
+ os.unlink(output_file.name)
239
+
240
+ if result.returncode == 0:
241
+ response = "Commands executed successfully"
242
+ if file_path:
243
+ response += f" on {os.path.basename(file_path)}"
244
+ if output_content:
245
+ response += f"\n\nOutput:\n{output_content}"
246
+ return response
247
+ else:
248
+ error_msg = "Error executing Neovim commands"
249
+ if result.stderr:
250
+ error_msg += f"\n\nError:\n{result.stderr}"
251
+ if output_content:
252
+ error_msg += f"\n\nOutput:\n{output_content}"
253
+ return error_msg
254
+ else:
255
+ # Just execute without capturing output
256
+ result = subprocess.run(cmd)
257
+
258
+ if result.returncode == 0:
259
+ response = "Commands executed successfully"
260
+ if file_path:
261
+ response += f" on {os.path.basename(file_path)}"
262
+ return response
263
+ else:
264
+ return f"Neovim exited with code {result.returncode}"
265
+
266
+ except Exception as e:
267
+ await tool_ctx.error(f"Failed to execute Neovim commands: {str(e)}")
268
+ return f"Error executing Neovim commands: {str(e)}"
269
+
270
+ def register(self, mcp_server) -> None:
271
+ """Register this tool with the MCP server."""
272
+ pass
@@ -0,0 +1,290 @@
1
+ """Open files in Neovim editor."""
2
+
3
+ import os
4
+ import subprocess
5
+ import shutil
6
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
7
+ from pathlib import Path
8
+
9
+ from fastmcp import Context as MCPContext
10
+ from pydantic import Field
11
+
12
+ from hanzo_mcp.tools.common.base import BaseTool
13
+ from hanzo_mcp.tools.common.context import create_tool_context
14
+ from hanzo_mcp.tools.common.permissions import PermissionManager
15
+
16
+
17
+ FilePath = Annotated[
18
+ str,
19
+ Field(
20
+ description="Path to the file to open",
21
+ min_length=1,
22
+ ),
23
+ ]
24
+
25
+ LineNumber = Annotated[
26
+ Optional[int],
27
+ Field(
28
+ description="Line number to jump to",
29
+ default=None,
30
+ ),
31
+ ]
32
+
33
+ ColumnNumber = Annotated[
34
+ Optional[int],
35
+ Field(
36
+ description="Column number to jump to",
37
+ default=None,
38
+ ),
39
+ ]
40
+
41
+ ReadOnly = Annotated[
42
+ bool,
43
+ Field(
44
+ description="Open file in read-only mode",
45
+ default=False,
46
+ ),
47
+ ]
48
+
49
+ Split = Annotated[
50
+ Optional[str],
51
+ Field(
52
+ description="Split mode: vsplit, split, tab",
53
+ default=None,
54
+ ),
55
+ ]
56
+
57
+ Wait = Annotated[
58
+ bool,
59
+ Field(
60
+ description="Wait for Neovim to exit before returning",
61
+ default=True,
62
+ ),
63
+ ]
64
+
65
+ InTerminal = Annotated[
66
+ bool,
67
+ Field(
68
+ description="Open in terminal (requires terminal that supports it)",
69
+ default=True,
70
+ ),
71
+ ]
72
+
73
+
74
+ class NeovimEditParams(TypedDict, total=False):
75
+ """Parameters for Neovim edit tool."""
76
+
77
+ file_path: str
78
+ line_number: Optional[int]
79
+ column_number: Optional[int]
80
+ read_only: bool
81
+ split: Optional[str]
82
+ wait: bool
83
+ in_terminal: bool
84
+
85
+
86
+ @final
87
+ class NeovimEditTool(BaseTool):
88
+ """Tool for opening files in Neovim."""
89
+
90
+ def __init__(self, permission_manager: PermissionManager):
91
+ """Initialize the Neovim edit tool.
92
+
93
+ Args:
94
+ permission_manager: Permission manager for access control
95
+ """
96
+ self.permission_manager = permission_manager
97
+
98
+ @property
99
+ @override
100
+ def name(self) -> str:
101
+ """Get the tool name."""
102
+ return "neovim_edit"
103
+
104
+ @property
105
+ @override
106
+ def description(self) -> str:
107
+ """Get the tool description."""
108
+ return """Open files in Neovim editor with advanced options.
109
+
110
+ Open files at specific lines/columns, in different split modes, or read-only.
111
+ Integrates with your existing Neovim configuration.
112
+
113
+ Examples:
114
+ - neovim_edit --file-path main.py
115
+ - neovim_edit --file-path main.py --line-number 42
116
+ - neovim_edit --file-path main.py --line-number 42 --column-number 10
117
+ - neovim_edit --file-path config.json --read-only
118
+ - neovim_edit --file-path test.py --split vsplit
119
+ - neovim_edit --file-path README.md --split tab
120
+
121
+ Split modes:
122
+ - vsplit: Open in vertical split
123
+ - split: Open in horizontal split
124
+ - tab: Open in new tab
125
+
126
+ Note: Requires Neovim to be installed and available in PATH.
127
+ """
128
+
129
+ @override
130
+ async def call(
131
+ self,
132
+ ctx: MCPContext,
133
+ **params: Unpack[NeovimEditParams],
134
+ ) -> str:
135
+ """Open file in Neovim.
136
+
137
+ Args:
138
+ ctx: MCP context
139
+ **params: Tool parameters
140
+
141
+ Returns:
142
+ Result of the operation
143
+ """
144
+ tool_ctx = create_tool_context(ctx)
145
+ await tool_ctx.set_tool_info(self.name)
146
+
147
+ # Extract parameters
148
+ file_path = params.get("file_path")
149
+ if not file_path:
150
+ return "Error: file_path is required"
151
+
152
+ line_number = params.get("line_number")
153
+ column_number = params.get("column_number")
154
+ read_only = params.get("read_only", False)
155
+ split = params.get("split")
156
+ wait = params.get("wait", True)
157
+ in_terminal = params.get("in_terminal", True)
158
+
159
+ # Check if Neovim is available
160
+ nvim_cmd = shutil.which("nvim")
161
+ if not nvim_cmd:
162
+ # Try common locations
163
+ common_paths = [
164
+ "/usr/local/bin/nvim",
165
+ "/usr/bin/nvim",
166
+ "/opt/homebrew/bin/nvim",
167
+ os.path.expanduser("~/.local/bin/nvim"),
168
+ ]
169
+ for path in common_paths:
170
+ if os.path.exists(path):
171
+ nvim_cmd = path
172
+ break
173
+
174
+ if not nvim_cmd:
175
+ return """Error: Neovim (nvim) not found. Install it with:
176
+
177
+ On macOS:
178
+ brew install neovim
179
+
180
+ On Ubuntu/Debian:
181
+ sudo apt install neovim
182
+
183
+ On Arch:
184
+ sudo pacman -S neovim
185
+
186
+ Or visit: https://neovim.io/"""
187
+
188
+ # Convert to absolute path
189
+ file_path = os.path.abspath(file_path)
190
+
191
+ # Check permissions
192
+ if not self.permission_manager.has_permission(file_path):
193
+ return f"Error: No permission to access {file_path}"
194
+
195
+ # Build Neovim command
196
+ cmd = [nvim_cmd]
197
+
198
+ # Add read-only flag
199
+ if read_only:
200
+ cmd.append("-R")
201
+
202
+ # Add split mode
203
+ if split:
204
+ if split == "vsplit":
205
+ cmd.extend(["-c", "vsplit"])
206
+ elif split == "split":
207
+ cmd.extend(["-c", "split"])
208
+ elif split == "tab":
209
+ cmd.extend(["-c", "tabnew"])
210
+ else:
211
+ return f"Error: Invalid split mode '{split}'. Use 'vsplit', 'split', or 'tab'"
212
+
213
+ # Add file path
214
+ cmd.append(file_path)
215
+
216
+ # Add line/column positioning
217
+ if line_number:
218
+ if column_number:
219
+ # Go to specific line and column
220
+ cmd.extend(["+call cursor({}, {})".format(line_number, column_number)])
221
+ else:
222
+ # Go to specific line
223
+ cmd.append(f"+{line_number}")
224
+
225
+ await tool_ctx.info(f"Opening {file_path} in Neovim")
226
+
227
+ try:
228
+ # Determine how to run Neovim
229
+ if in_terminal and not wait:
230
+ # Open in a new terminal window (platform-specific)
231
+ if os.uname().sysname == "Darwin": # macOS
232
+ # Try to use iTerm2 if available, otherwise Terminal
233
+ if shutil.which("osascript"):
234
+ # Build AppleScript to open in iTerm2 or Terminal
235
+ nvim_cmd_str = " ".join(f'"{arg}"' for arg in cmd)
236
+
237
+ # Try iTerm2 first
238
+ applescript = f'''tell application "System Events"
239
+ if exists application process "iTerm2" then
240
+ tell application "iTerm"
241
+ activate
242
+ tell current window
243
+ create tab with default profile
244
+ tell current session
245
+ write text "{nvim_cmd_str}"
246
+ end tell
247
+ end tell
248
+ end tell
249
+ else
250
+ tell application "Terminal"
251
+ activate
252
+ do script "{nvim_cmd_str}"
253
+ end tell
254
+ end if
255
+ end tell'''
256
+
257
+ subprocess.run(["osascript", "-e", applescript])
258
+ return f"Opened {file_path} in Neovim (new terminal window)"
259
+
260
+ elif shutil.which("gnome-terminal"):
261
+ # Linux with GNOME
262
+ subprocess.Popen(["gnome-terminal", "--"] + cmd)
263
+ return f"Opened {file_path} in Neovim (new terminal window)"
264
+
265
+ elif shutil.which("xterm"):
266
+ # Fallback to xterm
267
+ subprocess.Popen(["xterm", "-e"] + cmd)
268
+ return f"Opened {file_path} in Neovim (new terminal window)"
269
+
270
+ else:
271
+ # Can't open in terminal, fall back to subprocess
272
+ subprocess.Popen(cmd)
273
+ return f"Opened {file_path} in Neovim (background process)"
274
+
275
+ else:
276
+ # Run and wait for completion
277
+ result = subprocess.run(cmd)
278
+
279
+ if result.returncode == 0:
280
+ return f"Successfully edited {file_path} in Neovim"
281
+ else:
282
+ return f"Neovim exited with code {result.returncode}"
283
+
284
+ except Exception as e:
285
+ await tool_ctx.error(f"Failed to open Neovim: {str(e)}")
286
+ return f"Error opening Neovim: {str(e)}"
287
+
288
+ def register(self, mcp_server) -> None:
289
+ """Register this tool with the MCP server."""
290
+ pass