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.

Files changed (93) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +118 -170
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +449 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +117 -99
  14. hanzo_mcp/tools/__init__.py +121 -33
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/config_tool.py +396 -0
  23. hanzo_mcp/tools/common/context.py +26 -292
  24. hanzo_mcp/tools/common/permissions.py +12 -12
  25. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  26. hanzo_mcp/tools/common/validation.py +1 -63
  27. hanzo_mcp/tools/filesystem/__init__.py +97 -57
  28. hanzo_mcp/tools/filesystem/base.py +32 -24
  29. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  30. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  31. hanzo_mcp/tools/filesystem/edit.py +279 -0
  32. hanzo_mcp/tools/filesystem/grep.py +458 -0
  33. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  34. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  35. hanzo_mcp/tools/filesystem/read.py +255 -0
  36. hanzo_mcp/tools/filesystem/unified_search.py +689 -0
  37. hanzo_mcp/tools/filesystem/write.py +156 -0
  38. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  39. hanzo_mcp/tools/jupyter/base.py +66 -57
  40. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  41. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  42. hanzo_mcp/tools/shell/__init__.py +29 -20
  43. hanzo_mcp/tools/shell/base.py +87 -45
  44. hanzo_mcp/tools/shell/bash_session.py +731 -0
  45. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  46. hanzo_mcp/tools/shell/command_executor.py +435 -384
  47. hanzo_mcp/tools/shell/run_command.py +284 -131
  48. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  49. hanzo_mcp/tools/shell/session_manager.py +196 -0
  50. hanzo_mcp/tools/shell/session_storage.py +325 -0
  51. hanzo_mcp/tools/todo/__init__.py +66 -0
  52. hanzo_mcp/tools/todo/base.py +319 -0
  53. hanzo_mcp/tools/todo/todo_read.py +148 -0
  54. hanzo_mcp/tools/todo/todo_write.py +378 -0
  55. hanzo_mcp/tools/vector/__init__.py +99 -0
  56. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  57. hanzo_mcp/tools/vector/git_ingester.py +482 -0
  58. hanzo_mcp/tools/vector/infinity_store.py +731 -0
  59. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  60. hanzo_mcp/tools/vector/project_manager.py +361 -0
  61. hanzo_mcp/tools/vector/vector_index.py +116 -0
  62. hanzo_mcp/tools/vector/vector_search.py +225 -0
  63. hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
  64. hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
  65. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
  66. hanzo_mcp/tools/agent/base_provider.py +0 -73
  67. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  68. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  69. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  70. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  71. hanzo_mcp/tools/common/error_handling.py +0 -86
  72. hanzo_mcp/tools/common/logging_config.py +0 -115
  73. hanzo_mcp/tools/common/session.py +0 -91
  74. hanzo_mcp/tools/common/think_tool.py +0 -123
  75. hanzo_mcp/tools/common/version_tool.py +0 -120
  76. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  77. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  78. hanzo_mcp/tools/filesystem/read_files.py +0 -199
  79. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  80. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  81. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  82. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  83. hanzo_mcp/tools/project/__init__.py +0 -64
  84. hanzo_mcp/tools/project/analysis.py +0 -886
  85. hanzo_mcp/tools/project/base.py +0 -66
  86. hanzo_mcp/tools/project/project_analyze.py +0 -173
  87. hanzo_mcp/tools/shell/run_script.py +0 -215
  88. hanzo_mcp/tools/shell/script_tool.py +0 -244
  89. hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
  90. hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
  91. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
  92. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
  93. {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 mcp.server.fastmcp import FastMCP
7
+ from fastmcp import FastMCP
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
- from hanzo_mcp.tools.common.context import DocumentContext
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.edit_file import EditFileTool
15
- from hanzo_mcp.tools.filesystem.get_file_info import GetFileInfoTool
16
- from hanzo_mcp.tools.filesystem.read_files import ReadFilesTool
17
- from hanzo_mcp.tools.filesystem.search_content import SearchContentTool
18
- from hanzo_mcp.tools.filesystem.write_file import WriteFileTool
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
- "ReadFilesTool",
23
- "WriteFileTool",
24
- "EditFileTool",
24
+ "ReadTool",
25
+ "Write",
26
+ "Edit",
27
+ "MultiEdit",
25
28
  "DirectoryTreeTool",
26
- "GetFileInfoTool",
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
- document_context: DocumentContext, permission_manager: PermissionManager,
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
- tools = [
48
- ReadFilesTool(document_context, permission_manager),
49
- DirectoryTreeTool(document_context, permission_manager),
50
- GetFileInfoTool(document_context, permission_manager),
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
- disable_search_tools: Whether to disable search tools (default: False)
69
-
62
+
70
63
  Returns:
71
64
  List of filesystem tool instances
72
65
  """
73
- tools = [
74
- ReadFilesTool(document_context, permission_manager),
75
- WriteFileTool(document_context, permission_manager),
76
- EditFileTool(document_context, permission_manager),
77
- DirectoryTreeTool(document_context, permission_manager),
78
- GetFileInfoTool(document_context, permission_manager),
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
- ) -> None:
97
- """Register all filesystem tools with the MCP server.
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/edit tools (default: False)
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
- if disable_write_tools:
107
- tools = get_read_only_filesystem_tools(document_context, permission_manager, disable_search_tools)
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
- tools = get_filesystem_tools(document_context, permission_manager, disable_search_tools)
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 mcp.server.fastmcp import Context as MCPContext
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(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
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(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
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(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
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(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
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 Any, final, override
8
+ from typing import Annotated, TypedDict, Unpack, final, override
9
9
 
10
- from mcp.server.fastmcp import Context as MCPContext
11
- from mcp.server.fastmcp import FastMCP
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(self, ctx: MCPContext, **params: Any) -> str:
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.get("pattern")
108
- replacement = params.get("replacement")
109
- path = params.get("path")
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(input_path.name, file_pattern):
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(f"File does not match pattern '{file_pattern}': {path}")
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(entry.name, file_pattern):
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.mcp_description)
286
- async def content_replace(ctx: MCPContext, pattern: str, replacement: str, path: str, file_pattern: str = "*", dry_run: bool = False) -> str:
287
- return await tool_self.call(ctx, pattern=pattern, replacement=replacement, path=path, file_pattern=file_pattern, dry_run=dry_run)
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
+ )