hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.1__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 -1
- hanzo_mcp/cli.py +32 -0
- hanzo_mcp/dev_server.py +246 -0
- hanzo_mcp/prompts/__init__.py +1 -1
- hanzo_mcp/prompts/project_system.py +43 -7
- hanzo_mcp/server.py +5 -1
- hanzo_mcp/tools/__init__.py +168 -6
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent.py +401 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -4
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +9 -4
- hanzo_mcp/tools/common/batch_tool.py +3 -5
- hanzo_mcp/tools/common/config_tool.py +1 -1
- hanzo_mcp/tools/common/context.py +1 -1
- hanzo_mcp/tools/common/palette.py +344 -0
- hanzo_mcp/tools/common/palette_loader.py +108 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +260 -0
- hanzo_mcp/tools/config/__init__.py +10 -0
- hanzo_mcp/tools/config/config_tool.py +212 -0
- hanzo_mcp/tools/config/index_config.py +176 -0
- hanzo_mcp/tools/config/palette_tool.py +166 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +52 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/content_replace.py +3 -5
- hanzo_mcp/tools/filesystem/diff.py +193 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
- hanzo_mcp/tools/filesystem/edit.py +3 -5
- hanzo_mcp/tools/filesystem/find.py +443 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/filesystem/grep.py +2 -2
- hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
- hanzo_mcp/tools/filesystem/read.py +17 -5
- hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
- hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
- hanzo_mcp/tools/filesystem/tree.py +268 -0
- hanzo_mcp/tools/filesystem/unified_search.py +465 -443
- hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
- hanzo_mcp/tools/filesystem/watch.py +174 -0
- hanzo_mcp/tools/filesystem/write.py +3 -5
- hanzo_mcp/tools/jupyter/__init__.py +9 -12
- hanzo_mcp/tools/jupyter/base.py +1 -1
- hanzo_mcp/tools/jupyter/jupyter.py +326 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
- hanzo_mcp/tools/llm/__init__.py +31 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +15 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +21 -23
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +303 -0
- hanzo_mcp/tools/shell/bash_unified.py +134 -0
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/run_command.py +3 -4
- hanzo_mcp/tools/shell/run_command_windows.py +3 -4
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/shell/uvx_unified.py +101 -0
- hanzo_mcp/tools/todo/__init__.py +1 -1
- hanzo_mcp/tools/todo/base.py +1 -1
- hanzo_mcp/tools/todo/todo.py +265 -0
- hanzo_mcp/tools/todo/todo_read.py +3 -5
- hanzo_mcp/tools/todo/todo_write.py +3 -5
- hanzo_mcp/tools/vector/__init__.py +6 -1
- hanzo_mcp/tools/vector/git_ingester.py +3 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +98 -0
- hanzo_mcp/tools/vector/project_manager.py +27 -5
- hanzo_mcp/tools/vector/vector.py +311 -0
- hanzo_mcp/tools/vector/vector_index.py +1 -1
- hanzo_mcp/tools/vector/vector_search.py +12 -7
- hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
- hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
- hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
- hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.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 mcp.server.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 mcp.server.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
|