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
3
7
from .shell_executor import ShellExecutor
4
8
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
+
11
20
def __init__ (self ):
12
21
self .executor = ShellExecutor ()
13
22
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" )
26
49
27
50
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 )
32
54
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
34
108
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