hanzo-mcp 0.1.20__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 (44) hide show
  1. hanzo_mcp/__init__.py +3 -0
  2. hanzo_mcp/cli.py +213 -0
  3. hanzo_mcp/server.py +149 -0
  4. hanzo_mcp/tools/__init__.py +81 -0
  5. hanzo_mcp/tools/agent/__init__.py +59 -0
  6. hanzo_mcp/tools/agent/agent_tool.py +474 -0
  7. hanzo_mcp/tools/agent/prompt.py +137 -0
  8. hanzo_mcp/tools/agent/tool_adapter.py +75 -0
  9. hanzo_mcp/tools/common/__init__.py +18 -0
  10. hanzo_mcp/tools/common/base.py +216 -0
  11. hanzo_mcp/tools/common/context.py +444 -0
  12. hanzo_mcp/tools/common/permissions.py +253 -0
  13. hanzo_mcp/tools/common/thinking_tool.py +123 -0
  14. hanzo_mcp/tools/common/validation.py +124 -0
  15. hanzo_mcp/tools/filesystem/__init__.py +89 -0
  16. hanzo_mcp/tools/filesystem/base.py +113 -0
  17. hanzo_mcp/tools/filesystem/content_replace.py +287 -0
  18. hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
  19. hanzo_mcp/tools/filesystem/edit_file.py +287 -0
  20. hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
  21. hanzo_mcp/tools/filesystem/read_files.py +198 -0
  22. hanzo_mcp/tools/filesystem/search_content.py +275 -0
  23. hanzo_mcp/tools/filesystem/write_file.py +162 -0
  24. hanzo_mcp/tools/jupyter/__init__.py +71 -0
  25. hanzo_mcp/tools/jupyter/base.py +284 -0
  26. hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
  27. hanzo_mcp/tools/jupyter/notebook_operations.py +514 -0
  28. hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
  29. hanzo_mcp/tools/project/__init__.py +64 -0
  30. hanzo_mcp/tools/project/analysis.py +882 -0
  31. hanzo_mcp/tools/project/base.py +66 -0
  32. hanzo_mcp/tools/project/project_analyze.py +173 -0
  33. hanzo_mcp/tools/shell/__init__.py +58 -0
  34. hanzo_mcp/tools/shell/base.py +148 -0
  35. hanzo_mcp/tools/shell/command_executor.py +740 -0
  36. hanzo_mcp/tools/shell/run_command.py +204 -0
  37. hanzo_mcp/tools/shell/run_script.py +215 -0
  38. hanzo_mcp/tools/shell/script_tool.py +244 -0
  39. hanzo_mcp-0.1.20.dist-info/METADATA +111 -0
  40. hanzo_mcp-0.1.20.dist-info/RECORD +44 -0
  41. hanzo_mcp-0.1.20.dist-info/WHEEL +5 -0
  42. hanzo_mcp-0.1.20.dist-info/entry_points.txt +2 -0
  43. hanzo_mcp-0.1.20.dist-info/licenses/LICENSE +21 -0
  44. hanzo_mcp-0.1.20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,287 @@
