Skip to content

Commit 66c77cf

Browse files
committed
style: format code for improved readability and consistency across files
refactor: clean up whitespace and comments in mcp_shell_server module for better maintainability test: simplify test cases in test_server.py and test_shell_executor.py for clarity and conciseness
1 parent ba25a94 commit 66c77cf

File tree

6 files changed

+52
-61
lines changed

6 files changed

+52
-61
lines changed

mcp_shell_server/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""MCP Shell Server Package."""
2+
23
from .server import main
34

45
__version__ = "0.1.0"
5-
__all__ = ["main"]
6+
__all__ = ["main"]

mcp_shell_server/server.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212

1313
app = Server("mcp-shell-server")
1414

15+
1516
class ExecuteToolHandler:
1617
"""Handler for shell command execution"""
18+
1719
name = "execute"
1820
description = "Execute a shell command"
1921

@@ -31,51 +33,46 @@ def get_tool_description(self) -> Tool:
3133
"command": {
3234
"type": "array",
3335
"items": {"type": "string"},
34-
"description": "Command and its arguments as array"
36+
"description": "Command and its arguments as array",
3537
},
3638
"stdin": {
3739
"type": "string",
38-
"description": "Input to be passed to the command via stdin"
39-
}
40+
"description": "Input to be passed to the command via stdin",
41+
},
4042
},
41-
"required": ["command"]
42-
}
43+
"required": ["command"],
44+
},
4345
)
4446

4547
async def run_tool(self, arguments: dict) -> Sequence[TextContent]:
4648
"""Execute the shell command with the given arguments"""
4749
command = arguments.get("command", [])
4850
stdin = arguments.get("stdin")
49-
51+
5052
if not command:
5153
raise ValueError("No command provided")
52-
54+
5355
result = await self.executor.execute(command, stdin)
54-
56+
5557
# Raise error if command execution failed
5658
if result.get("error"):
5759
raise RuntimeError(result["error"])
5860

5961
# Convert executor result to TextContent sequence
6062
content: list[TextContent] = []
61-
63+
6264
if result.get("stdout"):
63-
content.append(TextContent(
64-
type="text",
65-
text=result["stdout"]
66-
))
65+
content.append(TextContent(type="text", text=result["stdout"]))
6766
if result.get("stderr"):
68-
content.append(TextContent(
69-
type="text",
70-
text=result["stderr"]
71-
))
72-
67+
content.append(TextContent(type="text", text=result["stderr"]))
68+
7369
return content
7470

7571

7672
# Initialize tool handlers
7773
tool_handler = ExecuteToolHandler()
7874

75+
7976
@app.list_tools()
8077
async def list_tools() -> list[Tool]:
8178
"""List available tools."""
@@ -107,10 +104,8 @@ async def main() -> None:
107104

108105
async with stdio_server() as (read_stream, write_stream):
109106
await app.run(
110-
read_stream,
111-
write_stream,
112-
app.create_initialization_options()
107+
read_stream, write_stream, app.create_initialization_options()
113108
)
114109
except Exception as e:
115110
logger.error(f"Server error: {str(e)}")
116-
raise
111+
raise

mcp_shell_server/shell_executor.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import os
22
import time
33
import asyncio
4-
import shlex
54
from typing import Dict, List, Optional, Any
65

6+
77
class ShellExecutor:
88
"""
99
Executes shell commands in a secure manner by validating against a whitelist.
1010
"""
11-
11+
1212
def __init__(self):
1313
"""
1414
Initialize the executor. The allowed commands are read from ALLOW_COMMANDS
@@ -19,7 +19,7 @@ def __init__(self):
1919
def _get_allowed_commands(self) -> set:
2020
"""
2121
Get the set of allowed commands from environment variable.
22-
22+
2323
Returns:
2424
set: Set of allowed command names
2525
"""
@@ -29,10 +29,10 @@ def _get_allowed_commands(self) -> set:
2929
def _clean_command(self, command: List[str]) -> List[str]:
3030
"""
3131
Clean command by trimming whitespace from each part.
32-
32+
3333
Args:
3434
command (List[str]): Original command and its arguments
35-
35+
3636
Returns:
3737
List[str]: Cleaned command with trimmed whitespace
3838
"""
@@ -41,10 +41,10 @@ def _clean_command(self, command: List[str]) -> List[str]:
4141
def _validate_command(self, command: List[str]) -> None:
4242
"""
4343
Validate if the command is allowed to be executed.
44-
44+
4545
Args:
4646
command (List[str]): Command and its arguments
47-
47+
4848
Raises:
4949
ValueError: If the command is empty, not allowed, or contains invalid shell operators
5050
"""
@@ -53,7 +53,9 @@ def _validate_command(self, command: List[str]) -> None:
5353

5454
allowed_commands = self._get_allowed_commands()
5555
if not allowed_commands:
56-
raise ValueError("No commands are allowed. Please set ALLOW_COMMANDS environment variable.")
56+
raise ValueError(
57+
"No commands are allowed. Please set ALLOW_COMMANDS environment variable."
58+
)
5759

5860
# Clean and check the first command
5961
cleaned_cmd = command[0].strip()
@@ -68,16 +70,20 @@ def _validate_command(self, command: List[str]) -> None:
6870
raise ValueError(f"Unexpected shell operator: {cleaned_arg}")
6971
next_cmd = command[i + 1].strip()
7072
if next_cmd not in allowed_commands:
71-
raise ValueError(f"Command not allowed after {cleaned_arg}: {next_cmd}")
73+
raise ValueError(
74+
f"Command not allowed after {cleaned_arg}: {next_cmd}"
75+
)
7276

