hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.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 -1
- hanzo_mcp/cli.py +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +388 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +117 -99
- hanzo_mcp/tools/__init__.py +105 -32
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +88 -57
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +95 -0
- hanzo_mcp/tools/vector/infinity_store.py +365 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +115 -0
- hanzo_mcp/tools/vector/vector_search.py +215 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +33 -1
- hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -199
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -886
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -5,33 +5,92 @@ This module provides the ContentReplaceTool for replacing text patterns in files
|
|
|
5
5
|
|
|
6
6
|
import fnmatch
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Annotated, TypedDict, Unpack, final, override
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from fastmcp import Context as MCPContext
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
from fastmcp.server.dependencies import get_context
|
|
13
|
+
from pydantic import Field
|
|
12
14
|
|
|
13
15
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
14
16
|
|
|
17
|
+
Pattern = Annotated[
|
|
18
|
+
str,
|
|
19
|
+
Field(
|
|
20
|
+
description="Text pattern to search for in files",
|
|
21
|
+
min_length=1,
|
|
22
|
+
),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
Replacement = Annotated[
|
|
26
|
+
str,
|
|
27
|
+
Field(
|
|
28
|
+
description="Text to replace the pattern with (can be empty string)",
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
SearchPath = Annotated[
|
|
33
|
+
str,
|
|
34
|
+
Field(
|
|
35
|
+
description="Path to file or directory to search in",
|
|
36
|
+
min_length=1,
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
FilePattern = Annotated[
|
|
41
|
+
str,
|
|
42
|
+
Field(
|
|
43
|
+
description="File name pattern to match (default: all files)",
|
|
44
|
+
default="*",
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
DryRun = Annotated[
|
|
49
|
+
bool,
|
|
50
|
+
Field(
|
|
51
|
+
description="If True, only preview changes without modifying files",
|
|
52
|
+
default=False,
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ContentReplaceToolParams(TypedDict):
|
|
58
|
+
"""Parameters for the ContentReplaceTool.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
pattern: Text pattern to search for in files
|
|
62
|
+
replacement: Text to replace the pattern with (can be empty string)
|
|
63
|
+
path: Path to file or directory to search in
|
|
64
|
+
file_pattern: File name pattern to match (default: all files)
|
|
65
|
+
dry_run: If True, only preview changes without modifying files
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
pattern: Pattern
|
|
69
|
+
replacement: Replacement
|
|
70
|
+
path: SearchPath
|
|
71
|
+
file_pattern: FilePattern
|
|
72
|
+
dry_run: DryRun
|
|
73
|
+
|
|
15
74
|
|
|
16
75
|
@final
|
|
17
76
|
class ContentReplaceTool(FilesystemBaseTool):
|
|
18
77
|
"""Tool for replacing text patterns in files."""
|
|
19
|
-
|
|
78
|
+
|
|
20
79
|
@property
|
|
21
80
|
@override
|
|
22
81
|
def name(self) -> str:
|
|
23
82
|
"""Get the tool name.
|
|
24
|
-
|
|
83
|
+
|
|
25
84
|
Returns:
|
|
26
85
|
Tool name
|
|
27
86
|
"""
|
|
28
87
|
return "content_replace"
|
|
29
|
-
|
|
88
|
+
|
|
30
89
|
@property
|
|
31
90
|
@override
|
|
32
91
|
def description(self) -> str:
|
|
33
92
|
"""Get the tool description.
|
|
34
|
-
|
|
93
|
+
|
|
35
94
|
Returns:
|
|
36
95
|
Tool description
|
|
37
96
|
"""
|
|
@@ -41,97 +100,30 @@ Searches for text patterns across all files in the specified directory
|
|
|
41
100
|
that match the file pattern and replaces them with the specified text.
|
|
42
101
|
Can be run in dry-run mode to preview changes without applying them.
|
|
43
102
|
Only works within allowed directories."""
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
@override
|
|
47
|
-
def parameters(self) -> dict[str, Any]:
|
|
48
|
-
"""Get the parameter specifications for the tool.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Parameter specifications
|
|
52
|
-
"""
|
|
53
|
-
return {
|
|
54
|
-
"properties": {
|
|
55
|
-
"pattern": {
|
|
56
|
-
"title": "Pattern",
|
|
57
|
-
"type": "string"
|
|
58
|
-
},
|
|
59
|
-
"replacement": {
|
|
60
|
-
"title": "Replacement",
|
|
61
|
-
"type": "string"
|
|
62
|
-
},
|
|
63
|
-
"path": {
|
|
64
|
-
"title": "Path",
|
|
65
|
-
"type": "string"
|
|
66
|
-
},
|
|
67
|
-
"file_pattern": {
|
|
68
|
-
"default": "*",
|
|
69
|
-
"title": "File Pattern",
|
|
70
|
-
"type": "string"
|
|
71
|
-
},
|
|
72
|
-
"dry_run": {
|
|
73
|
-
"default": False,
|
|
74
|
-
"title": "Dry Run",
|
|
75
|
-
"type": "boolean"
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
"required": ["pattern", "replacement", "path"],
|
|
79
|
-
"title": "content_replaceArguments",
|
|
80
|
-
"type": "object"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
@override
|
|
85
|
-
def required(self) -> list[str]:
|
|
86
|
-
"""Get the list of required parameter names.
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
List of required parameter names
|
|
90
|
-
"""
|
|
91
|
-
return ["pattern", "replacement", "path"]
|
|
92
|
-
|
|
103
|
+
|
|
93
104
|
@override
|
|
94
|
-
async def call(
|
|
105
|
+
async def call(
|
|
106
|
+
self,
|
|
107
|
+
ctx: MCPContext,
|
|
108
|
+
**params: Unpack[ContentReplaceToolParams],
|
|
109
|
+
) -> str:
|
|
95
110
|
"""Execute the tool with the given parameters.
|
|
96
|
-
|
|
111
|
+
|
|
97
112
|
Args:
|
|
98
113
|
ctx: MCP context
|
|
99
114
|
**params: Tool parameters
|
|
100
|
-
|
|
115
|
+
|
|
101
116
|
Returns:
|
|
102
117
|
Tool result
|
|
103
118
|
"""
|
|
104
119
|
tool_ctx = self.create_tool_context(ctx)
|
|
105
|
-
|
|
120
|
+
|
|
106
121
|
# Extract parameters
|
|
107
|
-
pattern = params
|
|
108
|
-
replacement = params
|
|
109
|
-
path = params
|
|
122
|
+
pattern: Pattern = params["pattern"]
|
|
123
|
+
replacement: Replacement = params["replacement"]
|
|
124
|
+
path: SearchPath = params["path"]
|
|
110
125
|
file_pattern = params.get("file_pattern", "*") # Default to all files
|
|
111
126
|
dry_run = params.get("dry_run", False) # Default to False
|
|
112
|
-
|
|
113
|
-
# Validate required parameters
|
|
114
|
-
if not pattern:
|
|
115
|
-
await tool_ctx.error("Parameter 'pattern' is required but was None")
|
|
116
|
-
return "Error: Parameter 'pattern' is required but was None"
|
|
117
|
-
|
|
118
|
-
if pattern.strip() == "":
|
|
119
|
-
await tool_ctx.error("Parameter 'pattern' cannot be empty")
|
|
120
|
-
return "Error: Parameter 'pattern' cannot be empty"
|
|
121
|
-
|
|
122
|
-
if replacement is None:
|
|
123
|
-
await tool_ctx.error("Parameter 'replacement' is required but was None")
|
|
124
|
-
return "Error: Parameter 'replacement' is required but was None"
|
|
125
|
-
|
|
126
|
-
if not path:
|
|
127
|
-
await tool_ctx.error("Parameter 'path' is required but was None")
|
|
128
|
-
return "Error: Parameter 'path' is required but was None"
|
|
129
|
-
|
|
130
|
-
if path.strip() == "":
|
|
131
|
-
await tool_ctx.error("Parameter 'path' cannot be empty")
|
|
132
|
-
return "Error: Parameter 'path' cannot be empty"
|
|
133
|
-
|
|
134
|
-
# Note: replacement can be an empty string as sometimes you want to delete the pattern
|
|
135
127
|
|
|
136
128
|
path_validation = self.validate_path(path)
|
|
137
129
|
if path_validation.is_error:
|
|
@@ -168,32 +160,38 @@ Only works within allowed directories."""
|
|
|
168
160
|
# Process based on whether path is a file or directory
|
|
169
161
|
if input_path.is_file():
|
|
170
162
|
# Single file search
|
|
171
|
-
if file_pattern == "*" or fnmatch.fnmatch(
|
|
163
|
+
if file_pattern == "*" or fnmatch.fnmatch(
|
|
164
|
+
input_path.name, file_pattern
|
|
165
|
+
):
|
|
172
166
|
matching_files.append(input_path)
|
|
173
167
|
await tool_ctx.info(f"Searching single file: {path}")
|
|
174
168
|
else:
|
|
175
|
-
await tool_ctx.info(
|
|
169
|
+
await tool_ctx.info(
|
|
170
|
+
f"File does not match pattern '{file_pattern}': {path}"
|
|
171
|
+
)
|
|
176
172
|
return f"File does not match pattern '{file_pattern}': {path}"
|
|
177
173
|
elif input_path.is_dir():
|
|
178
174
|
# Directory search - optimized file finding
|
|
179
175
|
await tool_ctx.info(f"Finding files in directory: {path}")
|
|
180
|
-
|
|
176
|
+
|
|
181
177
|
# Keep track of allowed paths for filtering
|
|
182
178
|
allowed_paths: set[str] = set()
|
|
183
|
-
|
|
179
|
+
|
|
184
180
|
# Collect all allowed paths first for faster filtering
|
|
185
181
|
for entry in input_path.rglob("*"):
|
|
186
182
|
entry_path = str(entry)
|
|
187
183
|
if self.is_path_allowed(entry_path):
|
|
188
184
|
allowed_paths.add(entry_path)
|
|
189
|
-
|
|
185
|
+
|
|
190
186
|
# Find matching files efficiently
|
|
191
187
|
for entry in input_path.rglob("*"):
|
|
192
188
|
entry_path = str(entry)
|
|
193
189
|
if entry_path in allowed_paths and entry.is_file():
|
|
194
|
-
if file_pattern == "*" or fnmatch.fnmatch(
|
|
190
|
+
if file_pattern == "*" or fnmatch.fnmatch(
|
|
191
|
+
entry.name, file_pattern
|
|
192
|
+
):
|
|
195
193
|
matching_files.append(entry)
|
|
196
|
-
|
|
194
|
+
|
|
197
195
|
await tool_ctx.info(f"Found {len(matching_files)} matching files")
|
|
198
196
|
else:
|
|
199
197
|
# This shouldn't happen since we already checked for existence
|
|
@@ -236,17 +234,11 @@ Only works within allowed directories."""
|
|
|
236
234
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
237
235
|
f.write(new_content)
|
|
238
236
|
|
|
239
|
-
# Update document context
|
|
240
|
-
self.document_context.update_document(
|
|
241
|
-
str(file_path), new_content
|
|
242
|
-
)
|
|
243
237
|
except UnicodeDecodeError:
|
|
244
238
|
# Skip binary files
|
|
245
239
|
continue
|
|
246
240
|
except Exception as e:
|
|
247
|
-
await tool_ctx.warning(
|
|
248
|
-
f"Error processing {file_path}: {str(e)}"
|
|
249
|
-
)
|
|
241
|
+
await tool_ctx.warning(f"Error processing {file_path}: {str(e)}")
|
|
250
242
|
|
|
251
243
|
# Final progress report
|
|
252
244
|
await tool_ctx.report_progress(total_files, total_files)
|
|
@@ -269,19 +261,34 @@ Only works within allowed directories."""
|
|
|
269
261
|
except Exception as e:
|
|
270
262
|
await tool_ctx.error(f"Error replacing content: {str(e)}")
|
|
271
263
|
return f"Error replacing content: {str(e)}"
|
|
272
|
-
|
|
264
|
+
|
|
273
265
|
@override
|
|
274
266
|
def register(self, mcp_server: FastMCP) -> None:
|
|
275
267
|
"""Register this content replace tool with the MCP server.
|
|
276
|
-
|
|
268
|
+
|
|
277
269
|
Creates a wrapper function with explicitly defined parameters that match
|
|
278
270
|
the tool's parameter schema and registers it with the MCP server.
|
|
279
|
-
|
|
271
|
+
|
|
280
272
|
Args:
|
|
281
273
|
mcp_server: The FastMCP server instance
|
|
282
274
|
"""
|
|
283
275
|
tool_self = self # Create a reference to self for use in the closure
|
|
284
|
-
|
|
285
|
-
@mcp_server.tool(name=self.name, description=self.
|
|
286
|
-
async def content_replace(
|
|
287
|
-
|
|
276
|
+
|
|
277
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
278
|
+
async def content_replace(
|
|
279
|
+
ctx: MCPContext,
|
|
280
|
+
pattern: Pattern,
|
|
281
|
+
replacement: Replacement,
|
|
282
|
+
path: SearchPath,
|
|
283
|
+
file_pattern: FilePattern,
|
|
284
|
+
dry_run: DryRun,
|
|
285
|
+
) -> str:
|
|
286
|
+
ctx = get_context()
|
|
287
|
+
return await tool_self.call(
|
|
288
|
+
ctx,
|
|
289
|
+
pattern=pattern,
|
|
290
|
+
replacement=replacement,
|
|
291
|
+
path=path,
|
|
292
|
+
file_pattern=file_pattern,
|
|
293
|
+
dry_run=dry_run,
|
|
294
|
+
)
|