Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ lint:

typecheck:
uv sync --group dev --extra test
uv run mypy src/mcp_shell_server tests
uv run mypy --install-types --non-interactive src/mcp_shell_server tests

coverage:
uv run pytest --cov=src/mcp_shell_server --cov-report=xml --cov-report=term-missing tests
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,17 @@ To install Shell Server for Claude Desktop automatically via [Smithery](https://
npx -y @smithery/cli install mcp-shell-server --client claude
```

## Usage
### Configuring Regex Patterns

You can allow commands using regex patterns by setting the `ALLOW_PATTERNS` environment variable. Patterns should be separated by commas.

Example:

```bash
ALLOW_PATTERNS="^cmd[0-9]+$,^test.*$"
```

This configuration allows commands like `cmd123` and `testCommand`.

### Starting the Server

Expand Down
28 changes: 21 additions & 7 deletions src/mcp_shell_server/command_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os
import re
from typing import Dict, List


Expand All @@ -15,7 +16,8 @@ def __init__(self):
"""
Initialize the validator.
"""
pass
# No state; environment variables are read on demand
return None

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

def _get_allowed_patterns(self) -> List[re.Pattern]:
"""Get the list of allowed regex patterns from environment variables"""
allow_patterns = os.environ.get("ALLOW_PATTERNS", "")
patterns = [
pattern.strip() for pattern in allow_patterns.split(",") if pattern.strip()
]
return [re.compile(pattern) for pattern in patterns]

def get_allowed_commands(self) -> list[str]:
"""Get the list of allowed commands from environment variables"""
"""Public API: return list form of allowed commands"""
return list(self._get_allowed_commands())

def is_command_allowed(self, command: str) -> bool:
"""Check if a command is in the allowed list"""
"""Check if a command is in the allowed list or matches an allowed pattern"""
cmd = command.strip()
return cmd in self._get_allowed_commands()
if cmd in self._get_allowed_commands():
return True
for pattern in self._get_allowed_patterns():
if pattern.match(cmd):
return True
return False

def validate_no_shell_operators(self, cmd: str) -> None:
"""
Expand Down Expand Up @@ -92,13 +107,12 @@ def validate_command(self, command: List[str]) -> None:
if not command:
raise ValueError("Empty command")

allowed_commands = self._get_allowed_commands()
if not allowed_commands:
if not self._get_allowed_commands() and not self._get_allowed_patterns():
raise ValueError(
"No commands are allowed. Please set ALLOW_COMMANDS environment variable."
)

# Clean and check the first command
cleaned_cmd = command[0].strip()
if cleaned_cmd not in allowed_commands:
if not self.is_command_allowed(cleaned_cmd):
raise ValueError(f"Command not allowed: {cleaned_cmd}")
12 changes: 11 additions & 1 deletion src/mcp_shell_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,21 @@ def get_allowed_commands(self) -> list[str]:
"""Get the allowed commands"""
return self.executor.validator.get_allowed_commands()

def get_allowed_patterns(self) -> list[str]:
"""Get the allowed regex patterns"""
return [
pattern.pattern
for pattern in self.executor.validator._get_allowed_patterns()
]

def get_tool_description(self) -> Tool:
"""Get the tool description for the execute command"""
allowed_commands = ", ".join(self.get_allowed_commands())
allowed_patterns = ", ".join(self.get_allowed_patterns())
"""Get the tool description for the execute command"""
return Tool(
name=self.name,
description=f"{self.description}\nAllowed commands: {', '.join(self.get_allowed_commands())}",
description=f"{self.description}\nAllowed commands: {allowed_commands}\nAllowed patterns: {allowed_patterns}",
inputSchema={
"type": "object",
"properties": {
Expand Down
10 changes: 9 additions & 1 deletion tests/test_command_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ def test_get_allowed_commands(validator, monkeypatch):
assert set(validator.get_allowed_commands()) == {"cmd1", "cmd2", "cmd3", "cmd4"}


def test_is_command_allowed(validator, monkeypatch):
def test_is_command_allowed_with_patterns(validator, monkeypatch):
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "allowed_cmd")
monkeypatch.setenv("ALLOW_PATTERNS", "^cmd[0-9]+$")

assert validator.is_command_allowed("allowed_cmd")
assert validator.is_command_allowed("cmd123")
assert not validator.is_command_allowed("disallowed_cmd")
assert not validator.is_command_allowed("cmdabc")
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "allowed_cmd")
assert validator.is_command_allowed("allowed_cmd")
Expand Down