Skip to content

Commit 7193838

Browse files
committed
feat(server.py): refactor ShellServer to ExecuteToolHandler for better structure and clarity
feat(server.py): implement logging for better error tracking and debugging feat(server.py): add tool description and input schema for command execution fix(server.py): improve error handling for command execution and argument validation
1 parent 0125f2c commit 7193838

File tree

1 file changed

+108
-29
lines changed

1 file changed

+108
-29
lines changed

mcp_shell_server/server.py

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,117 @@
1-
import asyncio
2-
from mcp.server.stdio import stdio_server
1+
import logging
2+
from collections.abc import Sequence
3+
from typing import Any
4+
import traceback
5+
from mcp.server import Server
6+
from mcp.types import Tool, TextContent
37
from .shell_executor import ShellExecutor
48

5-
class ShellServer:
6-
"""
7-
MCP server that executes shell commands in a secure manner.
8-
Only commands listed in the ALLOW_COMMANDS environment variable can be executed.
9-
"""
10-
9+
# Configure logging
10+
logging.basicConfig(level=logging.INFO)
11+
logger = logging.getLogger("mcp-shell-server")
12+
13+
app = Server("mcp-shell-server")
14+
15+
class ExecuteToolHandler:
16+
"""Handler for shell command execution"""
17+
name = "execute"
18+
description = "Execute a shell command"
19+
1120
def __init__(self):
1221
self.executor = ShellExecutor()
1322

14-
async def handle(self, args: dict) -> dict:
15-
"""
16-
Handle incoming MCP requests to execute shell commands.
17-
18-
Args:
19-
args (dict): Arguments containing the command to execute and optional stdin
20-
21-
Returns:
22-
dict: Execution results including stdout, stderr, status code, and execution time
23-
"""
24-
command = args.get("command", [])
25-
stdin = args.get("stdin")
23+
def get_tool_description(self) -> Tool:
24+
"""Get the tool description for the execute command"""
25+
return Tool(
26+
name=self.name,
27+
description=self.description,
28+
inputSchema={
29+
"type": "object",
30+
"properties": {
31+
"command": {
32+
"type": "array",
33+
"items": {"type": "string"},
34+
"description": "Command and its arguments as array"
35+
},
36+
"stdin": {
37+
"type": "string",
38+
"description": "Input to be passed to the command via stdin"
39+
}
40+
},
41+
"required": ["command"]
42+
}
43+
)
44+
45+
async def run_tool(self, arguments: dict) -> Sequence[TextContent]:
46+
"""Execute the shell command with the given arguments"""
47+
command = arguments.get("command", [])
48+
stdin = arguments.get("stdin")
2649

2750
if not command:
28-
return {
29-
"error": "No command provided",
30-
"status": 1
31-
}
51+
raise ValueError("No command provided")
52+
53+
result = await self.executor.execute(command, stdin)
3254

33-
return await self.executor.execute(command, stdin)
55+
# Convert executor result to TextContent sequence
56+
content: list[TextContent] = []
57+
58+
if result.get("error"):
59+
content.append(TextContent(
60+
type="text",
61+
text=result["error"]
62+
))
63+
if result.get("stdout"):
64+
content.append(TextContent(
65+
type="text",
66+
text=result["stdout"]
67+
))
68+
if result.get("stderr"):
69+
content.append(TextContent(
70+
type="text",
71+
text=result["stderr"]
72+
))
73+
74+
return content
75+
76+
77+
# Initialize tool handlers
78+
tool_handler = ExecuteToolHandler()
79+
80+
@app.list_tools()
81+
async def list_tools() -> list[Tool]:
82+
"""List available tools."""
83+
return [tool_handler.get_tool_description()]
84+
85+
86+
@app.call_tool()
87+
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
88+
"""Handle tool calls"""
89+
try:
90+
if name != tool_handler.name:
91+
raise ValueError(f"Unknown tool: {name}")
92+
93+
if not isinstance(arguments, dict):
94+
raise ValueError("Arguments must be a dictionary")
95+
96+
return await tool_handler.run_tool(arguments)
97+
98+
except Exception as e:
99+
logger.error(traceback.format_exc())
100+
logger.error(f"Error during call_tool: {str(e)}")
101+
raise RuntimeError(f"Error executing command: {str(e)}")
102+
103+
104+
async def main() -> None:
105+
"""Main entry point for the MCP shell server"""
106+
try:
107+
from mcp.server.stdio import stdio_server
34108

35-
def main():
36-
"""Entry point for the MCP shell server"""
37-
server = ShellServer()
38-
stdio_server(server.handle)
109+
async with stdio_server() as (read_stream, write_stream):
110+
await app.run(
111+
read_stream,
112+
write_stream,
113+
app.create_initialization_options()
114+
)
115+
except Exception as e:
116+
logger.error(f"Server error: {str(e)}")
117+
raise

0 commit comments

Comments
 (0)