hanzo-mcp 0.3.4__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.

Files changed (87) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +123 -160
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +388 -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 +120 -98
  14. hanzo_mcp/tools/__init__.py +107 -31
  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/context.py +26 -292
  23. hanzo_mcp/tools/common/permissions.py +12 -12
  24. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  25. hanzo_mcp/tools/common/validation.py +1 -63
  26. hanzo_mcp/tools/filesystem/__init__.py +88 -41
  27. hanzo_mcp/tools/filesystem/base.py +32 -24
  28. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  29. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  30. hanzo_mcp/tools/filesystem/edit.py +279 -0
  31. hanzo_mcp/tools/filesystem/grep.py +458 -0
  32. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  33. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  34. hanzo_mcp/tools/filesystem/read.py +255 -0
  35. hanzo_mcp/tools/filesystem/write.py +156 -0
  36. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  37. hanzo_mcp/tools/jupyter/base.py +66 -57
  38. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  39. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  40. hanzo_mcp/tools/shell/__init__.py +29 -20
  41. hanzo_mcp/tools/shell/base.py +87 -45
  42. hanzo_mcp/tools/shell/bash_session.py +731 -0
  43. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  44. hanzo_mcp/tools/shell/command_executor.py +435 -384
  45. hanzo_mcp/tools/shell/run_command.py +284 -131
  46. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  47. hanzo_mcp/tools/shell/session_manager.py +196 -0
  48. hanzo_mcp/tools/shell/session_storage.py +325 -0
  49. hanzo_mcp/tools/todo/__init__.py +66 -0
  50. hanzo_mcp/tools/todo/base.py +319 -0
  51. hanzo_mcp/tools/todo/todo_read.py +148 -0
  52. hanzo_mcp/tools/todo/todo_write.py +378 -0
  53. hanzo_mcp/tools/vector/__init__.py +95 -0
  54. hanzo_mcp/tools/vector/infinity_store.py +365 -0
  55. hanzo_mcp/tools/vector/project_manager.py +361 -0
  56. hanzo_mcp/tools/vector/vector_index.py +115 -0
  57. hanzo_mcp/tools/vector/vector_search.py +215 -0
  58. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +35 -3
  59. hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
  60. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
  61. hanzo_mcp/tools/agent/base_provider.py +0 -73
  62. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  63. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  64. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  65. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  66. hanzo_mcp/tools/common/error_handling.py +0 -86
  67. hanzo_mcp/tools/common/logging_config.py +0 -115
  68. hanzo_mcp/tools/common/session.py +0 -91
  69. hanzo_mcp/tools/common/think_tool.py +0 -123
  70. hanzo_mcp/tools/common/version_tool.py +0 -120
  71. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  72. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  73. hanzo_mcp/tools/filesystem/read_files.py +0 -198
  74. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  75. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  76. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  77. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  78. hanzo_mcp/tools/project/__init__.py +0 -64
  79. hanzo_mcp/tools/project/analysis.py +0 -882
  80. hanzo_mcp/tools/project/base.py +0 -66
  81. hanzo_mcp/tools/project/project_analyze.py +0 -173
  82. hanzo_mcp/tools/shell/run_script.py +0 -215
  83. hanzo_mcp/tools/shell/script_tool.py +0 -244
  84. hanzo_mcp-0.3.4.dist-info/RECORD +0 -53
  85. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
  86. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
  87. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,198 +0,0 @@
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)
@@ -1,275 +0,0 @@
1
- """Search content tool implementation.
2
-
3
- This module provides the SearchContentTool for finding text patterns in files.
4
- """
5
-
6
- import asyncio
7
- import fnmatch
8
- import re
9
- from pathlib import Path
10
- from typing import Any, final, override
11
-
12
- from mcp.server.fastmcp import Context as MCPContext
13
- from mcp.server.fastmcp import FastMCP
14
-
15
- from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
16
-
17
-
18
- @final
19
- class SearchContentTool(FilesystemBaseTool):
20
- """Tool for searching for text patterns in files."""
21
-
22
- @property
23
- @override
24
- def name(self) -> str:
25
- """Get the tool name.
26
-
27
- Returns:
28
- Tool name
29
- """
30
- return "search_content"
31
-
32
- @property
33
- @override
34
- def description(self) -> str:
35
- """Get the tool description.
36
-
37
- Returns:
38
- Tool description
39
- """
40
- return """Search for a pattern in file contents.
41
-
42
- Similar to grep, this tool searches for text patterns within files.
43
- Searches recursively through all files in the specified directory
44
- that match the file pattern. Returns matching lines with file and
45
- line number references. Only searches within allowed directories."""
46
-
47
- @property
48
- @override
49
- def parameters(self) -> dict[str, Any]:
50
- """Get the parameter specifications for the tool.
51
-
52
- Returns:
53
- Parameter specifications
54
- """
55
- return {
56
- "properties": {
57
- "pattern": {
58
- "type": "string",
59
- "description": "text pattern to search for"
60
- },
61
- "path": {
62
- "type": "string",
63
- "description": "path to the directory or file to search"
64
- },
65
- "file_pattern": {
66
- "default": "*",
67
- "type": "string"
68
- }
69
- },
70
- "required": ["pattern", "path"],
71
- "title": "search_contentArguments",
72
- "type": "object"
73
- }
74
-
75
- @property
76
- @override
77
- def required(self) -> list[str]:
78
- """Get the list of required parameter names.
79
-
80
- Returns:
81
- List of required parameter names
82
- """
83
- return ["pattern", "path"]
84
-
85
- @override
86
- async def call(self, ctx: MCPContext, **params: Any) -> str:
87
- """Execute the tool with the given parameters.
88
-
89
- Args:
90
- ctx: MCP context
91
- **params: Tool parameters
92
-
93
- Returns:
94
- Tool result
95
- """
96
- tool_ctx = self.create_tool_context(ctx)
97
-
98
- # Extract parameters
99
- pattern = params.get("pattern")
100
- path = params.get("path")
101
- file_pattern = params.get("file_pattern", "*") # Default to all files
102
-
103
- # Validate required parameters
104
- if not pattern:
105
- await tool_ctx.error("Parameter 'pattern' is required but was None")
106
- return "Error: Parameter 'pattern' is required but was None"
107
-
108
- if pattern.strip() == "":
109
- await tool_ctx.error("Parameter 'pattern' cannot be empty")
110
- return "Error: Parameter 'pattern' cannot be empty"
111
-
112
- if not path:
113
- await tool_ctx.error("Parameter 'path' is required but was None")
114
- return "Error: Parameter 'path' is required but was None"
115
-
116
- if path.strip() == "":
117
- await tool_ctx.error("Parameter 'path' cannot be empty")
118
- return "Error: Parameter 'path' cannot be empty"
119
-
120
-
121
- path_validation = self.validate_path(path)
122
- if path_validation.is_error:
123
- await tool_ctx.error(path_validation.error_message)
124
- return f"Error: {path_validation.error_message}"
125
-
126
- # file_pattern can be None safely as it has a default value
127
-
128
- await tool_ctx.info(
129
- f"Searching for pattern '{pattern}' in files matching '{file_pattern}' in {path}"
130
- )
131
-
132
- # Check if path is allowed
133
- allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
134
- if not allowed:
135
- return error_msg
136
-
137
- try:
138
- input_path = Path(path)
139
-
140
- # Check if path exists
141
- exists, error_msg = await self.check_path_exists(path, tool_ctx)
142
- if not exists:
143
- return error_msg
144
-
145
- # Find matching files using optimized file finding
146
- matching_files: list[Path] = []
147
-
148
- # Process based on whether path is a file or directory
149
- if input_path.is_file():
150
- # Single file search
151
- if file_pattern == "*" or fnmatch.fnmatch(input_path.name, file_pattern):
152
- matching_files.append(input_path)
153
- await tool_ctx.info(f"Searching single file: {path}")
154
- else:
155
- await tool_ctx.info(f"File does not match pattern '{file_pattern}': {path}")
156
- return f"File does not match pattern '{file_pattern}': {path}"
157
- elif input_path.is_dir():
158
- # Directory search - optimized file finding
159
- await tool_ctx.info(f"Finding files in directory: {path}")
160
-
161
- # Keep track of allowed paths for filtering
162
- allowed_paths: set[str] = set()
163
-
164
- # Collect all allowed paths first for faster filtering
165
- for entry in input_path.rglob("*"):
166
- entry_path = str(entry)
167
- if self.is_path_allowed(entry_path):
168
- allowed_paths.add(entry_path)
169
-
170
- # Find matching files efficiently
171
- for entry in input_path.rglob("*"):
172
- entry_path = str(entry)
173
- if entry_path in allowed_paths and entry.is_file():
174
- if file_pattern == "*" or fnmatch.fnmatch(entry.name, file_pattern):
175
- matching_files.append(entry)
176
-
177
- await tool_ctx.info(f"Found {len(matching_files)} matching files")
178
- else:
179
- # This shouldn't happen since we already checked for existence
180
- await tool_ctx.error(f"Path is neither a file nor a directory: {path}")
181
- return f"Error: Path is neither a file nor a directory: {path}"
182
-
183
- # Report progress
184
- total_files = len(matching_files)
185
- if input_path.is_file():
186
- await tool_ctx.info(f"Searching file: {path}")
187
- else:
188
- await tool_ctx.info(f"Searching through {total_files} files in directory")
189
-
190
- # set up for parallel processing
191
- results: list[str] = []
192
- files_processed = 0
193
- matches_found = 0
194
- batch_size = 20 # Process files in batches to avoid overwhelming the system
195
-
196
- # Use a semaphore to limit concurrent file operations
197
- # Adjust the value based on system resources
198
- semaphore = asyncio.Semaphore(10)
199
-
200
- # Create an async function to search a single file
201
- async def search_file(file_path: Path) -> list[str]:
202
- nonlocal files_processed, matches_found
203
- file_results: list[str] = []
204
-
205
- try:
206
- async with semaphore: # Limit concurrent operations
207
- try:
208
- with open(file_path, "r", encoding="utf-8") as f:
209
- for line_num, line in enumerate(f, 1):
210
- if re.search(pattern, line):
211
- file_results.append(
212
- f"{file_path}:{line_num}: {line.rstrip()}"
213
- )
214
- matches_found += 1
215
- files_processed += 1
216
- except UnicodeDecodeError:
217
- # Skip binary files
218
- files_processed += 1
219
- except Exception as e:
220
- await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
221
- except Exception as e:
222
- await tool_ctx.warning(f"Error processing {file_path}: {str(e)}")
223
-
224
- return file_results
225
-
226
- # Process files in parallel batches
227
- for i in range(0, len(matching_files), batch_size):
228
- batch = matching_files[i:i+batch_size]
229
- batch_tasks = [search_file(file_path) for file_path in batch]
230
-
231
- # Report progress
232
- await tool_ctx.report_progress(i, total_files)
233
-
234
- # Wait for the batch to complete
235
- batch_results = await asyncio.gather(*batch_tasks)
236
-
237
- # Flatten and collect results
238
- for file_result in batch_results:
239
- results.extend(file_result)
240
-
241
- # Final progress report
242
- await tool_ctx.report_progress(total_files, total_files)
243
-
244
- if not results:
245
- if input_path.is_file():
246
- return f"No matches found for pattern '{pattern}' in file: {path}"
247
- else:
248
- return f"No matches found for pattern '{pattern}' in files matching '{file_pattern}' in directory: {path}"
249
-
250
- await tool_ctx.info(
251
- f"Found {matches_found} matches in {files_processed} file{'s' if files_processed > 1 else ''}"
252
- )
253
- return (
254
- f"Found {matches_found} matches in {files_processed} file{'s' if files_processed > 1 else ''}:\n\n"
255
- + "\n".join(results)
256
- )
257
- except Exception as e:
258
- await tool_ctx.error(f"Error searching file contents: {str(e)}")
259
- return f"Error searching file contents: {str(e)}"
260
-
261
- @override
262
- def register(self, mcp_server: FastMCP) -> None:
263
- """Register this search content tool with the MCP server.
264
-
265
- Creates a wrapper function with explicitly defined parameters that match
266
- the tool's parameter schema and registers it with the MCP server.
267
-
268
- Args:
269
- mcp_server: The FastMCP server instance
270
- """
271
- tool_self = self # Create a reference to self for use in the closure
272
-
273
- @mcp_server.tool(name=self.name, description=self.mcp_description)
274
- async def search_content(ctx: MCPContext, pattern: str, path: str, file_pattern: str = "*") -> str:
275
- return await tool_self.call(ctx, pattern=pattern, path=path, file_pattern=file_pattern)
@@ -1,162 +0,0 @@
1
- """Write file tool implementation.
2
-
3
- This module provides the WriteFileTool for creating or overwriting 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 WriteFileTool(FilesystemBaseTool):
17
- """Tool for writing 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 "write_file"
28
-
29
- @property
30
- @override
31
- def description(self) -> str:
32
- """Get the tool description.
33
-
34
- Returns:
35
- Tool description
36
- """
37
- return """Create a new file or completely overwrite an existing file with new content.
38
-
39
- Use with caution as it will overwrite existing files without warning.
40
- Handles text content with proper encoding. Only works within allowed directories."""
41
-
42
- @property
43
- @override
44
- def parameters(self) -> dict[str, Any]:
45
- """Get the parameter specifications for the tool.
46
-
47
- Returns:
48
- Parameter specifications
49
- """
50
- return {
51
- "properties": {
52
- "path": {
53
- "type": "string",
54
- "description": "path to the file to write"
55
- },
56
- "content": {
57
- "type": "string",
58
- "description": "content to write to the file"
59
- }
60
- },
61
- "required": ["path", "content"],
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 ["path", "content"]
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
- path = params.get("path")
91
- content = params.get("content")
92
-
93
- if not path:
94
- await tool_ctx.error("Parameter 'path' is required but was None")
95
- return "Error: Parameter 'path' is required but was None"
96
-
97
- if path.strip() == "":
98
- await tool_ctx.error("Parameter 'path' cannot be empty")
99
- return "Error: Parameter 'path' cannot be empty"
100
-
101
- # Validate parameters
102
- path_validation = self.validate_path(path)
103
- if path_validation.is_error:
104
- await tool_ctx.error(path_validation.error_message)
105
- return f"Error: {path_validation.error_message}"
106
-
107
- if not content:
108
- await tool_ctx.error("Parameter 'content' is required but was None")
109
- return "Error: Parameter 'content' is required but was None"
110
-
111
- await tool_ctx.info(f"Writing file: {path}")
112
-
113
- # Check if file is allowed to be written
114
- allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
115
- if not allowed:
116
- return error_msg
117
-
118
- # Additional check already verified by is_path_allowed above
119
- await tool_ctx.info(f"Writing file: {path}")
120
-
121
- try:
122
- file_path = Path(path)
123
-
124
- # Check if parent directory is allowed
125
- parent_dir = str(file_path.parent)
126
- if not self.is_path_allowed(parent_dir):
127
- await tool_ctx.error(f"Parent directory not allowed: {parent_dir}")
128
- return f"Error: Parent directory not allowed: {parent_dir}"
129
-
130
- # Create parent directories if they don't exist
131
- file_path.parent.mkdir(parents=True, exist_ok=True)
132
-
133
- # Write the file
134
- with open(file_path, "w", encoding="utf-8") as f:
135
- f.write(content)
136
-
137
- # Add to document context
138
- self.document_context.add_document(path, content)
139
-
140
- await tool_ctx.info(
141
- f"Successfully wrote file: {path} ({len(content)} bytes)"
142
- )
143
- return f"Successfully wrote file: {path} ({len(content)} bytes)"
144
- except Exception as e:
145
- await tool_ctx.error(f"Error writing file: {str(e)}")
146
- return f"Error writing file: {str(e)}"
147
-
148
- @override
149
- def register(self, mcp_server: FastMCP) -> None:
150
- """Register this tool with the MCP server.
151
-
152
- Creates a wrapper function with explicitly defined parameters that match
153
- the tool's parameter schema and registers it with the MCP server.
154
-
155
- Args:
156
- mcp_server: The FastMCP server instance
157
- """
158
- tool_self = self # Create a reference to self for use in the closure
159
-
160
- @mcp_server.tool(name=self.name, description=self.mcp_description)
161
- async def write_file(path: str, content: str, ctx: MCPContext) -> str:
162
- return await tool_self.call(ctx, path=path, content=content)