hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.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 +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +449 -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 +121 -33
- 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/config_tool.py +396 -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 +97 -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/unified_search.py +689 -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 +99 -0
- hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
- hanzo_mcp/tools/vector/git_ingester.py +482 -0
- hanzo_mcp/tools/vector/infinity_store.py +731 -0
- hanzo_mcp/tools/vector/mock_infinity.py +162 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +116 -0
- hanzo_mcp/tools/vector/vector_search.py +225 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
- hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.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/METADATA +0 -196
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
|
@@ -4,107 +4,147 @@ This package provides tools for interacting with the filesystem, including readi
|
|
|
4
4
|
and editing files, directory navigation, and content searching.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
12
12
|
from hanzo_mcp.tools.filesystem.content_replace import ContentReplaceTool
|
|
13
13
|
from hanzo_mcp.tools.filesystem.directory_tree import DirectoryTreeTool
|
|
14
|
-
from hanzo_mcp.tools.filesystem.
|
|
15
|
-
from hanzo_mcp.tools.filesystem.
|
|
16
|
-
from hanzo_mcp.tools.filesystem.
|
|
17
|
-
from hanzo_mcp.tools.filesystem.
|
|
18
|
-
from hanzo_mcp.tools.filesystem.
|
|
14
|
+
from hanzo_mcp.tools.filesystem.edit import Edit
|
|
15
|
+
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
16
|
+
from hanzo_mcp.tools.filesystem.grep_ast_tool import GrepAstTool
|
|
17
|
+
from hanzo_mcp.tools.filesystem.multi_edit import MultiEdit
|
|
18
|
+
from hanzo_mcp.tools.filesystem.read import ReadTool
|
|
19
|
+
from hanzo_mcp.tools.filesystem.write import Write
|
|
20
|
+
from hanzo_mcp.tools.filesystem.unified_search import UnifiedSearchTool
|
|
19
21
|
|
|
20
22
|
# Export all tool classes
|
|
21
23
|
__all__ = [
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
24
|
+
"ReadTool",
|
|
25
|
+
"Write",
|
|
26
|
+
"Edit",
|
|
27
|
+
"MultiEdit",
|
|
25
28
|
"DirectoryTreeTool",
|
|
26
|
-
"
|
|
27
|
-
"SearchContentTool",
|
|
29
|
+
"Grep",
|
|
28
30
|
"ContentReplaceTool",
|
|
31
|
+
"GrepAstTool",
|
|
32
|
+
"UnifiedSearchTool",
|
|
29
33
|
"get_filesystem_tools",
|
|
30
34
|
"register_filesystem_tools",
|
|
31
35
|
]
|
|
32
36
|
|
|
37
|
+
|
|
33
38
|
def get_read_only_filesystem_tools(
|
|
34
|
-
|
|
35
|
-
disable_search_tools: bool = False
|
|
39
|
+
permission_manager: PermissionManager,
|
|
36
40
|
) -> list[BaseTool]:
|
|
37
41
|
"""Create instances of read-only filesystem tools.
|
|
38
|
-
|
|
42
|
+
|
|
39
43
|
Args:
|
|
40
|
-
document_context: Document context for tracking file contents
|
|
41
44
|
permission_manager: Permission manager for access control
|
|
42
|
-
disable_search_tools: Whether to disable search tools (default: False)
|
|
43
45
|
|
|
44
46
|
Returns:
|
|
45
47
|
List of read-only filesystem tool instances
|
|
46
48
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
DirectoryTreeTool(
|
|
50
|
-
|
|
49
|
+
return [
|
|
50
|
+
ReadTool(permission_manager),
|
|
51
|
+
DirectoryTreeTool(permission_manager),
|
|
52
|
+
Grep(permission_manager),
|
|
53
|
+
GrepAstTool(permission_manager),
|
|
51
54
|
]
|
|
52
|
-
|
|
53
|
-
if not disable_search_tools:
|
|
54
|
-
tools.append(SearchContentTool(document_context, permission_manager))
|
|
55
|
-
|
|
56
|
-
return tools
|
|
57
55
|
|
|
58
56
|
|
|
59
|
-
def get_filesystem_tools(
|
|
60
|
-
document_context: DocumentContext, permission_manager: PermissionManager,
|
|
61
|
-
disable_search_tools: bool = False
|
|
62
|
-
) -> list[BaseTool]:
|
|
57
|
+
def get_filesystem_tools(permission_manager: PermissionManager) -> list[BaseTool]:
|
|
63
58
|
"""Create instances of all filesystem tools.
|
|
64
|
-
|
|
59
|
+
|
|
65
60
|
Args:
|
|
66
|
-
document_context: Document context for tracking file contents
|
|
67
61
|
permission_manager: Permission manager for access control
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
|
|
70
63
|
Returns:
|
|
71
64
|
List of filesystem tool instances
|
|
72
65
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
return [
|
|
67
|
+
ReadTool(permission_manager),
|
|
68
|
+
Write(permission_manager),
|
|
69
|
+
Edit(permission_manager),
|
|
70
|
+
MultiEdit(permission_manager),
|
|
71
|
+
DirectoryTreeTool(permission_manager),
|
|
72
|
+
Grep(permission_manager),
|
|
73
|
+
ContentReplaceTool(permission_manager),
|
|
74
|
+
GrepAstTool(permission_manager),
|
|
79
75
|
]
|
|
80
|
-
|
|
81
|
-
if not disable_search_tools:
|
|
82
|
-
tools.extend([
|
|
83
|
-
SearchContentTool(document_context, permission_manager),
|
|
84
|
-
ContentReplaceTool(document_context, permission_manager),
|
|
85
|
-
])
|
|
86
|
-
|
|
87
|
-
return tools
|
|
88
76
|
|
|
89
77
|
|
|
90
78
|
def register_filesystem_tools(
|
|
91
79
|
mcp_server: FastMCP,
|
|
92
|
-
document_context: DocumentContext,
|
|
93
80
|
permission_manager: PermissionManager,
|
|
94
81
|
disable_write_tools: bool = False,
|
|
95
82
|
disable_search_tools: bool = False,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
83
|
+
enabled_tools: dict[str, bool] | None = None,
|
|
84
|
+
project_manager=None,
|
|
85
|
+
) -> list[BaseTool]:
|
|
86
|
+
"""Register filesystem tools with the MCP server.
|
|
87
|
+
|
|
99
88
|
Args:
|
|
100
89
|
mcp_server: The FastMCP server instance
|
|
101
|
-
document_context: Document context for tracking file contents
|
|
102
90
|
permission_manager: Permission manager for access control
|
|
103
|
-
disable_write_tools: Whether to disable write
|
|
91
|
+
disable_write_tools: Whether to disable write tools (default: False)
|
|
104
92
|
disable_search_tools: Whether to disable search tools (default: False)
|
|
93
|
+
enabled_tools: Dictionary of individual tool enable states (default: None)
|
|
94
|
+
project_manager: Optional project manager for unified search (default: None)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of registered tools
|
|
105
98
|
"""
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
# Define tool mapping
|
|
100
|
+
tool_classes = {
|
|
101
|
+
"read": ReadTool,
|
|
102
|
+
"write": Write,
|
|
103
|
+
"edit": Edit,
|
|
104
|
+
"multi_edit": MultiEdit,
|
|
105
|
+
"directory_tree": DirectoryTreeTool,
|
|
106
|
+
"grep": Grep,
|
|
107
|
+
"grep_ast": GrepAstTool,
|
|
108
|
+
"content_replace": ContentReplaceTool,
|
|
109
|
+
"unified_search": UnifiedSearchTool,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
tools = []
|
|
113
|
+
|
|
114
|
+
if enabled_tools:
|
|
115
|
+
# Use individual tool configuration
|
|
116
|
+
for tool_name, enabled in enabled_tools.items():
|
|
117
|
+
if enabled and tool_name in tool_classes:
|
|
118
|
+
tool_class = tool_classes[tool_name]
|
|
119
|
+
if tool_name == "unified_search":
|
|
120
|
+
# Unified search requires project_manager
|
|
121
|
+
tools.append(tool_class(permission_manager, project_manager))
|
|
122
|
+
else:
|
|
123
|
+
tools.append(tool_class(permission_manager))
|
|
108
124
|
else:
|
|
109
|
-
|
|
125
|
+
# Use category-level configuration (backward compatibility)
|
|
126
|
+
if disable_write_tools and disable_search_tools:
|
|
127
|
+
# Only read and directory tools
|
|
128
|
+
tools = [
|
|
129
|
+
ReadTool(permission_manager),
|
|
130
|
+
DirectoryTreeTool(permission_manager),
|
|
131
|
+
]
|
|
132
|
+
elif disable_write_tools:
|
|
133
|
+
# Read-only tools including search
|
|
134
|
+
tools = get_read_only_filesystem_tools(permission_manager)
|
|
135
|
+
elif disable_search_tools:
|
|
136
|
+
# Write tools but no search
|
|
137
|
+
tools = [
|
|
138
|
+
ReadTool(permission_manager),
|
|
139
|
+
Write(permission_manager),
|
|
140
|
+
Edit(permission_manager),
|
|
141
|
+
MultiEdit(permission_manager),
|
|
142
|
+
DirectoryTreeTool(permission_manager),
|
|
143
|
+
ContentReplaceTool(permission_manager),
|
|
144
|
+
]
|
|
145
|
+
else:
|
|
146
|
+
# All tools
|
|
147
|
+
tools = get_filesystem_tools(permission_manager)
|
|
148
|
+
|
|
110
149
|
ToolRegistry.register_tools(mcp_server, tools)
|
|
150
|
+
return tools
|
|
@@ -8,27 +8,29 @@ from abc import ABC
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from fastmcp import Context as MCPContext
|
|
12
12
|
|
|
13
13
|
from hanzo_mcp.tools.common.base import FileSystemTool
|
|
14
14
|
from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class FilesystemBaseTool(FileSystemTool,ABC):
|
|
17
|
+
class FilesystemBaseTool(FileSystemTool, ABC):
|
|
18
18
|
"""Enhanced base class for all filesystem tools.
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
Provides additional utilities specific to filesystem operations beyond
|
|
21
21
|
the base functionality in FileSystemTool.
|
|
22
22
|
"""
|
|
23
|
-
|
|
24
|
-
async def check_path_allowed(
|
|
23
|
+
|
|
24
|
+
async def check_path_allowed(
|
|
25
|
+
self, path: str, tool_ctx: Any, error_prefix: str = "Error"
|
|
26
|
+
) -> tuple[bool, str]:
|
|
25
27
|
"""Check if a path is allowed and log an error if not.
|
|
26
|
-
|
|
28
|
+
|
|
27
29
|
Args:
|
|
28
30
|
path: Path to check
|
|
29
31
|
tool_ctx: Tool context for logging
|
|
30
32
|
error_prefix: Prefix for error messages
|
|
31
|
-
|
|
33
|
+
|
|
32
34
|
Returns:
|
|
33
35
|
tuple of (is_allowed, error_message)
|
|
34
36
|
"""
|
|
@@ -37,15 +39,17 @@ class FilesystemBaseTool(FileSystemTool,ABC):
|
|
|
37
39
|
await tool_ctx.error(message)
|
|
38
40
|
return False, f"{error_prefix}: {message}"
|
|
39
41
|
return True, ""
|
|
40
|
-
|
|
41
|
-
async def check_path_exists(
|
|
42
|
+
|
|
43
|
+
async def check_path_exists(
|
|
44
|
+
self, path: str, tool_ctx: Any, error_prefix: str = "Error"
|
|
45
|
+
) -> tuple[bool, str]:
|
|
42
46
|
"""Check if a path exists and log an error if not.
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
Args:
|
|
45
49
|
path: Path to check
|
|
46
50
|
tool_ctx: Tool context for logging
|
|
47
51
|
error_prefix: Prefix for error messages
|
|
48
|
-
|
|
52
|
+
|
|
49
53
|
Returns:
|
|
50
54
|
tuple of (exists, error_message)
|
|
51
55
|
"""
|
|
@@ -55,15 +59,17 @@ class FilesystemBaseTool(FileSystemTool,ABC):
|
|
|
55
59
|
await tool_ctx.error(message)
|
|
56
60
|
return False, f"{error_prefix}: {message}"
|
|
57
61
|
return True, ""
|
|
58
|
-
|
|
59
|
-
async def check_is_file(
|
|
62
|
+
|
|
63
|
+
async def check_is_file(
|
|
64
|
+
self, path: str, tool_ctx: Any, error_prefix: str = "Error"
|
|
65
|
+
) -> tuple[bool, str]:
|
|
60
66
|
"""Check if a path is a file and log an error if not.
|
|
61
|
-
|
|
67
|
+
|
|
62
68
|
Args:
|
|
63
69
|
path: Path to check
|
|
64
70
|
tool_ctx: Tool context for logging
|
|
65
71
|
error_prefix: Prefix for error messages
|
|
66
|
-
|
|
72
|
+
|
|
67
73
|
Returns:
|
|
68
74
|
tuple of (is_file, error_message)
|
|
69
75
|
"""
|
|
@@ -73,15 +79,17 @@ class FilesystemBaseTool(FileSystemTool,ABC):
|
|
|
73
79
|
await tool_ctx.error(message)
|
|
74
80
|
return False, f"{error_prefix}: {message}"
|
|
75
81
|
return True, ""
|
|
76
|
-
|
|
77
|
-
async def check_is_directory(
|
|
82
|
+
|
|
83
|
+
async def check_is_directory(
|
|
84
|
+
self, path: str, tool_ctx: Any, error_prefix: str = "Error"
|
|
85
|
+
) -> tuple[bool, str]:
|
|
78
86
|
"""Check if a path is a directory and log an error if not.
|
|
79
|
-
|
|
87
|
+
|
|
80
88
|
Args:
|
|
81
89
|
path: Path to check
|
|
82
90
|
tool_ctx: Tool context for logging
|
|
83
91
|
error_prefix: Prefix for error messages
|
|
84
|
-
|
|
92
|
+
|
|
85
93
|
Returns:
|
|
86
94
|
tuple of (is_directory, error_message)
|
|
87
95
|
"""
|
|
@@ -91,22 +99,22 @@ class FilesystemBaseTool(FileSystemTool,ABC):
|
|
|
91
99
|
await tool_ctx.error(message)
|
|
92
100
|
return False, f"{error_prefix}: {message}"
|
|
93
101
|
return True, ""
|
|
94
|
-
|
|
102
|
+
|
|
95
103
|
def create_tool_context(self, ctx: MCPContext) -> ToolContext:
|
|
96
104
|
"""Create a tool context with the tool name.
|
|
97
|
-
|
|
105
|
+
|
|
98
106
|
Args:
|
|
99
107
|
ctx: MCP context
|
|
100
|
-
|
|
108
|
+
|
|
101
109
|
Returns:
|
|
102
110
|
Tool context
|
|
103
111
|
"""
|
|
104
112
|
tool_ctx = create_tool_context(ctx)
|
|
105
113
|
return tool_ctx
|
|
106
|
-
|
|
114
|
+
|
|
107
115
|
def set_tool_context_info(self, tool_ctx: ToolContext) -> None:
|
|
108
116
|
"""Set the tool info on the context.
|
|
109
|
-
|
|
117
|
+
|
|
110
118
|
Args:
|
|
111
119
|
tool_ctx: Tool context
|
|
112
120
|
"""
|
|
@@ -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
|
+
)
|