hanzo-mcp 0.1.21__py3-none-any.whl → 0.1.30__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 (48) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +81 -10
  3. hanzo_mcp/server.py +42 -11
  4. hanzo_mcp/tools/__init__.py +51 -32
  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 -1
  10. hanzo_mcp/tools/common/base.py +216 -0
  11. hanzo_mcp/tools/common/context.py +9 -5
  12. hanzo_mcp/tools/common/permissions.py +7 -3
  13. hanzo_mcp/tools/common/session.py +91 -0
  14. hanzo_mcp/tools/common/thinking_tool.py +123 -0
  15. hanzo_mcp/tools/common/validation.py +1 -1
  16. hanzo_mcp/tools/filesystem/__init__.py +85 -5
  17. hanzo_mcp/tools/filesystem/base.py +113 -0
  18. hanzo_mcp/tools/filesystem/content_replace.py +287 -0
  19. hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
  20. hanzo_mcp/tools/filesystem/edit_file.py +287 -0
  21. hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
  22. hanzo_mcp/tools/filesystem/read_files.py +198 -0
  23. hanzo_mcp/tools/filesystem/search_content.py +275 -0
  24. hanzo_mcp/tools/filesystem/write_file.py +162 -0
  25. hanzo_mcp/tools/jupyter/__init__.py +67 -4
  26. hanzo_mcp/tools/jupyter/base.py +284 -0
  27. hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
  28. hanzo_mcp/tools/jupyter/notebook_operations.py +73 -113
  29. hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
  30. hanzo_mcp/tools/project/__init__.py +64 -1
  31. hanzo_mcp/tools/project/analysis.py +8 -5
  32. hanzo_mcp/tools/project/base.py +66 -0
  33. hanzo_mcp/tools/project/project_analyze.py +173 -0
  34. hanzo_mcp/tools/shell/__init__.py +58 -1
  35. hanzo_mcp/tools/shell/base.py +148 -0
  36. hanzo_mcp/tools/shell/command_executor.py +198 -317
  37. hanzo_mcp/tools/shell/run_command.py +204 -0
  38. hanzo_mcp/tools/shell/run_script.py +215 -0
  39. hanzo_mcp/tools/shell/script_tool.py +244 -0
  40. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/METADATA +72 -77
  41. hanzo_mcp-0.1.30.dist-info/RECORD +45 -0
  42. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/WHEEL +1 -1
  43. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/licenses/LICENSE +2 -2
  44. hanzo_mcp/tools/common/thinking.py +0 -65
  45. hanzo_mcp/tools/filesystem/file_operations.py +0 -1050
  46. hanzo_mcp-0.1.21.dist-info/RECORD +0 -23
  47. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/entry_points.txt +0 -0
  48. {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,204 @@
1
+ """Run command tool implementation.
2
+
3
+ This module provides the RunCommandTool for running shell commands.
4
+ """
5
+
6
+ import os
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.common.context import create_tool_context
13
+ from hanzo_mcp.tools.shell.base import ShellBaseTool
14
+ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
15
+
16
+
17
+ @final
18
+ class RunCommandTool(ShellBaseTool):
19
+ """Tool for executing shell commands."""
20
+
21
+ def __init__(self, permission_manager: Any, command_executor: CommandExecutor) -> None:
22
+ """Initialize the run command tool.
23
+
24
+ Args:
25
+ permission_manager: Permission manager for access control
26
+ command_executor: Command executor for running commands
27
+ """
28
+ super().__init__(permission_manager)
29
+ self.command_executor: CommandExecutor = command_executor
30
+
31
+ @property
32
+ @override
33
+ def name(self) -> str:
34
+ """Get the tool name.
35
+
36
+ Returns:
37
+ Tool name
38
+ """
39
+ return "run_command"
40
+
41
+ @property
42
+ @override
43
+ def description(self) -> str:
44
+ """Get the tool description.
45
+
46
+ Returns:
47
+ Tool description
48
+ """
49
+ return """Execute a shell command.
50
+
51
+ Args:
52
+ command: The shell command to execute
53
+ cwd: Working directory for the command. MUST be a subdirectory of one of the allowed paths, not a parent directory. Specify the most specific path possible.
54
+
55
+ use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
56
+
57
+ Returns:
58
+ The output of the command
59
+ """
60
+
61
+ @property
62
+ @override
63
+ def parameters(self) -> dict[str, Any]:
64
+ """Get the parameter specifications for the tool.
65
+
66
+ Returns:
67
+ Parameter specifications
68
+ """
69
+ return {
70
+ "properties": {
71
+ "command": {
72
+ "title": "Command",
73
+ "type": "string"
74
+ },
75
+ "cwd": {
76
+ "title": "Cwd",
77
+ "type": "string"
78
+ },
79
+ "use_login_shell": {
80
+ "default": True,
81
+ "title": "Use Login Shell",
82
+ "type": "boolean"
83
+ }
84
+ },
85
+ "required": ["command", "cwd"],
86
+ "title": "run_commandArguments",
87
+ "type": "object"
88
+ }
89
+
90
+ @property
91
+ @override
92
+ def required(self) -> list[str]:
93
+ """Get the list of required parameter names.
94
+
95
+ Returns:
96
+ List of required parameter names
97
+ """
98
+ return ["command", "cwd"]
99
+
100
+ @override
101
+ async def prepare_tool_context(self, ctx: MCPContext) -> Any:
102
+ """Create and prepare the tool context.
103
+
104
+ Args:
105
+ ctx: MCP context
106
+
107
+ Returns:
108
+ Prepared tool context
109
+ """
110
+ tool_ctx = create_tool_context(ctx)
111
+ tool_ctx.set_tool_info(self.name)
112
+ return tool_ctx
113
+
114
+ @override
115
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
116
+ """Execute the tool with the given parameters.
117
+
118
+ Args:
119
+ ctx: MCP context
120
+ **params: Tool parameters
121
+
122
+ Returns:
123
+ Tool result
124
+ """
125
+ tool_ctx = await self.prepare_tool_context(ctx)
126
+
127
+ # Extract parameters
128
+ command = params.get("command")
129
+ cwd = params.get("cwd")
130
+ use_login_shell = params.get("use_login_shell", True)
131
+
132
+ # Validate required parameters
133
+ if not command:
134
+ await tool_ctx.error("Parameter 'command' is required but was None")
135
+ return "Error: Parameter 'command' is required but was None"
136
+
137
+ if command.strip() == "":
138
+ await tool_ctx.error("Parameter 'command' cannot be empty")
139
+ return "Error: Parameter 'command' cannot be empty"
140
+
141
+ if not cwd:
142
+ await tool_ctx.error("Parameter 'cwd' is required but was None")
143
+ return "Error: Parameter 'cwd' is required but was None"
144
+
145
+ if cwd.strip() == "":
146
+ await tool_ctx.error("Parameter 'cwd' cannot be empty")
147
+ return "Error: Parameter 'cwd' cannot be empty"
148
+
149
+ await tool_ctx.info(f"Executing command: {command}")
150
+
151
+ # Check if command is allowed
152
+ if not self.command_executor.is_command_allowed(command):
153
+ await tool_ctx.error(f"Command not allowed: {command}")
154
+ return f"Error: Command not allowed: {command}"
155
+
156
+ # Check if working directory is allowed
157
+ if not self.is_path_allowed(cwd):
158
+ await tool_ctx.error(f"Working directory not allowed: {cwd}")
159
+ return f"Error: Working directory not allowed: {cwd}"
160
+
161
+ # Check if working directory exists
162
+ if not os.path.isdir(cwd):
163
+ await tool_ctx.error(f"Working directory does not exist: {cwd}")
164
+ return f"Error: Working directory does not exist: {cwd}"
165
+
166
+ # Execute the command
167
+ result = await self.command_executor.execute_command(
168
+ command,
169
+ cwd=cwd,
170
+ timeout=30.0,
171
+ use_login_shell=use_login_shell
172
+ )
173
+
174
+ # Report result
175
+ if result.is_success:
176
+ await tool_ctx.info("Command executed successfully")
177
+ else:
178
+ await tool_ctx.error(f"Command failed with exit code {result.return_code}")
179
+
180
+ # Format the result
181
+ if result.is_success:
182
+ # For successful commands, just return stdout unless stderr has content
183
+ if result.stderr:
184
+ return f"Command executed successfully.\n\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
185
+ return result.stdout
186
+ else:
187
+ # For failed commands, include all available information
188
+ return result.format_output()
189
+
190
+ @override
191
+ def register(self, mcp_server: FastMCP) -> None:
192
+ """Register this run command tool with the MCP server.
193
+
194
+ Creates a wrapper function with explicitly defined parameters that match
195
+ the tool's parameter schema and registers it with the MCP server.
196
+
197
+ Args:
198
+ mcp_server: The FastMCP server instance
199
+ """
200
+ tool_self = self # Create a reference to self for use in the closure
201
+
202
+ @mcp_server.tool(name=self.name, description=self.mcp_description)
203
+ async def run_command(ctx: MCPContext, command: str, cwd: str, use_login_shell: bool = True) -> str:
204
+ return await tool_self.call(ctx, command=command, cwd=cwd, use_login_shell=use_login_shell)
@@ -0,0 +1,215 @@
1
+ """Run script tool implementation.
2
+
3
+ This module provides the RunScriptTool for executing scripts with interpreters.
4
+ """
5
+
6
+ import os
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.common.context import create_tool_context
13
+ from hanzo_mcp.tools.shell.base import ShellBaseTool
14
+ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
15
+
16
+
17
+ @final
18
+ class RunScriptTool(ShellBaseTool):
19
+ """Tool for executing scripts with interpreters."""
20
+
21
+ def __init__(self, permission_manager: Any, command_executor: CommandExecutor) -> None:
22
+ """Initialize the run script tool.
23
+
24
+ Args:
25
+ permission_manager: Permission manager for access control
26
+ command_executor: Command executor for running scripts
27
+ """
28
+ super().__init__(permission_manager)
29
+ self.command_executor: CommandExecutor = command_executor
30
+
31
+ @property
32
+ @override
33
+ def name(self) -> str:
34
+ """Get the tool name.
35
+
36
+ Returns:
37
+ Tool name
38
+ """
39
+ return "run_script"
40
+
41
+ @property
42
+ @override
43
+ def description(self) -> str:
44
+ """Get the tool description.
45
+
46
+ Returns:
47
+ Tool description
48
+ """
49
+ return """Execute a script with the specified interpreter.
50
+
51
+ Args:
52
+ script: The script content to execute
53
+ cwd: Working directory for script execution. MUST be a subdirectory of one of the allowed paths, not a parent directory. Specify the most specific path possible.
54
+
55
+ interpreter: The interpreter to use (bash, python, etc.)
56
+ use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
57
+
58
+ Returns:
59
+ The output of the script
60
+ """
61
+
62
+ @property
63
+ @override
64
+ def parameters(self) -> dict[str, Any]:
65
+ """Get the parameter specifications for the tool.
66
+
67
+ Returns:
68
+ Parameter specifications
69
+ """
70
+ return {
71
+ "properties": {
72
+ "script": {
73
+ "title": "Script",
74
+ "type": "string"
75
+ },
76
+ "cwd": {
77
+ "title": "Cwd",
78
+ "type": "string"
79
+ },
80
+ "interpreter": {
81
+ "default": "bash",
82
+ "title": "Interpreter",
83
+ "type": "string"
84
+ },
85
+ "use_login_shell": {
86
+ "default": True,
87
+ "title": "Use Login Shell",
88
+ "type": "boolean"
89
+ }
90
+ },
91
+ "required": ["script", "cwd"],
92
+ "title": "run_scriptArguments",
93
+ "type": "object"
94
+ }
95
+
96
+ @property
97
+ @override
98
+ def required(self) -> list[str]:
99
+ """Get the list of required parameter names.
100
+
101
+ Returns:
102
+ List of required parameter names
103
+ """
104
+ return ["script", "cwd"]
105
+
106
+ @override
107
+ async def prepare_tool_context(self, ctx: MCPContext) -> Any:
108
+ """Create and prepare the tool context.
109
+
110
+ Args:
111
+ ctx: MCP context
112
+
113
+ Returns:
114
+ Prepared tool context
115
+ """
116
+ tool_ctx = create_tool_context(ctx)
117
+ tool_ctx.set_tool_info(self.name)
118
+ return tool_ctx
119
+
120
+ @override
121
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
122
+ """Execute the tool with the given parameters.
123
+
124
+ Args:
125
+ ctx: MCP context
126
+ **params: Tool parameters
127
+
128
+ Returns:
129
+ Tool result
130
+ """
131
+ tool_ctx = await self.prepare_tool_context(ctx)
132
+
133
+ # Extract parameters
134
+ script = params.get("script")
135
+ cwd = params.get("cwd")
136
+ interpreter = params.get("interpreter", "bash")
137
+ use_login_shell = params.get("use_login_shell", True)
138
+
139
+ # Validate required parameters
140
+ if not script:
141
+ await tool_ctx.error("Parameter 'script' is required but was None")
142
+ return "Error: Parameter 'script' is required but was None"
143
+
144
+ if script.strip() == "":
145
+ await tool_ctx.error("Parameter 'script' cannot be empty")
146
+ return "Error: Parameter 'script' cannot be empty"
147
+
148
+ # Validate interpreter
149
+ if not interpreter:
150
+ interpreter = "bash" # Use default if None
151
+ elif interpreter.strip() == "":
152
+ await tool_ctx.error("Parameter 'interpreter' cannot be empty")
153
+ return "Error: Parameter 'interpreter' cannot be empty"
154
+
155
+ # Validate required cwd parameter
156
+ if not cwd:
157
+ await tool_ctx.error("Parameter 'cwd' is required but was None")
158
+ return "Error: Parameter 'cwd' is required but was None"
159
+
160
+ if cwd.strip() == "":
161
+ await tool_ctx.error("Parameter 'cwd' cannot be empty")
162
+ return "Error: Parameter 'cwd' cannot be empty"
163
+
164
+ await tool_ctx.info(f"Executing script with interpreter: {interpreter}")
165
+
166
+ # Check if working directory is allowed
167
+ if not self.is_path_allowed(cwd):
168
+ await tool_ctx.error(f"Working directory not allowed: {cwd}")
169
+ return f"Error: Working directory not allowed: {cwd}"
170
+
171
+ # Check if working directory exists
172
+ if not os.path.isdir(cwd):
173
+ await tool_ctx.error(f"Working directory does not exist: {cwd}")
174
+ return f"Error: Working directory does not exist: {cwd}"
175
+
176
+ # Execute the script
177
+ result = await self.command_executor.execute_script(
178
+ script=script,
179
+ interpreter=interpreter,
180
+ cwd=cwd,
181
+ timeout=30.0,
182
+ use_login_shell=use_login_shell
183
+ )
184
+
185
+ # Report result
186
+ if result.is_success:
187
+ await tool_ctx.info("Script executed successfully")
188
+ else:
189
+ await tool_ctx.error(f"Script execution failed with exit code {result.return_code}")
190
+
191
+ # Format the result
192
+ if result.is_success:
193
+ # For successful scripts, just return stdout unless stderr has content
194
+ if result.stderr:
195
+ return f"Script executed successfully.\n\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
196
+ return result.stdout
197
+ else:
198
+ # For failed scripts, include all available information
199
+ return result.format_output()
200
+
201
+ @override
202
+ def register(self, mcp_server: FastMCP) -> None:
203
+ """Register this run script tool with the MCP server.
204
+
205
+ Creates a wrapper function with explicitly defined parameters that match
206
+ the tool's parameter schema and registers it with the MCP server.
207
+
208
+ Args:
209
+ mcp_server: The FastMCP server instance
210
+ """
211
+ tool_self = self # Create a reference to self for use in the closure
212
+
213
+ @mcp_server.tool(name=self.name, description=self.mcp_description)
214
+ async def run_script(ctx: MCPContext, script: str, cwd: str, interpreter: str = "bash", use_login_shell: bool = True) -> str:
215
+ return await tool_self.call(ctx, script=script, cwd=cwd, interpreter=interpreter, use_login_shell=use_login_shell)
@@ -0,0 +1,244 @@
1
+ """Script tool implementation.
2
+
3
+ This module provides the ScriptTool for executing scripts in various languages.
4
+ """
5
+
6
+ import os
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.common.context import create_tool_context
13
+ from hanzo_mcp.tools.shell.base import ShellBaseTool
14
+ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
15
+
16
+
17
+ @final
18
+ class ScriptTool(ShellBaseTool):
19
+ """Tool for executing scripts in various languages."""
20
+
21
+ def __init__(self, permission_manager: Any, command_executor: CommandExecutor) -> None:
22
+ """Initialize the script tool.
23
+
24
+ Args:
25
+ permission_manager: Permission manager for access control
26
+ command_executor: Command executor for running scripts
27
+ """
28
+ super().__init__(permission_manager)
29
+ self.command_executor: CommandExecutor = command_executor
30
+
31
+ @property
32
+ @override
33
+ def name(self) -> str:
34
+ """Get the tool name.
35
+
36
+ Returns:
37
+ Tool name
38
+ """
39
+ return "script_tool"
40
+
41
+ @property
42
+ @override
43
+ def description(self) -> str:
44
+ """Get the tool description.
45
+
46
+ Returns:
47
+ Tool description
48
+ """
49
+ return """Execute a script in the specified language.
50
+
51
+ Args:
52
+ language: The programming language (python, javascript, etc.)
53
+ script: The script code to execute
54
+ cwd: Working directory for script execution. MUST be a subdirectory of one of the allowed paths, not a parent directory. Specify the most specific path possible.
55
+
56
+ args: Optional command-line arguments
57
+ use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
58
+
59
+ Returns:
60
+ Script execution results
61
+ """
62
+
63
+ @property
64
+ @override
65
+ def parameters(self) -> dict[str, Any]:
66
+ """Get the parameter specifications for the tool.
67
+
68
+ Returns:
69
+ Parameter specifications
70
+ """
71
+ return {
72
+ "properties": {
73
+ "language": {
74
+ "title": "Language",
75
+ "type": "string"
76
+ },
77
+ "script": {
78
+ "title": "Script",
79
+ "type": "string"
80
+ },
81
+ "cwd": {
82
+ "title": "Cwd",
83
+ "type": "string"
84
+ },
85
+ "args": {
86
+ "anyOf": [
87
+ {"items": {"type": "string"}, "type": "array"},
88
+ {"type": "null"}
89
+ ],
90
+ "default": None,
91
+ "title": "Args"
92
+ },
93
+ "use_login_shell": {
94
+ "default": True,
95
+ "title": "Use Login Shell",
96
+ "type": "boolean"
97
+ }
98
+ },
99
+ "required": ["language", "script", "cwd"],
100
+ "title": "script_toolArguments",
101
+ "type": "object"
102
+ }
103
+
104
+ @property
105
+ @override
106
+ def required(self) -> list[str]:
107
+ """Get the list of required parameter names.
108
+
109
+ Returns:
110
+ List of required parameter names
111
+ """
112
+ return ["language", "script", "cwd"]
113
+
114
+ @override
115
+ async def prepare_tool_context(self, ctx: MCPContext) -> Any:
116
+ """Create and prepare the tool context.
117
+
118
+ Args:
119
+ ctx: MCP context
120
+
121
+ Returns:
122
+ Prepared tool context
123
+ """
124
+ tool_ctx = create_tool_context(ctx)
125
+ tool_ctx.set_tool_info(self.name)
126
+ return tool_ctx
127
+
128
+ @override
129
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
130
+ """Execute the tool with the given parameters.
131
+
132
+ Args:
133
+ ctx: MCP context
134
+ **params: Tool parameters
135
+
136
+ Returns:
137
+ Tool result
138
+ """
139
+ tool_ctx = await self.prepare_tool_context(ctx)
140
+
141
+ # Extract parameters
142
+ language = params.get("language")
143
+ script = params.get("script")
144
+ cwd = params.get("cwd")
145
+ args = params.get("args")
146
+ use_login_shell = params.get("use_login_shell", True)
147
+
148
+ # Validate required parameters
149
+ if not language:
150
+ await tool_ctx.error("Parameter 'language' is required but was None")
151
+ return "Error: Parameter 'language' is required but was None"
152
+
153
+ if language.strip() == "":
154
+ await tool_ctx.error("Parameter 'language' cannot be empty")
155
+ return "Error: Parameter 'language' cannot be empty"
156
+
157
+ if not script:
158
+ await tool_ctx.error("Parameter 'script' is required but was None")
159
+ return "Error: Parameter 'script' is required but was None"
160
+
161
+ if script.strip() == "":
162
+ await tool_ctx.error("Parameter 'script' cannot be empty")
163
+ return "Error: Parameter 'script' cannot be empty"
164
+
165
+ # args can be None as it's optional
166
+ # Check for empty list but still allow None
167
+ if args is not None and len(args) == 0:
168
+ await tool_ctx.warning("Parameter 'args' is an empty list")
169
+ # We don't return error for this as empty args is acceptable
170
+
171
+ # Validate required cwd parameter
172
+ if not cwd:
173
+ await tool_ctx.error("Parameter 'cwd' is required but was None")
174
+ return "Error: Parameter 'cwd' is required but was None"
175
+
176
+ if cwd.strip() == "":
177
+ await tool_ctx.error("Parameter 'cwd' cannot be empty")
178
+ return "Error: Parameter 'cwd' cannot be empty"
179
+
180
+ await tool_ctx.info(f"Executing {language} script")
181
+
182
+ # Check if the language is supported
183
+ if language not in self.command_executor.get_available_languages():
184
+ await tool_ctx.error(f"Unsupported language: {language}")
185
+ return f"Error: Unsupported language: {language}. Supported languages: {', '.join(self.command_executor.get_available_languages())}"
186
+
187
+ # Check if working directory is allowed
188
+ if not self.is_path_allowed(cwd):
189
+ await tool_ctx.error(f"Working directory not allowed: {cwd}")
190
+ return f"Error: Working directory not allowed: {cwd}"
191
+
192
+ # Check if working directory exists
193
+ if not os.path.isdir(cwd):
194
+ await tool_ctx.error(f"Working directory does not exist: {cwd}")
195
+ return f"Error: Working directory does not exist: {cwd}"
196
+
197
+ # Execute the script
198
+ result = await self.command_executor.execute_script_from_file(
199
+ script=script,
200
+ language=language,
201
+ cwd=cwd,
202
+ timeout=30.0,
203
+ args=args,
204
+ use_login_shell=use_login_shell
205
+ )
206
+
207
+ # Report result
208
+ if result.is_success:
209
+ await tool_ctx.info(f"{language} script executed successfully")
210
+ else:
211
+ await tool_ctx.error(
212
+ f"{language} script execution failed with exit code {result.return_code}"
213
+ )
214
+
215
+ # Format the result
216
+ if result.is_success:
217
+ # Format the successful result
218
+ output = f"{language} script executed successfully.\n\n"
219
+ if result.stdout:
220
+ output += f"STDOUT:\n{result.stdout}\n\n"
221
+ if result.stderr:
222
+ output += f"STDERR:\n{result.stderr}"
223
+ return output.strip()
224
+ else:
225
+ # For failed scripts, include all available information
226
+ return result.format_output()
227
+
228
+ @override
229
+ def register(self, mcp_server: FastMCP) -> None:
230
+ """Register this script tool with the MCP server.
231
+
232
+ Creates a wrapper function with explicitly defined parameters that match
233
+ the tool's parameter schema and registers it with the MCP server.
234
+
235
+ Args:
236
+ mcp_server: The FastMCP server instance
237
+ """
238
+ tool_self = self # Create a reference to self for use in the closure
239
+
240
+ @mcp_server.tool(name=self.name, description=self.mcp_description)
241
+ async def script_tool(ctx: MCPContext, language: str, script: str, cwd: str, args: list[str] | None = None,
242
+ use_login_shell: bool = True) -> str:
243
+ return await tool_self.call(ctx, language=language, script=script, cwd=cwd,
244
+ args=args, use_login_shell=use_login_shell)