hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (87) 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 +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 +117 -99
  14. hanzo_mcp/tools/__init__.py +105 -32
  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 -57
  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.8.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +33 -1
  59. hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
  60. {hanzo_mcp-0.3.8.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 -199
  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 -886
  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.8.dist-info/RECORD +0 -53
  85. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
  86. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
  87. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,123 +0,0 @@
1
- """Thinking tool implementation.
2
-
3
- This module provides the ThinkingTool for Claude to engage in structured thinking.
4
- """
5
-
6
- from typing import Any, final, override
7
-
8
- from mcp.server.fastmcp import Context as MCPContext
9
- from mcp.server.fastmcp import FastMCP
10
-
11
- from hanzo_mcp.tools.common.base import BaseTool
12
- from hanzo_mcp.tools.common.context import create_tool_context
13
-
14
-
15
- @final
16
- class ThinkingTool(BaseTool):
17
- """Tool for Claude to engage in structured thinking."""
18
-
19
- @property
20
- @override
21
- def name(self) -> str:
22
- """Get the tool name.
23
-
24
- Returns:
25
- Tool name
26
- """
27
- return "think"
28
-
29
- @property
30
- @override
31
- def description(self) -> str:
32
- """Get the tool description.
33
-
34
- Returns:
35
- Tool description
36
- """
37
- return """Use the tool to think about something.
38
-
39
- It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed. For example, if you explore the repo and discover the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective. Alternatively, if you receive some test results, call this tool to brainstorm ways to fix the failing tests."""
40
-
41
- @property
42
- @override
43
- def parameters(self) -> dict[str, Any]:
44
- """Get the parameter specifications for the tool.
45
-
46
- Returns:
47
- Parameter specifications
48
- """
49
- return {
50
- "properties": {
51
- "thought": {
52
- "title": "Thought",
53
- "type": "string"
54
- }
55
- },
56
- "required": ["thought"],
57
- "title": "thinkArguments",
58
- "type": "object"
59
- }
60
-
61
- @property
62
- @override
63
- def required(self) -> list[str]:
64
- """Get the list of required parameter names.
65
-
66
- Returns:
67
- List of required parameter names
68
- """
69
- return ["thought"]
70
-
71
- def __init__(self) -> None:
72
- """Initialize the thinking tool."""
73
- pass
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 = create_tool_context(ctx)
87
- tool_ctx.set_tool_info(self.name)
88
-
89
- # Extract parameters
90
- thought = params.get("thought")
91
-
92
- # Validate required thought parameter
93
- if not thought:
94
- await tool_ctx.error(
95
- "Parameter 'thought' is required but was None or empty"
96
- )
97
- return "Error: Parameter 'thought' is required but was None or empty"
98
-
99
- if thought.strip() == "":
100
- await tool_ctx.error("Parameter 'thought' cannot be empty")
101
- return "Error: Parameter 'thought' cannot be empty"
102
-
103
- # Log the thought but don't take action
104
- await tool_ctx.info("Thinking process recorded")
105
-
106
- # Return confirmation
107
- return "I've recorded your thinking process. You can continue with your next action based on this analysis."
108
-
109
- @override
110
- def register(self, mcp_server: FastMCP) -> None:
111
- """Register this thinking tool with the MCP server.
112
-
113
- Creates a wrapper function with explicitly defined parameters that match
114
- the tool's parameter schema and registers it with the MCP server.
115
-
116
- Args:
117
- mcp_server: The FastMCP server instance
118
- """
119
- tool_self = self # Create a reference to self for use in the closure
120
-
121
- @mcp_server.tool(name=self.name, description=self.mcp_description)
122
- async def think(thought: str, ctx: MCPContext) -> str:
123
- return await tool_self.call(ctx, thought=thought)
@@ -1,120 +0,0 @@
1
- """Version tool for displaying project version information."""
2
-
3
- from typing import Any, Dict, TypedDict, final, override
4
-
5
- from mcp.server.fastmcp import Context as MCPContext
6
- from mcp.server.fastmcp import FastMCP
7
-
8
- from hanzo_mcp.tools.common.base import BaseTool
9
- from hanzo_mcp.tools.common.context import create_tool_context
10
-
11
-
12
- class VersionToolResponse(TypedDict):
13
- """Response from the version tool."""
14
-
15
- version: str
16
- package_name: str
17
-
18
-
19
- @final
20
- class VersionTool(BaseTool):
21
- """Tool for displaying version information about the Hanzo MCP package."""
22
-
23
- @property
24
- @override
25
- def name(self) -> str:
26
- """Get the tool name.
27
-
28
- Returns:
29
- Tool name
30
- """
31
- return "version"
32
-
33
- @property
34
- @override
35
- def description(self) -> str:
36
- """Get the tool description.
37
-
38
- Returns:
39
- Tool description
40
- """
41
- return "Display the current version of hanzo-mcp"
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
- "required": [],
54
- "title": "versionArguments",
55
- "type": "object"
56
- }
57
-
58
- @property
59
- @override
60
- def required(self) -> list[str]:
61
- """Get the list of required parameter names.
62
-
63
- Returns:
64
- List of required parameter names
65
- """
66
- return []
67
-
68
- def __init__(self, mcp_server: FastMCP) -> None:
69
- """Initialize the version tool and register it with the server.
70
-
71
- Args:
72
- mcp_server: The MCP server to register with
73
- """
74
- self.register(mcp_server)
75
-
76
- @override
77
- async def call(self, ctx: MCPContext, **params: Any) -> str:
78
- """Execute the tool with the given parameters.
79
-
80
- Args:
81
- ctx: MCP context
82
- **params: Tool parameters
83
-
84
- Returns:
85
- Tool result with version information
86
- """
87
- tool_ctx = create_tool_context(ctx)
88
- tool_ctx.set_tool_info(self.name)
89
-
90
- version_info = self.get_version()
91
- await tool_ctx.info(f"Hanzo MCP version: {version_info['version']}")
92
-
93
- return f"Hanzo MCP version: {version_info['version']}"
94
-
95
- def get_version(self) -> VersionToolResponse:
96
- """Get the current version of the hanzo-mcp package.
97
-
98
- Returns:
99
- A dictionary containing the package name and version
100
- """
101
- # Directly use the __version__ from the hanzo_mcp package
102
- from hanzo_mcp import __version__
103
-
104
- return {"version": __version__, "package_name": "hanzo-mcp"}
105
-
106
- @override
107
- def register(self, mcp_server: FastMCP) -> None:
108
- """Register this version tool with the MCP server.
109
-
110
- Creates a wrapper function that calls this tool's call method and
111
- registers it with the MCP server.
112
-
113
- Args:
114
- mcp_server: The FastMCP server instance
115
- """
116
- tool_self = self # Create a reference to self for use in the closure
117
-
118
- @mcp_server.tool(name=self.name, description=self.mcp_description)
119
- async def version(ctx: MCPContext) -> str:
120
- return await tool_self.call(ctx)
@@ -1,287 +0,0 @@
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)
@@ -1,170 +0,0 @@
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)