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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +123 -160
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +388 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +120 -98
- hanzo_mcp/tools/__init__.py +107 -31
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +88 -41
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +95 -0
- hanzo_mcp/tools/vector/infinity_store.py +365 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +115 -0
- hanzo_mcp/tools/vector/vector_search.py +215 -0
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +35 -3
- hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -198
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -882
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.4.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.4.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)
|