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 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(os.path.realpath(os.path.join(self.allowed_dir, path)))
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(f"Path '{path}' is outside of allowed directory: {self.allowed_dir}")
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 for unsupported shell operators and splits it into
87
- command and arguments. Only single commands without shell operators are allowed.
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
- - List of command arguments (List[str])
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 contains unsupported shell operators.
107
+ CommandSecurityError: If any part of the command fails security validation.
99
108
  """
100
109
 
101
- # Check for shell operators that we don't support
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 not self.security_config.allow_all_commands and command not in self.security_config.allowed_commands:
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 not self.security_config.allow_all_flags and arg not in self.security_config.allowed_flags:
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 _is_url_path(self, path: str) -> bool:
146
- """
147
- Checks if a given path is a URL of type http or https.
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
- url_pattern = re.compile(r"^(http|https)://")
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
- Validates that the absolute resolved path is within the allowed directory
164
- to prevent directory traversal attacks.
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
- path (str): The path to validate.
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
- bool: True if path is within allowed directory, False otherwise.
171
- Returns False if path resolution fails for any reason.
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
- Private method intended for internal use only.
249
+ Raises:
250
+ CommandSecurityError: If any part of the command fails validation.
174
251
  """
175
- try:
176
- # Resolve any symlinks and get absolute path
177
- real_path = os.path.abspath(os.path.realpath(path))
178
- allowed_dir_real = os.path.abspath(os.path.realpath(self.allowed_dir))
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
- # Check if the path starts with allowed_dir
181
- return real_path.startswith(allowed_dir_real)
182
- except Exception:
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
- - Executes with shell=False for security
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(f"Command exceeds maximum length of {self.security_config.max_command_length}")
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
- return subprocess.run(
218
- [command] + args,
219
- shell=False,
220
- text=True,
221
- capture_output=True,
222
- timeout=self.security_config.command_timeout,
223
- cwd=self.allowed_dir,
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(f"Command timed out after {self.security_config.command_timeout} seconds")
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
- allow_all_commands = allowed_commands.lower() == 'all'
261
- allow_all_flags = allowed_flags.lower() == 'all'
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=set() if allow_all_commands else set(allowed_commands.split(",")),
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(allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config())
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 = "all commands" if executor.security_config.allow_all_commands else ", ".join(executor.security_config.allowed_commands)
279
- flags_desc = "all flags" if executor.security_config.allow_all_flags else ", ".join(executor.security_config.allowed_flags)
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
- "Note: Shell operators (&&, |, >, >>) are not supported."
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=("Show what commands and operations are allowed in this environment.\n"),
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(name: str, arguments: Optional[Dict[str, Any]]) -> List[types.TextContent]:
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 [types.TextContent(type="text", text="No command provided", error=True)]
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(types.TextContent(type="text", text=result.stderr, error=True))
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 [types.TextContent(type="text", text=f"Security violation: {str(e)}", error=True)]
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 = "All commands allowed" if executor.security_config.allow_all_commands else ", ".join(sorted(executor.security_config.allowed_commands))
351
- flags_desc = "All flags allowed" if executor.security_config.allow_all_flags else ", ".join(sorted(executor.security_config.allowed_flags))
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.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.1.0
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
  ![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)
24
24
  ![MCP Protocol](https://img.shields.io/badge/MCP-Compatible-green)
25
25
  [![smithery badge](https://smithery.ai/badge/cli-mcp-server)](https://smithery.ai/protocol/cli-mcp-server)
26
+ [![Python Tests](https://github.com/MladenSU/cli-mcp-server/actions/workflows/python-tests.yml/badge.svg)](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,,