mcp-shell-server 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_shell_server/__init__.py +15 -0
- mcp_shell_server/server.py +123 -0
- mcp_shell_server/shell_executor.py +175 -0
- mcp_shell_server-0.1.0.dist-info/METADATA +180 -0
- mcp_shell_server-0.1.0.dist-info/RECORD +7 -0
- mcp_shell_server-0.1.0.dist-info/WHEEL +4 -0
- mcp_shell_server-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""MCP Shell Server Package."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from . import server
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
"""Main entry point for the package."""
|
|
10
|
+
asyncio.run(server.main())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Optionally expose other important items at package level
|
|
14
|
+
__all__ = ["main", "server"]
|
|
15
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import traceback
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from mcp.server import Server
|
|
7
|
+
from mcp.types import TextContent, Tool
|
|
8
|
+
|
|
9
|
+
from .shell_executor import ShellExecutor
|
|
10
|
+
|
|
11
|
+
# Configure logging
|
|
12
|
+
logging.basicConfig(level=logging.INFO)
|
|
13
|
+
logger = logging.getLogger("mcp-shell-server")
|
|
14
|
+
|
|
15
|
+
app = Server("mcp-shell-server")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExecuteToolHandler:
|
|
19
|
+
"""Handler for shell command execution"""
|
|
20
|
+
|
|
21
|
+
name = "shell_execute"
|
|
22
|
+
description = "Execute a shell command"
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.executor = ShellExecutor()
|
|
26
|
+
|
|
27
|
+
def get_allowed_commands(self) -> list[str]:
|
|
28
|
+
"""Get the allowed commands"""
|
|
29
|
+
return self.executor.get_allowed_commands()
|
|
30
|
+
|
|
31
|
+
def get_tool_description(self) -> Tool:
|
|
32
|
+
"""Get the tool description for the execute command"""
|
|
33
|
+
return Tool(
|
|
34
|
+
name=self.name,
|
|
35
|
+
description=f"{self.description}\nAllowed commands: {', '.join(self.get_allowed_commands())}",
|
|
36
|
+
inputSchema={
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"command": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {"type": "string"},
|
|
42
|
+
"description": "Command and its arguments as array",
|
|
43
|
+
},
|
|
44
|
+
"stdin": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Input to be passed to the command via stdin",
|
|
47
|
+
},
|
|
48
|
+
"directory": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Working directory where the command will be executed",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"required": ["command"],
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def run_tool(self, arguments: dict) -> Sequence[TextContent]:
|
|
58
|
+
"""Execute the shell command with the given arguments"""
|
|
59
|
+
command = arguments.get("command", [])
|
|
60
|
+
stdin = arguments.get("stdin")
|
|
61
|
+
directory = arguments.get("directory")
|
|
62
|
+
|
|
63
|
+
if not command:
|
|
64
|
+
raise ValueError("No command provided")
|
|
65
|
+
|
|
66
|
+
result = await self.executor.execute(command, stdin, directory)
|
|
67
|
+
|
|
68
|
+
# Raise error if command execution failed
|
|
69
|
+
if result.get("error"):
|
|
70
|
+
raise RuntimeError(result["error"])
|
|
71
|
+
|
|
72
|
+
# Convert executor result to TextContent sequence
|
|
73
|
+
content: list[TextContent] = []
|
|
74
|
+
|
|
75
|
+
if result.get("stdout"):
|
|
76
|
+
content.append(TextContent(type="text", text=result["stdout"]))
|
|
77
|
+
if result.get("stderr"):
|
|
78
|
+
content.append(TextContent(type="text", text=result["stderr"]))
|
|
79
|
+
|
|
80
|
+
return content
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Initialize tool handlers
|
|
84
|
+
tool_handler = ExecuteToolHandler()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.list_tools()
|
|
88
|
+
async def list_tools() -> list[Tool]:
|
|
89
|
+
"""List available tools."""
|
|
90
|
+
return [tool_handler.get_tool_description()]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.call_tool()
|
|
94
|
+
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
|
|
95
|
+
"""Handle tool calls"""
|
|
96
|
+
try:
|
|
97
|
+
if name != tool_handler.name:
|
|
98
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
99
|
+
|
|
100
|
+
if not isinstance(arguments, dict):
|
|
101
|
+
raise ValueError("Arguments must be a dictionary")
|
|
102
|
+
|
|
103
|
+
return await tool_handler.run_tool(arguments)
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(traceback.format_exc())
|
|
107
|
+
logger.error(f"Error during call_tool: {str(e)}")
|
|
108
|
+
raise RuntimeError(f"Error executing command: {str(e)}") from e
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def main() -> None:
|
|
112
|
+
"""Main entry point for the MCP shell server"""
|
|
113
|
+
logger.info("Starting MCP shell server")
|
|
114
|
+
try:
|
|
115
|
+
from mcp.server.stdio import stdio_server
|
|
116
|
+
|
|
117
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
118
|
+
await app.run(
|
|
119
|
+
read_stream, write_stream, app.create_initialization_options()
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"Server error: {str(e)}")
|
|
123
|
+
raise
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ShellExecutor:
|
|
8
|
+
"""
|
|
9
|
+
Executes shell commands in a secure manner by validating against a whitelist.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the executor. The allowed commands are read from ALLOW_COMMANDS
|
|
15
|
+
environment variable during command validation, not at initialization.
|
|
16
|
+
"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def _get_allowed_commands(self) -> set:
|
|
20
|
+
"""
|
|
21
|
+
Get the set of allowed commands from environment variable.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
set: Set of allowed command names
|
|
25
|
+
"""
|
|
26
|
+
allow_commands = os.environ.get("ALLOW_COMMANDS", "")
|
|
27
|
+
return {cmd.strip() for cmd in allow_commands.split(",") if cmd.strip()}
|
|
28
|
+
|
|
29
|
+
def _clean_command(self, command: List[str]) -> List[str]:
|
|
30
|
+
"""
|
|
31
|
+
Clean command by trimming whitespace from each part.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
command (List[str]): Original command and its arguments
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List[str]: Cleaned command with trimmed whitespace
|
|
38
|
+
"""
|
|
39
|
+
return [arg.strip() for arg in command if arg.strip()]
|
|
40
|
+
|
|
41
|
+
def _validate_command(self, command: List[str]) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Validate if the command is allowed to be executed.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
command (List[str]): Command and its arguments
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If the command is empty, not allowed, or contains invalid shell operators
|
|
50
|
+
"""
|
|
51
|
+
if not command:
|
|
52
|
+
raise ValueError("Empty command")
|
|
53
|
+
|
|
54
|
+
allowed_commands = self._get_allowed_commands()
|
|
55
|
+
if not allowed_commands:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"No commands are allowed. Please set ALLOW_COMMANDS environment variable."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Clean and check the first command
|
|
61
|
+
cleaned_cmd = command[0].strip()
|
|
62
|
+
if cleaned_cmd not in allowed_commands:
|
|
63
|
+
raise ValueError(f"Command not allowed: {cleaned_cmd}")
|
|
64
|
+
|
|
65
|
+
# Check for shell operators and validate subsequent commands
|
|
66
|
+
for i, arg in enumerate(command[1:], start=1):
|
|
67
|
+
cleaned_arg = arg.strip()
|
|
68
|
+
if cleaned_arg in [";", "&&", "||", "|"]:
|
|
69
|
+
if i + 1 >= len(command):
|
|
70
|
+
raise ValueError(f"Unexpected shell operator: {cleaned_arg}")
|
|
71
|
+
next_cmd = command[i + 1].strip()
|
|
72
|
+
if next_cmd not in allowed_commands:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Command not allowed after {cleaned_arg}: {next_cmd}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _validate_directory(self, directory: Optional[str]) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Validate if the directory exists and is accessible.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
directory (Optional[str]): Directory path to validate
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ValueError: If the directory doesn't exist or is not accessible
|
|
86
|
+
"""
|
|
87
|
+
if directory is None:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if not os.path.exists(directory):
|
|
91
|
+
raise ValueError(f"Directory does not exist: {directory}")
|
|
92
|
+
if not os.path.isdir(directory):
|
|
93
|
+
raise ValueError(f"Not a directory: {directory}")
|
|
94
|
+
if not os.access(directory, os.R_OK | os.X_OK):
|
|
95
|
+
raise ValueError(f"Directory is not accessible: {directory}")
|
|
96
|
+
|
|
97
|
+
def get_allowed_commands(self) -> list[str]:
|
|
98
|
+
"""Get the allowed commands"""
|
|
99
|
+
return list(self._get_allowed_commands())
|
|
100
|
+
|
|
101
|
+
async def execute(
|
|
102
|
+
self,
|
|
103
|
+
command: List[str],
|
|
104
|
+
stdin: Optional[str] = None,
|
|
105
|
+
directory: Optional[str] = None,
|
|
106
|
+
) -> Dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Execute a shell command with optional stdin input and working directory.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
command (List[str]): Command and its arguments
|
|
112
|
+
stdin (Optional[str]): Input to be passed to the command via stdin
|
|
113
|
+
directory (Optional[str]): Working directory for command execution
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict[str, Any]: Execution result containing stdout, stderr, status code, and execution time.
|
|
117
|
+
If error occurs, result contains additional 'error' field.
|
|
118
|
+
"""
|
|
119
|
+
start_time = time.time()
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Clean command before validation and execution
|
|
123
|
+
cleaned_command = self._clean_command(command)
|
|
124
|
+
if not cleaned_command:
|
|
125
|
+
raise ValueError("Empty command")
|
|
126
|
+
|
|
127
|
+
self._validate_command(cleaned_command)
|
|
128
|
+
self._validate_directory(directory)
|
|
129
|
+
except ValueError as e:
|
|
130
|
+
return {
|
|
131
|
+
"error": str(e),
|
|
132
|
+
"status": 1,
|
|
133
|
+
"stdout": "",
|
|
134
|
+
"stderr": str(e),
|
|
135
|
+
"execution_time": time.time() - start_time,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
process = await asyncio.create_subprocess_exec(
|
|
140
|
+
cleaned_command[0],
|
|
141
|
+
*cleaned_command[1:],
|
|
142
|
+
stdin=asyncio.subprocess.PIPE if stdin else None,
|
|
143
|
+
stdout=asyncio.subprocess.PIPE,
|
|
144
|
+
stderr=asyncio.subprocess.PIPE,
|
|
145
|
+
env={"PATH": os.environ.get("PATH", "")},
|
|
146
|
+
cwd=directory, # Set working directory if specified
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
stdin_bytes = stdin.encode() if stdin else None
|
|
150
|
+
stdout, stderr = await process.communicate(input=stdin_bytes)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"error": None, # Set error field to None for success case
|
|
154
|
+
"stdout": stdout.decode() if stdout else "",
|
|
155
|
+
"stderr": stderr.decode() if stderr else "",
|
|
156
|
+
"status": process.returncode,
|
|
157
|
+
"execution_time": time.time() - start_time,
|
|
158
|
+
"directory": directory, # Include working directory in response
|
|
159
|
+
}
|
|
160
|
+
except FileNotFoundError:
|
|
161
|
+
return {
|
|
162
|
+
"error": f"Command not found: {cleaned_command[0]}",
|
|
163
|
+
"status": 1,
|
|
164
|
+
"stdout": "",
|
|
165
|
+
"stderr": f"Command not found: {cleaned_command[0]}",
|
|
166
|
+
"execution_time": time.time() - start_time,
|
|
167
|
+
}
|
|
168
|
+
except Exception as e:
|
|
169
|
+
return {
|
|
170
|
+
"error": str(e),
|
|
171
|
+
"status": 1,
|
|
172
|
+
"stdout": "",
|
|
173
|
+
"stderr": str(e),
|
|
174
|
+
"execution_time": time.time() - start_time,
|
|
175
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mcp-shell-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP Shell Server - Execute shell commands via MCP protocol
|
|
5
|
+
Author: tumf
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: asyncio>=3.4.3
|
|
9
|
+
Requires-Dist: mcp>=1.1.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: black>=23.3.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: mypy>=1.2.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pre-commit>=3.2.2; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff>=0.0.262; extra == 'dev'
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
18
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'test'
|
|
19
|
+
Requires-Dist: pytest-env>=1.1.0; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest>=7.4.0; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# MCP Shell Server
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
* **Secure Command Execution**: Only whitelisted commands can be executed
|
|
30
|
+
* **Standard Input Support**: Pass input to commands via stdin
|
|
31
|
+
* **Comprehensive Output**: Returns stdout, stderr, exit status, and execution time
|
|
32
|
+
* **Shell Operator Safety**: Validates commands after shell operators (; , &&, ||, |)
|
|
33
|
+
|
|
34
|
+
## MCP client setting in your Claude.app
|
|
35
|
+
|
|
36
|
+
```shell
|
|
37
|
+
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"shell": {
|
|
44
|
+
"command": "uv",
|
|
45
|
+
"args": [
|
|
46
|
+
"--directory",
|
|
47
|
+
".",
|
|
48
|
+
"run",
|
|
49
|
+
"mcp-shell-server"
|
|
50
|
+
],
|
|
51
|
+
"env": {
|
|
52
|
+
"ALLOW_COMMANDS": "ls,cat,pwd,grep,wc,touch,find"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install mcp-shell-server
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Starting the Server
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
ALLOW_COMMANDS="ls,cat,echo" uvx mcp-shell-server
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `ALLOW_COMMANDS` environment variable specifies which commands are allowed to be executed. Commands can be separated by commas with optional spaces around them.
|
|
74
|
+
|
|
75
|
+
Valid formats for ALLOW_COMMANDS:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ALLOW_COMMANDS="ls,cat,echo" # Basic format
|
|
79
|
+
ALLOW_COMMANDS="ls ,echo, cat" # With spaces
|
|
80
|
+
ALLOW_COMMANDS="ls, cat , echo" # Multiple spaces
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Request Format
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# Basic command execution
|
|
87
|
+
{
|
|
88
|
+
"command": ["ls", "-l", "/tmp"]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Command with stdin input
|
|
92
|
+
{
|
|
93
|
+
"command": ["cat"],
|
|
94
|
+
"stdin": "Hello, World!"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Response Format
|
|
99
|
+
|
|
100
|
+
Successful response:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"stdout": "command output",
|
|
105
|
+
"stderr": "",
|
|
106
|
+
"status": 0,
|
|
107
|
+
"execution_time": 0.123
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Error response:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"error": "Command not allowed: rm",
|
|
116
|
+
"status": 1,
|
|
117
|
+
"stdout": "",
|
|
118
|
+
"stderr": "Command not allowed: rm",
|
|
119
|
+
"execution_time": 0
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Security
|
|
124
|
+
|
|
125
|
+
The server implements several security measures:
|
|
126
|
+
|
|
127
|
+
1. **Command Whitelisting**: Only explicitly allowed commands can be executed
|
|
128
|
+
2. **Shell Operator Validation**: Commands after shell operators (;, &&, ||, |) are also validated against the whitelist
|
|
129
|
+
3. **No Shell Injection**: Commands are executed directly without shell interpretation
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
### Setting up Development Environment
|
|
134
|
+
|
|
135
|
+
1. Clone the repository
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/yourusername/mcp-shell-server.git
|
|
139
|
+
cd mcp-shell-server
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
2. Install dependencies including test requirements
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install -e ".[test]"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Running Tests
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pytest
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## API Reference
|
|
155
|
+
|
|
156
|
+
### Request Arguments
|
|
157
|
+
|
|
158
|
+
| Field | Type | Required | Description |
|
|
159
|
+
|----------|------------|----------|-----------------------------------------------|
|
|
160
|
+
| command | string[] | Yes | Command and its arguments as array elements |
|
|
161
|
+
| stdin | string | No | Input to be passed to the command |
|
|
162
|
+
|
|
163
|
+
### Response Fields
|
|
164
|
+
|
|
165
|
+
| Field | Type | Description |
|
|
166
|
+
|----------------|---------|---------------------------------------------|
|
|
167
|
+
| stdout | string | Standard output from the command |
|
|
168
|
+
| stderr | string | Standard error output from the command |
|
|
169
|
+
| status | integer | Exit status code |
|
|
170
|
+
| execution_time | float | Time taken to execute (in seconds) |
|
|
171
|
+
| error | string | Error message (only present if failed) |
|
|
172
|
+
|
|
173
|
+
## Requirements
|
|
174
|
+
|
|
175
|
+
* Python 3.11 or higher
|
|
176
|
+
* mcp>=1.1.0
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT License - See LICENSE file for details
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mcp_shell_server/__init__.py,sha256=PXUa6MYNfCK7XRaIeod8TRp4_NXWRhnC2MyZq4iwbjA,271
|
|
2
|
+
mcp_shell_server/server.py,sha256=EPQ3p232eeak4vAfaKJKn5dXa7CWZK4oawRKnng5JUU,3890
|
|
3
|
+
mcp_shell_server/shell_executor.py,sha256=5_wKIQQzobaYXdCUYe17qPDH4GMmOgop4wm3IDmDcG0,6329
|
|
4
|
+
mcp_shell_server-0.1.0.dist-info/METADATA,sha256=A0L0xv1ex-xoaUtebiQ-DWA0bHFwoO_tp-0_yhkj56M,4400
|
|
5
|
+
mcp_shell_server-0.1.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
6
|
+
mcp_shell_server-0.1.0.dist-info/entry_points.txt,sha256=cliA1OUE6oGFNpFqxK39iVUAEYITAYn1JnufKxl5Kn8,59
|
|
7
|
+
mcp_shell_server-0.1.0.dist-info/RECORD,,
|