73-
async def execute(self, command: List[str], stdin: Optional[str] = None) -> Dict[str, Any]:
77+
async def execute(
78+
self, command: List[str], stdin: Optional[str] = None
79+
) -> Dict[str, Any]:
7480
"""
7581
Execute a shell command with optional stdin input.
76-
82+
7783
Args:
7884
command (List[str]): Command and its arguments
7985
stdin (Optional[str]): Input to be passed to the command via stdin
80-
86+
8187
Returns:
8288
Dict[str, Any]: Execution result containing stdout, stderr, status code, and execution time.
8389
If error occurs, result contains additional 'error' field.
@@ -89,15 +95,15 @@ async def execute(self, command: List[str], stdin: Optional[str] = None) -> Dict
8995
cleaned_command = self._clean_command(command)
9096
if not cleaned_command:
9197
raise ValueError("Empty command")
92-
98+
9399
self._validate_command(cleaned_command)
94100
except ValueError as e:
95101
return {
96102
"error": str(e),
97103
"status": 1,
98104
"stdout": "",
99105
"stderr": str(e),
100-
"execution_time": time.time() - start_time
106+
"execution_time": time.time() - start_time,
101107
}
102108

103109
try:
@@ -107,7 +113,7 @@ async def execute(self, command: List[str], stdin: Optional[str] = None) -> Dict
107113
stdin=asyncio.subprocess.PIPE if stdin else None,
108114
stdout=asyncio.subprocess.PIPE,
109115
stderr=asyncio.subprocess.PIPE,
110-
env={"PATH": os.environ.get("PATH", "")}
116+
env={"PATH": os.environ.get("PATH", "")},
111117
)
112118

113119
stdin_bytes = stdin.encode() if stdin else None
@@ -118,21 +124,21 @@ async def execute(self, command: List[str], stdin: Optional[str] = None) -> Dict
118124
"stdout": stdout.decode() if stdout else "",
119125
"stderr": stderr.decode() if stderr else "",
120126
"status": process.returncode,
121-
"execution_time": time.time() - start_time
127+
"execution_time": time.time() - start_time,
122128
}
123129
except FileNotFoundError:
124130
return {
125131
"error": f"Command not found: {cleaned_command[0]}",
126132
"status": 1,
127133
"stdout": "",
128134
"stderr": f"Command not found: {cleaned_command[0]}",
129-
"execution_time": time.time() - start_time
135+
"execution_time": time.time() - start_time,
130136
}
131137
except Exception as e:
132138
return {
133139
"error": str(e),
134140
"status": 1,
135141
"stdout": "",
136142
"stderr": str(e),
137-
"execution_time": time.time() - start_time
138-
}
143+
"execution_time": time.time() - start_time,
144+
}

tests/conftest.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import pytest
2-
31
# Configure pytest-asyncio
42
def pytest_configure(config):
53
"""Configure pytest-asyncio defaults"""
6-
config.option.asyncio_mode = "strict"
4+
config.option.asyncio_mode = "strict"

tests/test_server.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
from mcp.types import TextContent, Tool
3-
from mcp_shell_server.server import call_tool, list_tools, tool_handler
3+
from mcp_shell_server.server import call_tool, list_tools
44

55

66
@pytest.mark.asyncio
@@ -22,9 +22,7 @@ async def test_list_tools():
2222
async def test_call_tool_valid_command(monkeypatch):
2323
"""Test execution of a valid command"""
2424
monkeypatch.setenv("ALLOW_COMMANDS", "echo")
25-
result = await call_tool("execute", {
26-
"command": ["echo", "hello world"]
27-
})
25+
result = await call_tool("execute", {"command": ["echo", "hello world"]})
2826
assert len(result) == 1
2927
assert isinstance(result[0], TextContent)
3028
assert result[0].type == "text"
@@ -35,10 +33,7 @@ async def test_call_tool_valid_command(monkeypatch):
3533
async def test_call_tool_with_stdin(monkeypatch):
3634
"""Test command execution with stdin"""
3735
monkeypatch.setenv("ALLOW_COMMANDS", "cat")
38-
result = await call_tool("execute", {
39-
"command": ["cat"],
40-
"stdin": "test input"
41-
})
36+
result = await call_tool("execute", {"command": ["cat"], "stdin": "test input"})
4237
assert len(result) == 1
4338
assert isinstance(result[0], TextContent)
4439
assert result[0].type == "text"
@@ -50,9 +45,7 @@ async def test_call_tool_invalid_command(monkeypatch):
5045
"""Test execution of an invalid command"""
5146
monkeypatch.setenv("ALLOW_COMMANDS", "echo")
5247
with pytest.raises(RuntimeError) as excinfo:
53-
await call_tool("execute", {
54-
"command": ["invalid_command"]
55-
})
48+
await call_tool("execute", {"command": ["invalid_command"]})
5649
assert "Command not allowed: invalid_command" in str(excinfo.value)
5750

5851

@@ -76,7 +69,5 @@ async def test_call_tool_invalid_arguments():
7669
async def test_call_tool_empty_command():
7770
"""Test execution with empty command"""
7871
with pytest.raises(RuntimeError) as excinfo:
79-
await call_tool("execute", {
80-
"command": []
81-
})
82-
assert "No command provided" in str(excinfo.value)
72+
await call_tool("execute", {"command": []})
73+
assert "No command provided" in str(excinfo.value)

tests/test_shell_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async def test_stdin_input(executor, monkeypatch):
2727
async def test_command_with_space_allowed(executor, monkeypatch):
2828
monkeypatch.setenv("ALLOW_COMMANDS", "cat")
2929
result = await executor.execute(["cat "], stdin="hello world")
30-
assert result["error"] == None
30+
assert result["error"] is None
3131
assert result["stdout"].strip() == "hello world"
3232
assert result["status"] == 0
3333

0 commit comments

Comments
 (0)