Skip to content

Commit 64fb33e

Browse files
committed
Merge branch 'pr/merge-allow-regex-final' into develop
2 parents 6168041 + 3ffe31f commit 64fb33e

File tree

4 files changed

+54
-10
lines changed

4 files changed

+54
-10
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/tumf-mcp-shell-server-badge.png)](https://mseep.ai/app/tumf-mcp-shell-server)
77

8-
98
A secure shell command execution server implementing the Model Context Protocol (MCP). This server allows remote execution of whitelisted shell commands with support for stdin input.
109

1110
<a href="https://glama.ai/mcp/servers/rt2d4pbn22"><img width="380" height="200" src="https://glama.ai/mcp/servers/rt2d4pbn22/badge" alt="mcp-shell-server MCP server" /></a>
1211

12+
<a href="https://glama.ai/mcp/servers/rt2d4pbn22"><img width="380" height="200" src="https://glama.ai/mcp/servers/rt2d4pbn22/badge" alt="mcp-shell-server MCP server" /></a>
13+
1314
## Features
1415

1516
* **Secure Command Execution**: Only whitelisted commands can be executed
@@ -80,6 +81,17 @@ npx -y @smithery/cli install mcp-shell-server --client claude
8081
```
8182

8283
### Manual Installation
84+
85+
### Installing via Smithery
86+
87+
To install Shell Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-shell-server):
88+
89+
```bash
90+
npx -y @smithery/cli install mcp-shell-server --client claude
91+
```
92+
93+
### Manual Installation
94+
8395
```bash
8496
pip install mcp-shell-server
8597
```

src/mcp_shell_server/command_validator.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import os
6+
import re
67
from typing import Dict, List
78

89

@@ -15,7 +16,8 @@ def __init__(self):
1516
"""
1617
Initialize the validator.
1718
"""
18-
pass
19+
# No state; environment variables are read on demand
20+
return None
1921

2022
def _get_allowed_commands(self) -> set[str]:
2123
"""Get the set of allowed commands from environment variables"""
@@ -24,14 +26,27 @@ def _get_allowed_commands(self) -> set[str]:
2426
commands = allow_commands + "," + allowed_commands
2527
return {cmd.strip() for cmd in commands.split(",") if cmd.strip()}
2628

29+
def _get_allowed_patterns(self) -> List[re.Pattern]:
30+
"""Get the list of allowed regex patterns from environment variables"""
31+
allow_patterns = os.environ.get("ALLOW_PATTERNS", "")
32+
patterns = [
33+
pattern.strip() for pattern in allow_patterns.split(",") if pattern.strip()
34+
]
35+
return [re.compile(pattern) for pattern in patterns]
36+
2737
def get_allowed_commands(self) -> list[str]:
28-
"""Get the list of allowed commands from environment variables"""
38+
"""Public API: return list form of allowed commands"""
2939
return list(self._get_allowed_commands())
3040

3141
def is_command_allowed(self, command: str) -> bool:
32-
"""Check if a command is in the allowed list"""
42+
"""Check if a command is in the allowed list or matches an allowed pattern"""
3343
cmd = command.strip()
34-
return cmd in self._get_allowed_commands()
44+
if cmd in self._get_allowed_commands():
45+
return True
46+
for pattern in self._get_allowed_patterns():
47+
if pattern.match(cmd):
48+
return True
49+
return False
3550

3651
def validate_no_shell_operators(self, cmd: str) -> None:
3752
"""
@@ -92,13 +107,12 @@ def validate_command(self, command: List[str]) -> None:
92107
if not command:
93108
raise ValueError("Empty command")
94109

95-
allowed_commands = self._get_allowed_commands()
96-
if not allowed_commands:
110+
if not self._get_allowed_commands() and not self._get_allowed_patterns():
97111
raise ValueError(
98112
"No commands are allowed. Please set ALLOW_COMMANDS environment variable."
99113
)
100114

101115
# Clean and check the first command
102116
cleaned_cmd = command[0].strip()
103-
if cleaned_cmd not in allowed_commands:
117+
if not self.is_command_allowed(cleaned_cmd):
104118
raise ValueError(f"Command not allowed: {cleaned_cmd}")

src/mcp_shell_server/server.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,21 @@ def get_allowed_commands(self) -> list[str]:
3131
"""Get the allowed commands"""
3232
return self.executor.validator.get_allowed_commands()
3333

34+
def get_allowed_patterns(self) -> list[str]:
35+
"""Get the allowed regex patterns"""
36+
return [
37+
pattern.pattern
38+
for pattern in self.executor.validator._get_allowed_patterns()
39+
]
40+
3441
def get_tool_description(self) -> Tool:
42+
"""Get the tool description for the execute command"""
43+
allowed_commands = ", ".join(self.get_allowed_commands())
44+
allowed_patterns = ", ".join(self.get_allowed_patterns())
3545
"""Get the tool description for the execute command"""
3646
return Tool(
3747
name=self.name,
38-
description=f"{self.description}\nAllowed commands: {', '.join(self.get_allowed_commands())}",
48+
description=f"{self.description}\nAllowed commands: {allowed_commands}\nAllowed patterns: {allowed_patterns}",
3949
inputSchema={
4050
"type": "object",
4151
"properties": {

tests/test_command_validator.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@ def test_get_allowed_commands(validator, monkeypatch):
2222
assert set(validator.get_allowed_commands()) == {"cmd1", "cmd2", "cmd3", "cmd4"}
2323

2424

25-
def test_is_command_allowed(validator, monkeypatch):
25+
def test_is_command_allowed_with_patterns(validator, monkeypatch):
26+
clear_env(monkeypatch)
27+
monkeypatch.setenv("ALLOW_COMMANDS", "allowed_cmd")
28+
monkeypatch.setenv("ALLOW_PATTERNS", "^cmd[0-9]+$")
29+
30+
assert validator.is_command_allowed("allowed_cmd")
31+
assert validator.is_command_allowed("cmd123")
32+
assert not validator.is_command_allowed("disallowed_cmd")
33+
assert not validator.is_command_allowed("cmdabc")
2634
clear_env(monkeypatch)
2735
monkeypatch.setenv("ALLOW_COMMANDS", "allowed_cmd")
2836
assert validator.is_command_allowed("allowed_cmd")

0 commit comments

Comments
 (0)