cli-mcp-server 0.2.1__py3-none-any.whl → 0.2.3__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.
- cli_mcp_server/server.py +91 -25
- {cli_mcp_server-0.2.1.dist-info → cli_mcp_server-0.2.3.dist-info}/METADATA +43 -33
- cli_mcp_server-0.2.3.dist-info/RECORD +7 -0
- cli_mcp_server-0.2.1.dist-info/RECORD +0 -7
- {cli_mcp_server-0.2.1.dist-info → cli_mcp_server-0.2.3.dist-info}/WHEEL +0 -0
- {cli_mcp_server-0.2.1.dist-info → cli_mcp_server-0.2.3.dist-info}/entry_points.txt +0 -0
- {cli_mcp_server-0.2.1.dist-info → cli_mcp_server-0.2.3.dist-info}/licenses/LICENSE +0 -0
cli_mcp_server/server.py
CHANGED
@@ -68,10 +68,14 @@ class CommandExecutor:
|
|
68
68
|
real_path = os.path.abspath(os.path.realpath(path))
|
69
69
|
else:
|
70
70
|
# If relative path, combine with allowed_dir first
|
71
|
-
real_path = os.path.abspath(
|
71
|
+
real_path = os.path.abspath(
|
72
|
+
os.path.realpath(os.path.join(self.allowed_dir, path))
|
73
|
+
)
|
72
74
|
|
73
75
|
if not self._is_path_safe(real_path):
|
74
|
-
raise CommandSecurityError(
|
76
|
+
raise CommandSecurityError(
|
77
|
+
f"Path '{path}' is outside of allowed directory: {self.allowed_dir}"
|
78
|
+
)
|
75
79
|
|
76
80
|
return real_path
|
77
81
|
except CommandSecurityError:
|
@@ -102,7 +106,9 @@ class CommandExecutor:
|
|
102
106
|
shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
|
103
107
|
for operator in shell_operators:
|
104
108
|
if operator in command_string:
|
105
|
-
raise CommandSecurityError(
|
109
|
+
raise CommandSecurityError(
|
110
|
+
f"Shell operator '{operator}' is not supported"
|
111
|
+
)
|
106
112
|
|
107
113
|
try:
|
108
114
|
parts = shlex.split(command_string)
|
@@ -112,20 +118,31 @@ class CommandExecutor:
|
|
112
118
|
command, args = parts[0], parts[1:]
|
113
119
|
|
114
120
|
# Validate command if not in allow-all mode
|
115
|
-
if
|
121
|
+
if (
|
122
|
+
not self.security_config.allow_all_commands
|
123
|
+
and command not in self.security_config.allowed_commands
|
124
|
+
):
|
116
125
|
raise CommandSecurityError(f"Command '{command}' is not allowed")
|
117
126
|
|
118
127
|
# Process and validate arguments
|
119
128
|
validated_args = []
|
120
129
|
for arg in args:
|
121
130
|
if arg.startswith("-"):
|
122
|
-
if
|
131
|
+
if (
|
132
|
+
not self.security_config.allow_all_flags
|
133
|
+
and arg not in self.security_config.allowed_flags
|
134
|
+
):
|
123
135
|
raise CommandSecurityError(f"Flag '{arg}' is not allowed")
|
124
136
|
validated_args.append(arg)
|
125
137
|
continue
|
126
138
|
|
127
139
|
# For any path-like argument, validate it
|
128
140
|
if "/" in arg or "\\" in arg or os.path.isabs(arg) or arg == ".":
|
141
|
+
if self._is_url_path(arg):
|
142
|
+
# If it's a URL, we don't need to normalize it
|
143
|
+
validated_args.append(arg)
|
144
|
+
continue
|
145
|
+
|
129
146
|
normalized_path = self._normalize_path(arg)
|
130
147
|
validated_args.append(normalized_path)
|
131
148
|
else:
|
@@ -137,6 +154,19 @@ class CommandExecutor:
|
|
137
154
|
except ValueError as e:
|
138
155
|
raise CommandSecurityError(f"Invalid command format: {str(e)}")
|
139
156
|
|
157
|
+
def _is_url_path(self, path: str) -> bool:
|
158
|
+
"""
|
159
|
+
Checks if a given path is a URL of type http or https.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
path (str): The path to check.
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
bool: True if the path is a URL, False otherwise.
|
166
|
+
"""
|
167
|
+
url_pattern = re.compile(r"^https?://")
|
168
|
+
return bool(url_pattern.match(path))
|
169
|
+
|
140
170
|
def _is_path_safe(self, path: str) -> bool:
|
141
171
|
"""
|
142
172
|
Checks if a given path is safe to access within allowed directory boundaries.
|
@@ -190,7 +220,9 @@ class CommandExecutor:
|
|
190
220
|
- Captures both stdout and stderr
|
191
221
|
"""
|
192
222
|
if len(command_string) > self.security_config.max_command_length:
|
193
|
-
raise CommandSecurityError(
|
223
|
+
raise CommandSecurityError(
|
224
|
+
f"Command exceeds maximum length of {self.security_config.max_command_length}"
|
225
|
+
)
|
194
226
|
|
195
227
|
try:
|
196
228
|
command, args = self.validate_command(command_string)
|
@@ -204,7 +236,9 @@ class CommandExecutor:
|
|
204
236
|
cwd=self.allowed_dir,
|
205
237
|
)
|
206
238
|
except subprocess.TimeoutExpired:
|
207
|
-
raise CommandTimeoutError(
|
239
|
+
raise CommandTimeoutError(
|
240
|
+
f"Command timed out after {self.security_config.command_timeout} seconds"
|
241
|
+
)
|
208
242
|
except CommandError:
|
209
243
|
raise
|
210
244
|
except Exception as e:
|
@@ -237,12 +271,14 @@ def load_security_config() -> SecurityConfig:
|
|
237
271
|
"""
|
238
272
|
allowed_commands = os.getenv("ALLOWED_COMMANDS", "ls,cat,pwd")
|
239
273
|
allowed_flags = os.getenv("ALLOWED_FLAGS", "-l,-a,--help")
|
240
|
-
|
241
|
-
allow_all_commands = allowed_commands.lower() ==
|
242
|
-
allow_all_flags = allowed_flags.lower() ==
|
243
|
-
|
274
|
+
|
275
|
+
allow_all_commands = allowed_commands.lower() == "all"
|
276
|
+
allow_all_flags = allowed_flags.lower() == "all"
|
277
|
+
|
244
278
|
return SecurityConfig(
|
245
|
-
allowed_commands=
|
279
|
+
allowed_commands=(
|
280
|
+
set() if allow_all_commands else set(allowed_commands.split(","))
|
281
|
+
),
|
246
282
|
allowed_flags=set() if allow_all_flags else set(allowed_flags.split(",")),
|
247
283
|
max_command_length=int(os.getenv("MAX_COMMAND_LENGTH", "1024")),
|
248
284
|
command_timeout=int(os.getenv("COMMAND_TIMEOUT", "30")),
|
@@ -251,14 +287,24 @@ def load_security_config() -> SecurityConfig:
|
|
251
287
|
)
|
252
288
|
|
253
289
|
|
254
|
-
executor = CommandExecutor(
|
290
|
+
executor = CommandExecutor(
|
291
|
+
allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config()
|
292
|
+
)
|
255
293
|
|
256
294
|
|
257
295
|
@server.list_tools()
|
258
296
|
async def handle_list_tools() -> list[types.Tool]:
|
259
|
-
commands_desc =
|
260
|
-
|
261
|
-
|
297
|
+
commands_desc = (
|
298
|
+
"all commands"
|
299
|
+
if executor.security_config.allow_all_commands
|
300
|
+
else ", ".join(executor.security_config.allowed_commands)
|
301
|
+
)
|
302
|
+
flags_desc = (
|
303
|
+
"all flags"
|
304
|
+
if executor.security_config.allow_all_flags
|
305
|
+
else ", ".join(executor.security_config.allowed_flags)
|
306
|
+
)
|
307
|
+
|
262
308
|
return [
|
263
309
|
types.Tool(
|
264
310
|
name="run_command",
|
@@ -281,7 +327,9 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
281
327
|
),
|
282
328
|
types.Tool(
|
283
329
|
name="show_security_rules",
|
284
|
-
description=(
|
330
|
+
description=(
|
331
|
+
"Show what commands and operations are allowed in this environment.\n"
|
332
|
+
),
|
285
333
|
inputSchema={
|
286
334
|
"type": "object",
|
287
335
|
"properties": {},
|
@@ -291,10 +339,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
291
339
|
|
292
340
|
|
293
341
|
@server.call_tool()
|
294
|
-
async def handle_call_tool(
|
342
|
+
async def handle_call_tool(
|
343
|
+
name: str, arguments: Optional[Dict[str, Any]]
|
344
|
+
) -> List[types.TextContent]:
|
295
345
|
if name == "run_command":
|
296
346
|
if not arguments or "command" not in arguments:
|
297
|
-
return [
|
347
|
+
return [
|
348
|
+
types.TextContent(type="text", text="No command provided", error=True)
|
349
|
+
]
|
298
350
|
|
299
351
|
try:
|
300
352
|
result = executor.execute(arguments["command"])
|
@@ -303,7 +355,9 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
|
|
303
355
|
if result.stdout:
|
304
356
|
response.append(types.TextContent(type="text", text=result.stdout))
|
305
357
|
if result.stderr:
|
306
|
-
response.append(
|
358
|
+
response.append(
|
359
|
+
types.TextContent(type="text", text=result.stderr, error=True)
|
360
|
+
)
|
307
361
|
|
308
362
|
response.append(
|
309
363
|
types.TextContent(
|
@@ -315,7 +369,11 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
|
|
315
369
|
return response
|
316
370
|
|
317
371
|
except CommandSecurityError as e:
|
318
|
-
return [
|
372
|
+
return [
|
373
|
+
types.TextContent(
|
374
|
+
type="text", text=f"Security violation: {str(e)}", error=True
|
375
|
+
)
|
376
|
+
]
|
319
377
|
except subprocess.TimeoutExpired:
|
320
378
|
return [
|
321
379
|
types.TextContent(
|
@@ -328,9 +386,17 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
|
|
328
386
|
return [types.TextContent(type="text", text=f"Error: {str(e)}", error=True)]
|
329
387
|
|
330
388
|
elif name == "show_security_rules":
|
331
|
-
commands_desc =
|
332
|
-
|
333
|
-
|
389
|
+
commands_desc = (
|
390
|
+
"All commands allowed"
|
391
|
+
if executor.security_config.allow_all_commands
|
392
|
+
else ", ".join(sorted(executor.security_config.allowed_commands))
|
393
|
+
)
|
394
|
+
flags_desc = (
|
395
|
+
"All flags allowed"
|
396
|
+
if executor.security_config.allow_all_flags
|
397
|
+
else ", ".join(sorted(executor.security_config.allowed_flags))
|
398
|
+
)
|
399
|
+
|
334
400
|
security_info = (
|
335
401
|
"Security Configuration:\n"
|
336
402
|
f"==================\n"
|
@@ -364,4 +430,4 @@ async def main():
|
|
364
430
|
experimental_capabilities={},
|
365
431
|
),
|
366
432
|
),
|
367
|
-
)
|
433
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cli-mcp-server
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
4
4
|
Summary: Command line interface for MCP clients with secure execution and customizable security policies
|
5
5
|
Project-URL: Homepage, https://github.com/MladenSU/cli-mcp-server
|
6
6
|
Project-URL: Documentation, https://github.com/MladenSU/cli-mcp-server#readme
|
@@ -9,7 +9,7 @@ Project-URL: Bug Tracker, https://github.com/MladenSU/cli-mcp-server/issues
|
|
9
9
|
Author-email: Mladen <fangs-lever6n@icloud.com>
|
10
10
|
License-File: LICENSE
|
11
11
|
Requires-Python: >=3.10
|
12
|
-
Requires-Dist: mcp>=1.
|
12
|
+
Requires-Dist: mcp>=1.6.0
|
13
13
|
Description-Content-Type: text/markdown
|
14
14
|
|
15
15
|
# CLI MCP Server
|
@@ -17,8 +17,7 @@ Description-Content-Type: text/markdown
|
|
17
17
|
---
|
18
18
|
|
19
19
|
A secure Model Context Protocol (MCP) server implementation for executing controlled command-line operations with
|
20
|
-
comprehensive security
|
21
|
-
features.
|
20
|
+
comprehensive security features.
|
22
21
|
|
23
22
|

|
24
23
|

|
@@ -53,30 +52,32 @@ features.
|
|
53
52
|
## Overview
|
54
53
|
|
55
54
|
This MCP server enables secure command-line execution with robust security measures including command whitelisting, path
|
56
|
-
validation, and
|
57
|
-
execution controls. Perfect for providing controlled CLI access to LLM applications while maintaining security.
|
55
|
+
validation, and execution controls. Perfect for providing controlled CLI access to LLM applications while maintaining security.
|
58
56
|
|
59
57
|
## Features
|
60
58
|
|
61
59
|
- 🔒 Secure command execution with strict validation
|
62
|
-
- ⚙️ Configurable command and flag whitelisting
|
63
|
-
- 🛡️ Path traversal prevention
|
60
|
+
- ⚙️ Configurable command and flag whitelisting with 'all' option
|
61
|
+
- 🛡️ Path traversal prevention and validation
|
64
62
|
- 🚫 Shell operator injection protection
|
65
63
|
- ⏱️ Execution timeouts and length limits
|
66
64
|
- 📝 Detailed error reporting
|
67
65
|
- 🔄 Async operation support
|
66
|
+
- 🎯 Working directory restriction and validation
|
68
67
|
|
69
68
|
## Configuration
|
70
69
|
|
71
70
|
Configure the server using environment variables:
|
72
71
|
|
73
|
-
| Variable | Description
|
74
|
-
|
75
|
-
| `ALLOWED_DIR`
|
76
|
-
| `ALLOWED_COMMANDS`
|
77
|
-
| `ALLOWED_FLAGS`
|
78
|
-
| `MAX_COMMAND_LENGTH
|
79
|
-
| `COMMAND_TIMEOUT`
|
72
|
+
| Variable | Description | Default |
|
73
|
+
|---------------------|------------------------------------------------------|-------------------|
|
74
|
+
| `ALLOWED_DIR` | Base directory for command execution (Required) | None (Required) |
|
75
|
+
| `ALLOWED_COMMANDS` | Comma-separated list of allowed commands or 'all' | `ls,cat,pwd` |
|
76
|
+
| `ALLOWED_FLAGS` | Comma-separated list of allowed flags or 'all' | `-l,-a,--help` |
|
77
|
+
| `MAX_COMMAND_LENGTH`| Maximum command string length | `1024` |
|
78
|
+
| `COMMAND_TIMEOUT` | Command execution timeout (seconds) | `30` |
|
79
|
+
|
80
|
+
Note: Setting `ALLOWED_COMMANDS` or `ALLOWED_FLAGS` to 'all' will allow any command or flag respectively.
|
80
81
|
|
81
82
|
## Installation
|
82
83
|
|
@@ -93,19 +94,28 @@ npx @smithery/cli install cli-mcp-server --client claude
|
|
93
94
|
Executes whitelisted CLI commands within allowed directories.
|
94
95
|
|
95
96
|
**Input Schema:**
|
96
|
-
|
97
|
-
|
98
|
-
{
|
97
|
+
```json
|
98
|
+
{
|
99
99
|
"command": {
|
100
100
|
"type": "string",
|
101
|
-
"description": "
|
101
|
+
"description": "Single command to execute (e.g., 'ls -l' or 'cat file.txt')"
|
102
102
|
}
|
103
103
|
}
|
104
|
-
|
104
|
+
```
|
105
|
+
|
106
|
+
**Security Notes:**
|
107
|
+
- Shell operators (&&, |, >, >>) are not supported
|
108
|
+
- Commands must be whitelisted unless ALLOWED_COMMANDS='all'
|
109
|
+
- Flags must be whitelisted unless ALLOWED_FLAGS='all'
|
110
|
+
- All paths are validated to be within ALLOWED_DIR
|
105
111
|
|
106
112
|
### show_security_rules
|
107
113
|
|
108
|
-
Displays current security configuration and restrictions
|
114
|
+
Displays current security configuration and restrictions, including:
|
115
|
+
- Working directory
|
116
|
+
- Allowed commands
|
117
|
+
- Allowed flags
|
118
|
+
- Security limits (max command length and timeout)
|
109
119
|
|
110
120
|
## Usage with Claude Desktop
|
111
121
|
|
@@ -113,7 +123,7 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
113
123
|
|
114
124
|
> Development/Unpublished Servers Configuration
|
115
125
|
|
116
|
-
|
126
|
+
```json
|
117
127
|
{
|
118
128
|
"mcpServers": {
|
119
129
|
"cli-mcp-server": {
|
@@ -134,7 +144,7 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
134
144
|
}
|
135
145
|
}
|
136
146
|
}
|
137
|
-
|
147
|
+
```
|
138
148
|
|
139
149
|
> Published Servers Configuration
|
140
150
|
|
@@ -161,23 +171,25 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
161
171
|
|
162
172
|
## Security Features
|
163
173
|
|
164
|
-
- ✅ Command whitelist enforcement
|
165
|
-
- ✅ Flag validation
|
166
|
-
- ✅ Path traversal prevention
|
174
|
+
- ✅ Command whitelist enforcement with 'all' option
|
175
|
+
- ✅ Flag validation with 'all' option
|
176
|
+
- ✅ Path traversal prevention and normalization
|
167
177
|
- ✅ Shell operator blocking
|
168
178
|
- ✅ Command length limits
|
169
179
|
- ✅ Execution timeouts
|
170
180
|
- ✅ Working directory restrictions
|
181
|
+
- ✅ Symlink resolution and validation
|
171
182
|
|
172
183
|
## Error Handling
|
173
184
|
|
174
185
|
The server provides detailed error messages for:
|
175
186
|
|
176
|
-
- Security violations
|
177
|
-
- Command timeouts
|
187
|
+
- Security violations (CommandSecurityError)
|
188
|
+
- Command timeouts (CommandTimeoutError)
|
178
189
|
- Invalid command formats
|
179
190
|
- Path security violations
|
180
|
-
- Execution failures
|
191
|
+
- Execution failures (CommandExecutionError)
|
192
|
+
- General command errors (CommandError)
|
181
193
|
|
182
194
|
## Development
|
183
195
|
|
@@ -186,8 +198,6 @@ The server provides detailed error messages for:
|
|
186
198
|
- Python 3.10+
|
187
199
|
- MCP protocol library
|
188
200
|
|
189
|
-
## Development
|
190
|
-
|
191
201
|
### Building and Publishing
|
192
202
|
|
193
203
|
To prepare the package for distribution:
|
@@ -227,6 +237,6 @@ Upon launching, the Inspector will display a URL that you can access in your bro
|
|
227
237
|
|
228
238
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
229
239
|
|
230
|
-
|
240
|
+
---
|
231
241
|
|
232
|
-
For more information or support, please open an issue on the project repository.
|
242
|
+
For more information or support, please open an issue on the project repository.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
cli_mcp_server/__init__.py,sha256=bGLiX7XuhVsS-PJdoRIWXiilZ3NTAQ7fb9_8kkNzLlM,216
|
2
|
+
cli_mcp_server/server.py,sha256=CHP2x_FDx1Rz79ZnmznE6J7-9EHdafK-UibrVTvQx7k,15124
|
3
|
+
cli_mcp_server-0.2.3.dist-info/METADATA,sha256=EiU10rdnN39XiTq2m0UjTrZDR0-1AEei9NFNf-6NBfk,7371
|
4
|
+
cli_mcp_server-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
cli_mcp_server-0.2.3.dist-info/entry_points.txt,sha256=07bDmJJSXg-6VCFEFTOlsGoxI-0faJafT1yEEjUdN-U,55
|
6
|
+
cli_mcp_server-0.2.3.dist-info/licenses/LICENSE,sha256=85rOR_IMpb2VzXBA4VCRZh_KWlaO1Rly8HDYDwGgMWk,1062
|
7
|
+
cli_mcp_server-0.2.3.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
cli_mcp_server/__init__.py,sha256=bGLiX7XuhVsS-PJdoRIWXiilZ3NTAQ7fb9_8kkNzLlM,216
|
2
|
-
cli_mcp_server/server.py,sha256=N2BtE8YLf8P643GPwawnnmf1kg7blY6Z0x2KP9zYhS8,13918
|
3
|
-
cli_mcp_server-0.2.1.dist-info/METADATA,sha256=rWoOhhU4jK0Yt8BAguG1_k9v3y5q9SiJbw6PWfKl75M,6566
|
4
|
-
cli_mcp_server-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
-
cli_mcp_server-0.2.1.dist-info/entry_points.txt,sha256=07bDmJJSXg-6VCFEFTOlsGoxI-0faJafT1yEEjUdN-U,55
|
6
|
-
cli_mcp_server-0.2.1.dist-info/licenses/LICENSE,sha256=85rOR_IMpb2VzXBA4VCRZh_KWlaO1Rly8HDYDwGgMWk,1062
|
7
|
-
cli_mcp_server-0.2.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|