1
+ """Edit file tool implementation.
2
+
3
+ This module provides the EditFileTool for making line-based edits to text files.
4
+ """
5
+
6
+ from difflib import unified_diff
7
+ from pathlib import Path
8
+ from typing import Any, final, override
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
14
+
15
+
16
+ @final
17
+ class EditFileTool(FilesystemBaseTool):
18
+ """Tool for making line-based edits to files."""
19
+
20
+ @property
21
+ @override
22
+ def name(self) -> str:
23
+ """Get the tool name.
24
+
25
+ Returns:
26
+ Tool name
27
+ """
28
+ return "edit_file"
29
+
30
+ @property
31
+ @override
32
+ def description(self) -> str:
33
+ """Get the tool description.
34
+
35
+ Returns:
36
+ Tool description
37
+ """
38
+ return """Make line-based edits to a text file.
39
+
40
+ Each edit replaces exact line sequences with new content.
41
+ Returns a git-style diff showing the changes made.
42
+ Only works within allowed directories."""
43
+
44
+ @property
45
+ @override
46
+ def parameters(self) -> dict[str, Any]:
47
+ """Get the parameter specifications for the tool.
48
+
49
+ Returns:
50
+ Parameter specifications
51
+ """
52
+ return {
53
+ "properties": {
54
+ "path": {
55
+ "type": "string",
56
+ "description": "The absolute path to the file to modify (must be absolute, not relative)",
57
+ },
58
+ "edits": {
59
+ "items": {
60
+ "properties": {
61
+ "oldText": {
62
+ "type": "string",
63
+ "description": "The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)",
64
+ },
65
+ "newText": {
66
+ "type": "string",
67
+ "description": "The edited text to replace the old_string",
68
+ },
69
+ },
70
+ "additionalProperties": {
71
+ "type": "string"
72
+ },
73
+ "type": "object"
74
+ },
75
+ "description":"List of edit operations [{\"oldText\": \"...\", \"newText\": \"...\"}]",
76
+ "type": "array"
77
+ },
78
+ "dry_run": {
79
+ "default": False,
80
+ "type": "boolean",
81
+ "description": "If true, do not write changes to the file"
82
+ }
83
+ },
84
+ "required": ["path", "edits"],
85
+ "title": "edit_fileArguments",
86
+ "type": "object"
87
+ }
88
+
89
+ @property
90
+ @override
91
+ def required(self) -> list[str]:
92
+ """Get the list of required parameter names.
93
+
94
+ Returns:
95
+ List of required parameter names
96
+ """
97
+ return ["path", "edits"]
98
+
99
+ @override
100
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
101
+ """Execute the tool with the given parameters.
102
+
103
+ Args:
104
+ ctx: MCP context
105
+ **params: Tool parameters
106
+
107
+ Returns:
108
+ Tool result
109
+ """
110
+ tool_ctx = self.create_tool_context(ctx)
111
+ self.set_tool_context_info(tool_ctx)
112
+
113
+ # Extract parameters
114
+ path = params.get("path")
115
+ edits = params.get("edits")
116
+ dry_run = params.get("dry_run", False) # Default to False if not provided
117
+
118
+ if not path:
119
+ await tool_ctx.error("Parameter 'path' is required but was None")
120
+ return "Error: Parameter 'path' is required but was None"
121
+
122
+ if path.strip() == "":
123
+ await tool_ctx.error("Parameter 'path' cannot be empty")
124
+ return "Error: Parameter 'path' cannot be empty"
125
+
126
+ # Validate parameters
127
+ path_validation = self.validate_path(path)
128
+ if path_validation.is_error:
129
+ await tool_ctx.error(path_validation.error_message)
130
+ return f"Error: {path_validation.error_message}"
131
+
132
+ if not edits:
133
+ await tool_ctx.error("Parameter 'edits' is required but was None")
134
+ return "Error: Parameter 'edits' is required but was None"
135
+
136
+ if not edits: # Check for empty list
137
+ await tool_ctx.warning("No edits specified")
138
+ return "Error: No edits specified"
139
+
140
+ # Validate each edit to ensure oldText is not empty
141
+ for i, edit in enumerate(edits):
142
+ old_text = edit.get("oldText", "")
143
+ if not old_text or old_text.strip() == "":
144
+ await tool_ctx.error(
145
+ f"Parameter 'oldText' in edit at index {i} is empty"
146
+ )
147
+ return f"Error: Parameter 'oldText' in edit at index {i} cannot be empty - must provide text to match"
148
+
149
+ # dry_run parameter can be None safely as it has a default value in the function signature
150
+
151
+ await tool_ctx.info(f"Editing file: {path}")
152
+
153
+ # Check if file is allowed to be edited
154
+ allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
155
+ if not allowed:
156
+ return error_msg
157
+
158
+ # Additional check already verified by is_path_allowed above
159
+ await tool_ctx.info(f"Editing file: {path}")
160
+
161
+ try:
162
+ file_path = Path(path)
163
+
164
+ # Check file exists
165
+ exists, error_msg = await self.check_path_exists(path, tool_ctx)
166
+ if not exists:
167
+ return error_msg
168
+
169
+ # Check is a file
170
+ is_file, error_msg = await self.check_is_file(path, tool_ctx)
171
+ if not is_file:
172
+ return error_msg
173
+
174
+ # Read the file
175
+ try:
176
+ with open(file_path, "r", encoding="utf-8") as f:
177
+ original_content = f.read()
178
+
179
+ # Apply edits
180
+ modified_content = original_content
181
+ edits_applied = 0
182
+
183
+ for edit in edits:
184
+ old_text = edit.get("oldText", "")
185
+ new_text = edit.get("newText", "")
186
+
187
+ if old_text in modified_content:
188
+ modified_content = modified_content.replace(
189
+ old_text, new_text
190
+ )
191
+ edits_applied += 1
192
+ else:
193
+ # Try line-by-line matching for whitespace flexibility
194
+ old_lines = old_text.splitlines()
195
+ content_lines = modified_content.splitlines()
196
+
197
+ for i in range(len(content_lines) - len(old_lines) + 1):
198
+ current_chunk = content_lines[i : i + len(old_lines)]
199
+
200
+ # Compare with whitespace normalization
201
+ matches = all(
202
+ old_line.strip() == content_line.strip()
203
+ for old_line, content_line in zip(
204
+ old_lines, current_chunk
205
+ )
206
+ )
207
+
208
+ if matches:
209
+ # Replace the matching lines
210
+ new_lines = new_text.splitlines()
211
+ content_lines[i : i + len(old_lines)] = new_lines
212
+ modified_content = "\n".join(content_lines)
213
+ edits_applied += 1
214
+ break
215
+
216
+ if edits_applied < len(edits):
217
+ await tool_ctx.warning(
218
+ f"Some edits could not be applied: {edits_applied}/{len(edits)}"
219
+ )
220
+
221
+ # Generate diff
222
+ original_lines = original_content.splitlines(keepends=True)
223
+ modified_lines = modified_content.splitlines(keepends=True)
224
+
225
+ diff_lines = list(
226
+ unified_diff(
227
+ original_lines,
228
+ modified_lines,
229
+ fromfile=f"{path} (original)",
230
+ tofile=f"{path} (modified)",
231
+ n=3,
232
+ )
233
+ )
234
+
235
+ diff_text = "".join(diff_lines)
236
+
237
+ # Determine the number of backticks needed
238
+ num_backticks = 3
239
+ while f"```{num_backticks}" in diff_text:
240
+ num_backticks += 1
241
+
242
+ # Format diff with appropriate number of backticks
243
+ formatted_diff = (
244
+ f"```{num_backticks}diff\n{diff_text}```{num_backticks}\n"
245
+ )
246
+
247
+ # Write the file if not a dry run
248
+ if not dry_run and diff_text: # Only write if there are changes
249
+ with open(file_path, "w", encoding="utf-8") as f:
250
+ f.write(modified_content)
251
+
252
+ # Update document context
253
+ self.document_context.update_document(path, modified_content)
254
+
255
+ await tool_ctx.info(
256
+ f"Successfully edited file: {path} ({edits_applied} edits applied)"
257
+ )
258
+ return f"Successfully edited file: {path} ({edits_applied} edits applied)\n\n{formatted_diff}"
259
+ elif not diff_text:
260
+ return f"No changes made to file: {path}"
261
+ else:
262
+ await tool_ctx.info(
263
+ f"Dry run: {edits_applied} edits would be applied"
264
+ )
265
+ return f"Dry run: {edits_applied} edits would be applied\n\n{formatted_diff}"
266
+ except UnicodeDecodeError:
267
+ await tool_ctx.error(f"Cannot edit binary file: {path}")
268
+ return f"Error: Cannot edit binary file: {path}"
269
+ except Exception as e:
270
+ await tool_ctx.error(f"Error editing file: {str(e)}")
271
+ return f"Error editing file: {str(e)}"
272
+
273
+ @override
274
+ def register(self, mcp_server: FastMCP) -> None:
275
+ """Register this edit file tool with the MCP server.
276
+
277
+ Creates a wrapper function with explicitly defined parameters that match
278
+ the tool's parameter schema and registers it with the MCP server.
279
+
280
+ Args:
281
+ mcp_server: The FastMCP server instance
282
+ """
283
+ 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 edit_file(ctx: MCPContext, path: str, edits: list[dict[str, str]], dry_run: bool = False) -> str:
287
+ return await tool_self.call(ctx, path=path, edits=edits, dry_run=dry_run)
@@ -0,0 +1,170 @@
1
+ """Get file info tool implementation.
2
+
3
+ This module provides the GetFileInfoTool for retrieving metadata about files and directories.
4
+ """
5
+
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any, final, override
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
14
+
15
+
16
+ @final
17
+ class GetFileInfoTool(FilesystemBaseTool):
18
+ """Tool for retrieving metadata about files and directories."""
19
+
20
+ @property
21
+ @override
22
+ def name(self) -> str:
23
+ """Get the tool name.
24
+
25
+ Returns:
26
+ Tool name
27
+ """
28
+ return "get_file_info"
29
+
30
+ @property
31
+ @override
32
+ def description(self) -> str:
33
+ """Get the tool description.
34
+
35
+ Returns:
36
+ Tool description
37
+ """
38
+ return """Retrieve detailed metadata about a file or directory.
39
+
40
+ Returns comprehensive information including size, creation time,
41
+ last modified time, permissions, and type. This tool is perfect for
42
+ understanding file characteristics without reading the actual content.
43
+ 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
+ "path": {
56
+ "type": "string",
57
+ "description": "path to the file or directory to inspect"
58
+ }
59
+ },
60
+ "required": ["path"],
61
+ "type": "object"
62
+ }
63
+
64
+ @property
65
+ @override
66
+ def required(self) -> list[str]:
67
+ """Get the list of required parameter names.
68
+
69
+ Returns:
70
+ List of required parameter names
71
+ """
72
+ return ["path"]
73
+
74
+ @override
75
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
76
+ """Execute the tool with the given parameters.
77
+
78
+ Args:
79
+ ctx: MCP context
80
+ **params: Tool parameters
81
+
82
+ Returns:
83
+ Tool result
84
+ """
85
+ tool_ctx = self.create_tool_context(ctx)
86
+
87
+ # Extract parameters
88
+ path = params.get("path")
89
+
90
+ if not path:
91
+ await tool_ctx.error("Parameter 'path' is required but was None")
92
+ return "Error: Parameter 'path' is required but was None"
93
+
94
+ if path.strip() == "":
95
+ await tool_ctx.error("Parameter 'path' cannot be empty")
96
+ return "Error: Parameter 'path' cannot be empty"
97
+
98
+ # Validate path parameter
99
+ path_validation = self.validate_path(path)
100
+ if path_validation.is_error:
101
+ await tool_ctx.error(path_validation.error_message)
102
+ return f"Error: {path_validation.error_message}"
103
+
104
+ await tool_ctx.info(f"Getting file info: {path}")
105
+
106
+ # Check if path is allowed
107
+ allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
108
+ if not allowed:
109
+ return error_msg
110
+
111
+ try:
112
+ file_path = Path(path)
113
+
114
+ # Check if path exists
115
+ exists, error_msg = await self.check_path_exists(path, tool_ctx)
116
+ if not exists:
117
+ return error_msg
118
+
119
+ # Get file stats
120
+ stats = file_path.stat()
121
+
122
+ # Format timestamps
123
+ created_time = time.strftime(
124
+ "%Y-%m-%d %H:%M:%S", time.localtime(stats.st_ctime)
125
+ )
126
+ modified_time = time.strftime(
127
+ "%Y-%m-%d %H:%M:%S", time.localtime(stats.st_mtime)
128
+ )
129
+ accessed_time = time.strftime(
130
+ "%Y-%m-%d %H:%M:%S", time.localtime(stats.st_atime)
131
+ )
132
+
133
+ # Format permissions in octal
134
+ permissions = oct(stats.st_mode)[-3:]
135
+
136
+ # Build info dictionary
137
+ file_info: dict[str, Any] = {
138
+ "name": file_path.name,
139
+ "type": "directory" if file_path.is_dir() else "file",
140
+ "size": stats.st_size,
141
+ "created": created_time,
142
+ "modified": modified_time,
143
+ "accessed": accessed_time,
144
+ "permissions": permissions,
145
+ }
146
+
147
+ # Format the output
148
+ result = [f"{key}: {value}" for key, value in file_info.items()]
149
+
150
+ await tool_ctx.info(f"Retrieved info for {path}")
151
+ return "\n".join(result)
152
+ except Exception as e:
153
+ await tool_ctx.error(f"Error getting file info: {str(e)}")
154
+ return f"Error getting file info: {str(e)}"
155
+
156
+ @override
157
+ def register(self, mcp_server: FastMCP) -> None:
158
+ """Register this get file info tool with the MCP server.
159
+
160
+ Creates a wrapper function with explicitly defined parameters that match
161
+ the tool's parameter schema and registers it with the MCP server.
162
+
163
+ Args:
164
+ mcp_server: The FastMCP server instance
165
+ """
166
+ tool_self = self # Create a reference to self for use in the closure
167
+
168
+ @mcp_server.tool(name=self.name, description=self.mcp_description)
169
+ async def get_file_info(path: str, ctx: MCPContext) -> str:
170
+ return await tool_self.call(ctx, path=path)
@@ -0,0 +1,198 @@
1
+ """Read files tool implementation.
2
+
3
+ This module provides the ReadFilesTool for reading the contents of files.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Any, final, override
8
+
9
+ from mcp.server.fastmcp import Context as MCPContext
10
+ from mcp.server.fastmcp import FastMCP
11
+
12
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
13
+
14
+
15
+ @final
16
+ class ReadFilesTool(FilesystemBaseTool):
17
+ """Tool for reading file contents."""
18
+
19
+ @property
20
+ @override
21
+ def name(self) -> str:
22
+ """Get the tool name.
23
+
24
+ Returns:
25
+ Tool name
26
+ """
27
+ return "read_files"
28
+
29
+ @property
30
+ @override
31
+ def description(self) -> str:
32
+ """Get the tool description.
33
+
34
+ Returns:
35
+ Tool description
36
+ """
37
+ return """Read the contents of one or multiple files.
38
+
39
+ Can read a single file or multiple files simultaneously. When reading multiple files,
40
+ each file's content is returned with its path as a reference. Failed reads for
41
+ individual files won't stop the entire operation. Only works within allowed directories."""
42
+
43
+ @property
44
+ @override
45
+ def parameters(self) -> dict[str, Any]:
46
+ """Get the parameter specifications for the tool.
47
+
48
+ Returns:
49
+ Parameter specifications
50
+ """
51
+ return {
52
+ "properties": {
53
+ "paths": {
54
+ "anyOf": [
55
+ {"items": {"type": "string"}, "type": "array"},
56
+ {"type": "string"}
57
+ ],
58
+ "description": "absolute path to the file or files to read"
59
+ }
60
+ },
61
+ "required": ["paths"],
62
+ "type": "object"
63
+ }
64
+
65
+ @property
66
+ @override
67
+ def required(self) -> list[str]:
68
+ """Get the list of required parameter names.
69
+
70
+ Returns:
71
+ List of required parameter names
72
+ """
73
+ return ["paths"]
74
+
75
+ @override
76
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
77
+ """Execute the tool with the given parameters.
78
+
79
+ Args:
80
+ ctx: MCP context
81
+ **params: Tool parameters
82
+
83
+ Returns:
84
+ Tool result
85
+ """
86
+ tool_ctx = self.create_tool_context(ctx)
87
+ self.set_tool_context_info(tool_ctx)
88
+
89
+ # Extract parameters
90
+ paths = params.get("paths")
91
+
92
+ # Validate the 'paths' parameter
93
+ if not paths:
94
+ await tool_ctx.error("Parameter 'paths' is required but was None")
95
+ return "Error: Parameter 'paths' is required but was None"
96
+
97
+ # Convert single path to list if necessary
98
+ path_list: list[str] = [paths] if isinstance(paths, str) else paths
99
+
100
+ # Handle empty list case
101
+ if not path_list:
102
+ await tool_ctx.warning("No files specified to read")
103
+ return "Error: No files specified to read"
104
+
105
+ # For a single file with direct string return
106
+ single_file_mode = isinstance(paths, str)
107
+
108
+ await tool_ctx.info(f"Reading {len(path_list)} file(s)")
109
+
110
+ results: list[str] = []
111
+
112
+ # Read each file
113
+ for i, path in enumerate(path_list):
114
+ # Report progress
115
+ await tool_ctx.report_progress(i, len(path_list))
116
+
117
+ # Check if path is allowed
118
+ if not self.is_path_allowed(path):
119
+ await tool_ctx.error(
120
+ f"Access denied - path outside allowed directories: {path}"
121
+ )
122
+ results.append(
123
+ f"{path}: Error - Access denied - path outside allowed directories"
124
+ )
125
+ continue
126
+
127
+ try:
128
+ file_path = Path(path)
129
+
130
+ if not file_path.exists():
131
+ await tool_ctx.error(f"File does not exist: {path}")
132
+ results.append(f"{path}: Error - File does not exist")
133
+ continue
134
+
135
+ if not file_path.is_file():
136
+ await tool_ctx.error(f"Path is not a file: {path}")
137
+ results.append(f"{path}: Error - Path is not a file")
138
+ continue
139
+
140
+ # Read the file
141
+ try:
142
+ with open(file_path, "r", encoding="utf-8") as f:
143
+ content = f.read()
144
+
145
+ # Add to document context
146
+ self.document_context.add_document(path, content)
147
+
148
+ results.append(f"{path}:\n{content}")
149
+ except UnicodeDecodeError:
150
+ try:
151
+ with open(file_path, "r", encoding="latin-1") as f:
152
+ content = f.read()
153
+ await tool_ctx.warning(
154
+ f"File read with latin-1 encoding: {path}"
155
+ )
156
+ results.append(f"{path} (latin-1 encoding):\n{content}")
157
+ except Exception:
158
+ await tool_ctx.error(f"Cannot read binary file: {path}")
159
+ results.append(f"{path}: Error - Cannot read binary file")
160
+ except Exception as e:
161
+ await tool_ctx.error(f"Error reading file: {str(e)}")
162
+ results.append(f"{path}: Error - {str(e)}")
163
+
164
+ # Final progress report
165
+ await tool_ctx.report_progress(len(path_list), len(path_list))
166
+
167
+ await tool_ctx.info(f"Read {len(path_list)} file(s)")
168
+
169
+ # For single file mode with direct string input, return just the content
170
+ # if successful, otherwise return the error
171
+ if single_file_mode and len(results) == 1:
172
+ result_text = results[0]
173
+ # If it's a successful read (doesn't contain "Error - ")
174
+ if not result_text.split(":", 1)[1].strip().startswith("Error - "):
175
+ # Just return the content part (after the first colon and newline)
176
+ return result_text.split(":", 1)[1].strip()
177
+ else:
178
+ # Return just the error message
179
+ return "Error: " + result_text.split("Error - ", 1)[1]
180
+
181
+ # For multiple files or failed single file read, return all results
182
+ return "\n\n---\n\n".join(results)
183
+
184
+ @override
185
+ def register(self, mcp_server: FastMCP) -> None:
186
+ """Register this tool with the MCP server.
187
+
188
+ Creates a wrapper function with explicitly defined parameters that match
189
+ the tool's parameter schema and registers it with the MCP server.
190
+
191
+ Args:
192
+ mcp_server: The FastMCP server instance
193
+ """
194
+ tool_self = self # Create a reference to self for use in the closure
195
+
196
+ @mcp_server.tool(name=self.name, description=self.mcp_description)
197
+ async def read_files(ctx: MCPContext, paths: list[str] | str) -> str:
198
+ return await tool_self.call(ctx, paths=paths)