cli-mcp-server 0.2.2__py3-none-any.whl → 0.2.4__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 +253 -77
- {cli_mcp_server-0.2.2.dist-info → cli_mcp_server-0.2.4.dist-info}/METADATA +10 -6
- cli_mcp_server-0.2.4.dist-info/RECORD +7 -0
- cli_mcp_server-0.2.2.dist-info/RECORD +0 -7
- {cli_mcp_server-0.2.2.dist-info → cli_mcp_server-0.2.4.dist-info}/WHEEL +0 -0
- {cli_mcp_server-0.2.2.dist-info → cli_mcp_server-0.2.4.dist-info}/entry_points.txt +0 -0
- {cli_mcp_server-0.2.2.dist-info → cli_mcp_server-0.2.4.dist-info}/licenses/LICENSE +0 -0
cli_mcp_server/server.py
CHANGED
@@ -49,6 +49,7 @@ class SecurityConfig:
|
|
49
49
|
command_timeout: int
|
50
50
|
allow_all_commands: bool = False
|
51
51
|
allow_all_flags: bool = False
|
52
|
+
allow_shell_operators: bool = False
|
52
53
|
|
53
54
|
|
54
55
|
class CommandExecutor:
|
@@ -62,16 +63,20 @@ class CommandExecutor:
|
|
62
63
|
"""
|
63
64
|
Normalizes a path and ensures it's within allowed directory.
|
64
65
|
"""
|
65
|
-
try:
|
66
|
+
try:
|
66
67
|
if os.path.isabs(path):
|
67
68
|
# If absolute path, check directly
|
68
69
|
real_path = os.path.abspath(os.path.realpath(path))
|
69
70
|
else:
|
70
71
|
# If relative path, combine with allowed_dir first
|
71
|
-
real_path = os.path.abspath(
|
72
|
+
real_path = os.path.abspath(
|
73
|
+
os.path.realpath(os.path.join(self.allowed_dir, path))
|
74
|
+
)
|
72
75
|
|
73
76
|
if not self._is_path_safe(real_path):
|
74
|
-
raise CommandSecurityError(
|
77
|
+
raise CommandSecurityError(
|
78
|
+
f"Path '{path}' is outside of allowed directory: {self.allowed_dir}"
|
79
|
+
)
|
75
80
|
|
76
81
|
return real_path
|
77
82
|
except CommandSecurityError:
|
@@ -83,27 +88,103 @@ class CommandExecutor:
|
|
83
88
|
"""
|
84
89
|
Validates and parses a command string for security and formatting.
|
85
90
|
|
86
|
-
Checks the command string
|
87
|
-
|
91
|
+
Checks if the command string contains shell operators. If it does, splits the command
|
92
|
+
by operators and validates each part individually. If all parts are valid, returns
|
93
|
+
the original command string to be executed with shell=True.
|
94
|
+
|
95
|
+
For commands without shell operators, splits into command and arguments and validates
|
96
|
+
each part according to security rules.
|
88
97
|
|
89
98
|
Args:
|
90
99
|
command_string (str): The command string to validate and parse.
|
91
100
|
|
92
101
|
Returns:
|
93
102
|
tuple[str, List[str]]: A tuple containing:
|
94
|
-
- The command name (str)
|
95
|
-
-
|
103
|
+
- For regular commands: The command name (str) and list of arguments (List[str])
|
104
|
+
- For commands with shell operators: The full command string and empty args list
|
96
105
|
|
97
106
|
Raises:
|
98
|
-
CommandSecurityError: If the command
|
107
|
+
CommandSecurityError: If any part of the command fails security validation.
|
99
108
|
"""
|
100
109
|
|
101
|
-
#
|
110
|
+
# Define shell operators
|
102
111
|
shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
|
103
|
-
for operator in shell_operators:
|
104
|
-
if operator in command_string:
|
105
|
-
raise CommandSecurityError(f"Shell operator '{operator}' is not supported")
|
106
112
|
|
113
|
+
# Check if command contains shell operators
|
114
|
+
contains_shell_operator = any(
|
115
|
+
operator in command_string for operator in shell_operators
|
116
|
+
)
|
117
|
+
|
118
|
+
if contains_shell_operator:
|
119
|
+
# Check if shell operators are allowed
|
120
|
+
if not self.security_config.allow_shell_operators:
|
121
|
+
# If shell operators are not allowed, raise an error
|
122
|
+
for operator in shell_operators:
|
123
|
+
if operator in command_string:
|
124
|
+
raise CommandSecurityError(
|
125
|
+
f"Shell operator '{operator}' is not supported. Set ALLOW_SHELL_OPERATORS=true to enable."
|
126
|
+
)
|
127
|
+
|
128
|
+
# Split the command by shell operators and validate each part
|
129
|
+
return self._validate_command_with_operators(
|
130
|
+
command_string, shell_operators
|
131
|
+
)
|
132
|
+
|
133
|
+
# Process single command without shell operators
|
134
|
+
return self._validate_single_command(command_string)
|
135
|
+
|
136
|
+
def _is_url_path(self, path: str) -> bool:
|
137
|
+
"""
|
138
|
+
Checks if a given path is a URL of type http or https.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
path (str): The path to check.
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
bool: True if the path is a URL, False otherwise.
|
145
|
+
"""
|
146
|
+
url_pattern = re.compile(r"^https?://")
|
147
|
+
return bool(url_pattern.match(path))
|
148
|
+
|
149
|
+
def _is_path_safe(self, path: str) -> bool:
|
150
|
+
"""
|
151
|
+
Checks if a given path is safe to access within allowed directory boundaries.
|
152
|
+
|
153
|
+
Validates that the absolute resolved path is within the allowed directory
|
154
|
+
to prevent directory traversal attacks.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
path (str): The path to validate.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
bool: True if path is within allowed directory, False otherwise.
|
161
|
+
Returns False if path resolution fails for any reason.
|
162
|
+
|
163
|
+
Private method intended for internal use only.
|
164
|
+
"""
|
165
|
+
try:
|
166
|
+
# Resolve any symlinks and get absolute path
|
167
|
+
real_path = os.path.abspath(os.path.realpath(path))
|
168
|
+
allowed_dir_real = os.path.abspath(os.path.realpath(self.allowed_dir))
|
169
|
+
|
170
|
+
# Check if the path starts with allowed_dir
|
171
|
+
return real_path.startswith(allowed_dir_real)
|
172
|
+
except Exception:
|
173
|
+
return False
|
174
|
+
|
175
|
+
def _validate_single_command(self, command_string: str) -> tuple[str, List[str]]:
|
176
|
+
"""
|
177
|
+
Validates a single command without shell operators.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
command_string (str): The command string to validate.
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
tuple[str, List[str]]: A tuple containing the command and validated arguments.
|
184
|
+
|
185
|
+
Raises:
|
186
|
+
CommandSecurityError: If the command fails validation.
|
187
|
+
"""
|
107
188
|
try:
|
108
189
|
parts = shlex.split(command_string)
|
109
190
|
if not parts:
|
@@ -112,25 +193,31 @@ class CommandExecutor:
|
|
112
193
|
command, args = parts[0], parts[1:]
|
113
194
|
|
114
195
|
# Validate command if not in allow-all mode
|
115
|
-
if
|
196
|
+
if (
|
197
|
+
not self.security_config.allow_all_commands
|
198
|
+
and command not in self.security_config.allowed_commands
|
199
|
+
):
|
116
200
|
raise CommandSecurityError(f"Command '{command}' is not allowed")
|
117
201
|
|
118
202
|
# Process and validate arguments
|
119
203
|
validated_args = []
|
120
204
|
for arg in args:
|
121
205
|
if arg.startswith("-"):
|
122
|
-
if
|
206
|
+
if (
|
207
|
+
not self.security_config.allow_all_flags
|
208
|
+
and arg not in self.security_config.allowed_flags
|
209
|
+
):
|
123
210
|
raise CommandSecurityError(f"Flag '{arg}' is not allowed")
|
124
211
|
validated_args.append(arg)
|
125
212
|
continue
|
126
213
|
|
127
214
|
# For any path-like argument, validate it
|
128
|
-
if "/" in arg or "\\" in arg or os.path.isabs(arg) or arg == ".":
|
215
|
+
if "/" in arg or "\\" in arg or os.path.isabs(arg) or arg == ".":
|
129
216
|
if self._is_url_path(arg):
|
130
217
|
# If it's a URL, we don't need to normalize it
|
131
218
|
validated_args.append(arg)
|
132
219
|
continue
|
133
|
-
|
220
|
+
|
134
221
|
normalized_path = self._normalize_path(arg)
|
135
222
|
validated_args.append(normalized_path)
|
136
223
|
else:
|
@@ -142,45 +229,69 @@ class CommandExecutor:
|
|
142
229
|
except ValueError as e:
|
143
230
|
raise CommandSecurityError(f"Invalid command format: {str(e)}")
|
144
231
|
|
145
|
-
def
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
Args:
|
150
|
-
path (str): The path to check.
|
151
|
-
|
152
|
-
Returns:
|
153
|
-
bool: True if the path is a URL, False otherwise.
|
232
|
+
def _validate_command_with_operators(
|
233
|
+
self, command_string: str, shell_operators: List[str]
|
234
|
+
) -> tuple[str, List[str]]:
|
154
235
|
"""
|
155
|
-
|
156
|
-
return bool(url_pattern.match(path))
|
157
|
-
|
158
|
-
|
159
|
-
def _is_path_safe(self, path: str) -> bool:
|
160
|
-
"""
|
161
|
-
Checks if a given path is safe to access within allowed directory boundaries.
|
236
|
+
Validates a command string that contains shell operators.
|
162
237
|
|
163
|
-
|
164
|
-
to
|
238
|
+
Splits the command string by shell operators and validates each part individually.
|
239
|
+
If all parts are valid, returns the original command to be executed with shell=True.
|
165
240
|
|
166
241
|
Args:
|
167
|
-
|
242
|
+
command_string (str): The command string containing shell operators.
|
243
|
+
shell_operators (List[str]): List of shell operators to split by.
|
168
244
|
|
169
245
|
Returns:
|
170
|
-
|
171
|
-
|
246
|
+
tuple[str, List[str]]: A tuple containing the command and empty args list
|
247
|
+
(since the command will be executed with shell=True)
|
172
248
|
|
173
|
-
|
249
|
+
Raises:
|
250
|
+
CommandSecurityError: If any part of the command fails validation.
|
174
251
|
"""
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
252
|
+
# Create a regex pattern to split by any of the shell operators
|
253
|
+
# We need to escape special regex characters in the operators
|
254
|
+
escaped_operators = [re.escape(op) for op in shell_operators]
|
255
|
+
pattern = "|".join(escaped_operators)
|
256
|
+
|
257
|
+
# Split the command string by shell operators, keeping the operators
|
258
|
+
parts = re.split(f"({pattern})", command_string)
|
259
|
+
|
260
|
+
# Filter out empty parts and whitespace-only parts
|
261
|
+
parts = [part.strip() for part in parts if part.strip()]
|
262
|
+
|
263
|
+
# Group commands and operators
|
264
|
+
commands = []
|
265
|
+
i = 0
|
266
|
+
while i < len(parts):
|
267
|
+
if i + 1 < len(parts) and parts[i + 1] in shell_operators:
|
268
|
+
# If next part is an operator, current part is a command
|
269
|
+
if parts[i]: # Skip empty commands
|
270
|
+
commands.append(parts[i])
|
271
|
+
i += 2 # Skip the operator
|
272
|
+
else:
|
273
|
+
# If no operator follows, this is the last command
|
274
|
+
if (
|
275
|
+
parts[i] and parts[i] not in shell_operators
|
276
|
+
): # Skip if it's an operator
|
277
|
+
commands.append(parts[i])
|
278
|
+
i += 1
|
279
|
+
|
280
|
+
# Validate each command individually
|
281
|
+
for cmd in commands:
|
282
|
+
try:
|
283
|
+
# Use the extracted validation method for each command
|
284
|
+
self._validate_single_command(cmd)
|
285
|
+
except CommandSecurityError as e:
|
286
|
+
raise CommandSecurityError(f"Invalid command part '{cmd}': {str(e)}")
|
287
|
+
except ValueError as e:
|
288
|
+
raise CommandSecurityError(
|
289
|
+
f"Invalid command format in '{cmd}': {str(e)}"
|
290
|
+
)
|
179
291
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
return False
|
292
|
+
# If we get here, all commands passed validation
|
293
|
+
# Return the original command string to be executed with shell=True
|
294
|
+
return command_string, []
|
184
295
|
|
185
296
|
def execute(self, command_string: str) -> subprocess.CompletedProcess:
|
186
297
|
"""
|
@@ -199,31 +310,58 @@ class CommandExecutor:
|
|
199
310
|
Raises:
|
200
311
|
CommandSecurityError: If the command:
|
201
312
|
- Exceeds maximum length
|
202
|
-
- Contains invalid shell operators
|
203
313
|
- Fails security validation
|
204
314
|
- Fails during execution
|
205
315
|
|
206
316
|
Notes:
|
207
|
-
-
|
317
|
+
- Uses shell=True for commands with shell operators, shell=False otherwise
|
208
318
|
- Uses timeout and working directory constraints
|
209
319
|
- Captures both stdout and stderr
|
210
320
|
"""
|
211
321
|
if len(command_string) > self.security_config.max_command_length:
|
212
|
-
raise CommandSecurityError(
|
322
|
+
raise CommandSecurityError(
|
323
|
+
f"Command exceeds maximum length of {self.security_config.max_command_length}"
|
324
|
+
)
|
213
325
|
|
214
326
|
try:
|
215
327
|
command, args = self.validate_command(command_string)
|
216
328
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
329
|
+
# Check if this is a command with shell operators
|
330
|
+
shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
|
331
|
+
use_shell = any(operator in command_string for operator in shell_operators)
|
332
|
+
|
333
|
+
# Double-check that shell operators are allowed if they are present
|
334
|
+
if use_shell and not self.security_config.allow_shell_operators:
|
335
|
+
for operator in shell_operators:
|
336
|
+
if operator in command_string:
|
337
|
+
raise CommandSecurityError(
|
338
|
+
f"Shell operator '{operator}' is not supported. Set ALLOW_SHELL_OPERATORS=true to enable."
|
339
|
+
)
|
340
|
+
|
341
|
+
if use_shell:
|
342
|
+
# For commands with shell operators, execute with shell=True
|
343
|
+
return subprocess.run(
|
344
|
+
command, # command is the full command string in this case
|
345
|
+
shell=True,
|
346
|
+
text=True,
|
347
|
+
capture_output=True,
|
348
|
+
timeout=self.security_config.command_timeout,
|
349
|
+
cwd=self.allowed_dir,
|
350
|
+
)
|
351
|
+
else:
|
352
|
+
# For regular commands, execute with shell=False
|
353
|
+
return subprocess.run(
|
354
|
+
[command] + args,
|
355
|
+
shell=False,
|
356
|
+
text=True,
|
357
|
+
capture_output=True,
|
358
|
+
timeout=self.security_config.command_timeout,
|
359
|
+
cwd=self.allowed_dir,
|
360
|
+
)
|
225
361
|
except subprocess.TimeoutExpired:
|
226
|
-
raise CommandTimeoutError(
|
362
|
+
raise CommandTimeoutError(
|
363
|
+
f"Command timed out after {self.security_config.command_timeout} seconds"
|
364
|
+
)
|
227
365
|
except CommandError:
|
228
366
|
raise
|
229
367
|
except Exception as e:
|
@@ -247,37 +385,55 @@ def load_security_config() -> SecurityConfig:
|
|
247
385
|
- command_timeout: Maximum execution time in seconds
|
248
386
|
- allow_all_commands: Whether all commands are allowed
|
249
387
|
- allow_all_flags: Whether all flags are allowed
|
388
|
+
- allow_shell_operators: Whether shell operators (&&, ||, |, etc.) are allowed
|
250
389
|
|
251
390
|
Environment Variables:
|
252
391
|
ALLOWED_COMMANDS: Comma-separated list of allowed commands or 'all' (default: "ls,cat,pwd")
|
253
392
|
ALLOWED_FLAGS: Comma-separated list of allowed flags or 'all' (default: "-l,-a,--help")
|
254
393
|
MAX_COMMAND_LENGTH: Maximum command string length (default: 1024)
|
255
394
|
COMMAND_TIMEOUT: Command timeout in seconds (default: 30)
|
395
|
+
ALLOW_SHELL_OPERATORS: Whether to allow shell operators like &&, ||, |, >, etc. (default: false)
|
396
|
+
Set to "true" or "1" to enable, any other value to disable.
|
256
397
|
"""
|
257
398
|
allowed_commands = os.getenv("ALLOWED_COMMANDS", "ls,cat,pwd")
|
258
399
|
allowed_flags = os.getenv("ALLOWED_FLAGS", "-l,-a,--help")
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
400
|
+
allow_shell_operators_env = os.getenv("ALLOW_SHELL_OPERATORS", "false")
|
401
|
+
|
402
|
+
allow_all_commands = allowed_commands.lower() == "all"
|
403
|
+
allow_all_flags = allowed_flags.lower() == "all"
|
404
|
+
allow_shell_operators = allow_shell_operators_env.lower() in ("true", "1")
|
405
|
+
|
263
406
|
return SecurityConfig(
|
264
|
-
allowed_commands=
|
407
|
+
allowed_commands=(
|
408
|
+
set() if allow_all_commands else set(allowed_commands.split(","))
|
409
|
+
),
|
265
410
|
allowed_flags=set() if allow_all_flags else set(allowed_flags.split(",")),
|
266
411
|
max_command_length=int(os.getenv("MAX_COMMAND_LENGTH", "1024")),
|
267
412
|
command_timeout=int(os.getenv("COMMAND_TIMEOUT", "30")),
|
268
413
|
allow_all_commands=allow_all_commands,
|
269
414
|
allow_all_flags=allow_all_flags,
|
415
|
+
allow_shell_operators=allow_shell_operators,
|
270
416
|
)
|
271
417
|
|
272
418
|
|
273
|
-
executor = CommandExecutor(
|
419
|
+
executor = CommandExecutor(
|
420
|
+
allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config()
|
421
|
+
)
|
274
422
|
|
275
423
|
|
276
424
|
@server.list_tools()
|
277
425
|
async def handle_list_tools() -> list[types.Tool]:
|
278
|
-
commands_desc =
|
279
|
-
|
280
|
-
|
426
|
+
commands_desc = (
|
427
|
+
"all commands"
|
428
|
+
if executor.security_config.allow_all_commands
|
429
|
+
else ", ".join(executor.security_config.allowed_commands)
|
430
|
+
)
|
431
|
+
flags_desc = (
|
432
|
+
"all flags"
|
433
|
+
if executor.security_config.allow_all_flags
|
434
|
+
else ", ".join(executor.security_config.allowed_flags)
|
435
|
+
)
|
436
|
+
|
281
437
|
return [
|
282
438
|
types.Tool(
|
283
439
|
name="run_command",
|
@@ -285,7 +441,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
285
441
|
f"Allows command (CLI) execution in the directory: {executor.allowed_dir}\n\n"
|
286
442
|
f"Available commands: {commands_desc}\n"
|
287
443
|
f"Available flags: {flags_desc}\n\n"
|
288
|
-
"
|
444
|
+
f"Shell operators (&&, ||, |, >, >>, <, <<, ;) are {'supported' if executor.security_config.allow_shell_operators else 'not supported'}. Set ALLOW_SHELL_OPERATORS=true to enable."
|
289
445
|
),
|
290
446
|
inputSchema={
|
291
447
|
"type": "object",
|
@@ -300,7 +456,9 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
300
456
|
),
|
301
457
|
types.Tool(
|
302
458
|
name="show_security_rules",
|
303
|
-
description=(
|
459
|
+
description=(
|
460
|
+
"Show what commands and operations are allowed in this environment.\n"
|
461
|
+
),
|
304
462
|
inputSchema={
|
305
463
|
"type": "object",
|
306
464
|
"properties": {},
|
@@ -310,10 +468,14 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
310
468
|
|
311
469
|
|
312
470
|
@server.call_tool()
|
313
|
-
async def handle_call_tool(
|
471
|
+
async def handle_call_tool(
|
472
|
+
name: str, arguments: Optional[Dict[str, Any]]
|
473
|
+
) -> List[types.TextContent]:
|
314
474
|
if name == "run_command":
|
315
475
|
if not arguments or "command" not in arguments:
|
316
|
-
return [
|
476
|
+
return [
|
477
|
+
types.TextContent(type="text", text="No command provided", error=True)
|
478
|
+
]
|
317
479
|
|
318
480
|
try:
|
319
481
|
result = executor.execute(arguments["command"])
|
@@ -322,7 +484,9 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
|
|
322
484
|
if result.stdout:
|
323
485
|
response.append(types.TextContent(type="text", text=result.stdout))
|
324
486
|
if result.stderr:
|
325
|
-
response.append(
|
487
|
+
response.append(
|
488
|
+
types.TextContent(type="text", text=result.stderr, error=True)
|
489
|
+
)
|
326
490
|
|
327
491
|
response.append(
|
328
492
|
types.TextContent(
|
@@ -334,7 +498,11 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
|
|
334
498
|
return response
|
335
499
|
|
336
500
|
except CommandSecurityError as e:
|
337
|
-
return [
|
501
|
+
return [
|
502
|
+
types.TextContent(
|
503
|
+
type="text", text=f"Security violation: {str(e)}", error=True
|
504
|
+
)
|
505
|
+
]
|
338
506
|
except subprocess.TimeoutExpired:
|
339
507
|
return [
|
340
508
|
types.TextContent(
|
@@ -347,9 +515,17 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
|
|
347
515
|
return [types.TextContent(type="text", text=f"Error: {str(e)}", error=True)]
|
348
516
|
|
349
517
|
elif name == "show_security_rules":
|
350
|
-
commands_desc =
|
351
|
-
|
352
|
-
|
518
|
+
commands_desc = (
|
519
|
+
"All commands allowed"
|
520
|
+
if executor.security_config.allow_all_commands
|
521
|
+
else ", ".join(sorted(executor.security_config.allowed_commands))
|
522
|
+
)
|
523
|
+
flags_desc = (
|
524
|
+
"All flags allowed"
|
525
|
+
if executor.security_config.allow_all_flags
|
526
|
+
else ", ".join(sorted(executor.security_config.allowed_flags))
|
527
|
+
)
|
528
|
+
|
353
529
|
security_info = (
|
354
530
|
"Security Configuration:\n"
|
355
531
|
f"==================\n"
|
@@ -383,4 +559,4 @@ async def main():
|
|
383
559
|
experimental_capabilities={},
|
384
560
|
),
|
385
561
|
),
|
386
|
-
)
|
562
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cli-mcp-server
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.4
|
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
|
@@ -23,6 +23,7 @@ comprehensive security features.
|
|
23
23
|

|
24
24
|

|
25
25
|
[](https://smithery.ai/protocol/cli-mcp-server)
|
26
|
+
[](https://github.com/MladenSU/cli-mcp-server/actions/workflows/python-tests.yml)
|
26
27
|
|
27
28
|
<a href="https://glama.ai/mcp/servers/q89277vzl1"><img width="380" height="200" src="https://glama.ai/mcp/servers/q89277vzl1/badge" /></a>
|
28
29
|
|
@@ -76,6 +77,7 @@ Configure the server using environment variables:
|
|
76
77
|
| `ALLOWED_FLAGS` | Comma-separated list of allowed flags or 'all' | `-l,-a,--help` |
|
77
78
|
| `MAX_COMMAND_LENGTH`| Maximum command string length | `1024` |
|
78
79
|
| `COMMAND_TIMEOUT` | Command execution timeout (seconds) | `30` |
|
80
|
+
| `ALLOW_SHELL_OPERATORS` | Allow shell operators (&&, \|\|, \|, >, etc.) | `false` |
|
79
81
|
|
80
82
|
Note: Setting `ALLOWED_COMMANDS` or `ALLOWED_FLAGS` to 'all' will allow any command or flag respectively.
|
81
83
|
|
@@ -104,7 +106,7 @@ Executes whitelisted CLI commands within allowed directories.
|
|
104
106
|
```
|
105
107
|
|
106
108
|
**Security Notes:**
|
107
|
-
- Shell operators (&&, |, >, >>) are not supported
|
109
|
+
- Shell operators (&&, |, >, >>) are not supported by default, but can be enabled with `ALLOW_SHELL_OPERATORS=true`
|
108
110
|
- Commands must be whitelisted unless ALLOWED_COMMANDS='all'
|
109
111
|
- Flags must be whitelisted unless ALLOWED_FLAGS='all'
|
110
112
|
- All paths are validated to be within ALLOWED_DIR
|
@@ -139,7 +141,8 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
139
141
|
"ALLOWED_COMMANDS": "ls,cat,pwd,echo",
|
140
142
|
"ALLOWED_FLAGS": "-l,-a,--help,--version",
|
141
143
|
"MAX_COMMAND_LENGTH": "1024",
|
142
|
-
"COMMAND_TIMEOUT": "30"
|
144
|
+
"COMMAND_TIMEOUT": "30",
|
145
|
+
"ALLOW_SHELL_OPERATORS": "false"
|
143
146
|
}
|
144
147
|
}
|
145
148
|
}
|
@@ -161,7 +164,8 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
161
164
|
"ALLOWED_COMMANDS": "ls,cat,pwd,echo",
|
162
165
|
"ALLOWED_FLAGS": "-l,-a,--help,--version",
|
163
166
|
"MAX_COMMAND_LENGTH": "1024",
|
164
|
-
"COMMAND_TIMEOUT": "30"
|
167
|
+
"COMMAND_TIMEOUT": "30",
|
168
|
+
"ALLOW_SHELL_OPERATORS": "false"
|
165
169
|
}
|
166
170
|
}
|
167
171
|
}
|
@@ -174,7 +178,7 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
174
178
|
- ✅ Command whitelist enforcement with 'all' option
|
175
179
|
- ✅ Flag validation with 'all' option
|
176
180
|
- ✅ Path traversal prevention and normalization
|
177
|
-
- ✅ Shell operator blocking
|
181
|
+
- ✅ Shell operator blocking (with opt-in support via `ALLOW_SHELL_OPERATORS=true`)
|
178
182
|
- ✅ Command length limits
|
179
183
|
- ✅ Execution timeouts
|
180
184
|
- ✅ Working directory restrictions
|
@@ -0,0 +1,7 @@
|
|
1
|
+
cli_mcp_server/__init__.py,sha256=bGLiX7XuhVsS-PJdoRIWXiilZ3NTAQ7fb9_8kkNzLlM,216
|
2
|
+
cli_mcp_server/server.py,sha256=MCjBmXutf4CZCy32e67-3tDI_AF2b_9qzq49ZkUR2qM,21288
|
3
|
+
cli_mcp_server-0.2.4.dist-info/METADATA,sha256=eJFjLeByTc-sUtiDlDyN4HB_NtmrgAyNSZ8kxg7TlqE,7860
|
4
|
+
cli_mcp_server-0.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
cli_mcp_server-0.2.4.dist-info/entry_points.txt,sha256=07bDmJJSXg-6VCFEFTOlsGoxI-0faJafT1yEEjUdN-U,55
|
6
|
+
cli_mcp_server-0.2.4.dist-info/licenses/LICENSE,sha256=85rOR_IMpb2VzXBA4VCRZh_KWlaO1Rly8HDYDwGgMWk,1062
|
7
|
+
cli_mcp_server-0.2.4.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=MzehdxGx_EtqVO0-OeZeDpayRK-PDRqD8uaQEVApHks,14564
|
3
|
-
cli_mcp_server-0.2.2.dist-info/METADATA,sha256=Rawjyx3K6EXw-Yc-VzMYT_jq4nlQTHjARzBQWU6ggT8,7371
|
4
|
-
cli_mcp_server-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
-
cli_mcp_server-0.2.2.dist-info/entry_points.txt,sha256=07bDmJJSXg-6VCFEFTOlsGoxI-0faJafT1yEEjUdN-U,55
|
6
|
-
cli_mcp_server-0.2.2.dist-info/licenses/LICENSE,sha256=85rOR_IMpb2VzXBA4VCRZh_KWlaO1Rly8HDYDwGgMWk,1062
|
7
|
-
cli_mcp_server-0.2.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|