cli-mcp-server 0.2.3__tar.gz → 0.2.5__tar.gz
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-0.2.3 → cli_mcp_server-0.2.5}/PKG-INFO +10 -6
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/README.md +8 -4
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/pyproject.toml +2 -2
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/src/cli_mcp_server/server.py +183 -53
- cli_mcp_server-0.2.5/tests/test_cli_mcp_server.py +224 -0
- cli_mcp_server-0.2.5/uv.lock +532 -0
- cli_mcp_server-0.2.3/tests/test_cli_mcp_server.py +0 -98
- cli_mcp_server-0.2.3/uv.lock +0 -345
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/.github/workflows/python-tests.yml +0 -0
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/.gitignore +0 -0
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/.python-version +0 -0
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/LICENSE +0 -0
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/glama.json +0 -0
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/src/cli_mcp_server/__init__.py +0 -0
- {cli_mcp_server-0.2.3 → cli_mcp_server-0.2.5}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cli-mcp-server
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.5
|
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.10.1
|
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
|
@@ -9,6 +9,7 @@ comprehensive security features.
|
|
9
9
|

|
10
10
|

|
11
11
|
[](https://smithery.ai/protocol/cli-mcp-server)
|
12
|
+
[](https://github.com/MladenSU/cli-mcp-server/actions/workflows/python-tests.yml)
|
12
13
|
|
13
14
|
<a href="https://glama.ai/mcp/servers/q89277vzl1"><img width="380" height="200" src="https://glama.ai/mcp/servers/q89277vzl1/badge" /></a>
|
14
15
|
|
@@ -62,6 +63,7 @@ Configure the server using environment variables:
|
|
62
63
|
| `ALLOWED_FLAGS` | Comma-separated list of allowed flags or 'all' | `-l,-a,--help` |
|
63
64
|
| `MAX_COMMAND_LENGTH`| Maximum command string length | `1024` |
|
64
65
|
| `COMMAND_TIMEOUT` | Command execution timeout (seconds) | `30` |
|
66
|
+
| `ALLOW_SHELL_OPERATORS` | Allow shell operators (&&, \|\|, \|, >, etc.) | `false` |
|
65
67
|
|
66
68
|
Note: Setting `ALLOWED_COMMANDS` or `ALLOWED_FLAGS` to 'all' will allow any command or flag respectively.
|
67
69
|
|
@@ -90,7 +92,7 @@ Executes whitelisted CLI commands within allowed directories.
|
|
90
92
|
```
|
91
93
|
|
92
94
|
**Security Notes:**
|
93
|
-
- Shell operators (&&, |, >, >>) are not supported
|
95
|
+
- Shell operators (&&, |, >, >>) are not supported by default, but can be enabled with `ALLOW_SHELL_OPERATORS=true`
|
94
96
|
- Commands must be whitelisted unless ALLOWED_COMMANDS='all'
|
95
97
|
- Flags must be whitelisted unless ALLOWED_FLAGS='all'
|
96
98
|
- All paths are validated to be within ALLOWED_DIR
|
@@ -125,7 +127,8 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
125
127
|
"ALLOWED_COMMANDS": "ls,cat,pwd,echo",
|
126
128
|
"ALLOWED_FLAGS": "-l,-a,--help,--version",
|
127
129
|
"MAX_COMMAND_LENGTH": "1024",
|
128
|
-
"COMMAND_TIMEOUT": "30"
|
130
|
+
"COMMAND_TIMEOUT": "30",
|
131
|
+
"ALLOW_SHELL_OPERATORS": "false"
|
129
132
|
}
|
130
133
|
}
|
131
134
|
}
|
@@ -147,7 +150,8 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
147
150
|
"ALLOWED_COMMANDS": "ls,cat,pwd,echo",
|
148
151
|
"ALLOWED_FLAGS": "-l,-a,--help,--version",
|
149
152
|
"MAX_COMMAND_LENGTH": "1024",
|
150
|
-
"COMMAND_TIMEOUT": "30"
|
153
|
+
"COMMAND_TIMEOUT": "30",
|
154
|
+
"ALLOW_SHELL_OPERATORS": "false"
|
151
155
|
}
|
152
156
|
}
|
153
157
|
}
|
@@ -160,7 +164,7 @@ Add to your `~/Library/Application\ Support/Claude/claude_desktop_config.json`:
|
|
160
164
|
- ✅ Command whitelist enforcement with 'all' option
|
161
165
|
- ✅ Flag validation with 'all' option
|
162
166
|
- ✅ Path traversal prevention and normalization
|
163
|
-
- ✅ Shell operator blocking
|
167
|
+
- ✅ Shell operator blocking (with opt-in support via `ALLOW_SHELL_OPERATORS=true`)
|
164
168
|
- ✅ Command length limits
|
165
169
|
- ✅ Execution timeouts
|
166
170
|
- ✅ Working directory restrictions
|
@@ -1,10 +1,10 @@
|
|
1
1
|
[project]
|
2
2
|
name = "cli-mcp-server"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.5"
|
4
4
|
description = "Command line interface for MCP clients with secure execution and customizable security policies"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
7
|
-
dependencies = ["mcp>=1.
|
7
|
+
dependencies = ["mcp>=1.10.1"]
|
8
8
|
authors = [
|
9
9
|
{ name = "Mladen", email = "fangs-lever6n@icloud.com" },
|
10
10
|
]
|
@@ -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:
|
@@ -87,29 +88,103 @@ class CommandExecutor:
|
|
87
88
|
"""
|
88
89
|
Validates and parses a command string for security and formatting.
|
89
90
|
|
90
|
-
Checks the command string
|
91
|
-
|
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.
|
92
97
|
|
93
98
|
Args:
|
94
99
|
command_string (str): The command string to validate and parse.
|
95
100
|
|
96
101
|
Returns:
|
97
102
|
tuple[str, List[str]]: A tuple containing:
|
98
|
-
- The command name (str)
|
99
|
-
-
|
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
|
100
105
|
|
101
106
|
Raises:
|
102
|
-
CommandSecurityError: If the command
|
107
|
+
CommandSecurityError: If any part of the command fails security validation.
|
103
108
|
"""
|
104
109
|
|
105
|
-
#
|
110
|
+
# Define shell operators
|
106
111
|
shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
|
107
|
-
for operator in shell_operators:
|
108
|
-
if operator in command_string:
|
109
|
-
raise CommandSecurityError(
|
110
|
-
f"Shell operator '{operator}' is not supported"
|
111
|
-
)
|
112
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
|
+
"""
|
113
188
|
try:
|
114
189
|
parts = shlex.split(command_string)
|
115
190
|
if not parts:
|
@@ -127,6 +202,8 @@ class CommandExecutor:
|
|
127
202
|
# Process and validate arguments
|
128
203
|
validated_args = []
|
129
204
|
for arg in args:
|
205
|
+
is_explicit_path = (arg.startswith(("./", "../", "/")) and not arg.startswith("//")) or arg == "."
|
206
|
+
|
130
207
|
if arg.startswith("-"):
|
131
208
|
if (
|
132
209
|
not self.security_config.allow_all_flags
|
@@ -135,9 +212,8 @@ class CommandExecutor:
|
|
135
212
|
raise CommandSecurityError(f"Flag '{arg}' is not allowed")
|
136
213
|
validated_args.append(arg)
|
137
214
|
continue
|
138
|
-
|
139
215
|
# For any path-like argument, validate it
|
140
|
-
if
|
216
|
+
if is_explicit_path or ("/" in arg and os.path.exists(os.path.join(self.allowed_dir, arg))):
|
141
217
|
if self._is_url_path(arg):
|
142
218
|
# If it's a URL, we don't need to normalize it
|
143
219
|
validated_args.append(arg)
|
@@ -154,44 +230,69 @@ class CommandExecutor:
|
|
154
230
|
except ValueError as e:
|
155
231
|
raise CommandSecurityError(f"Invalid command format: {str(e)}")
|
156
232
|
|
157
|
-
def
|
158
|
-
|
159
|
-
|
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
|
-
|
170
|
-
def _is_path_safe(self, path: str) -> bool:
|
233
|
+
def _validate_command_with_operators(
|
234
|
+
self, command_string: str, shell_operators: List[str]
|
235
|
+
) -> tuple[str, List[str]]:
|
171
236
|
"""
|
172
|
-
|
237
|
+
Validates a command string that contains shell operators.
|
173
238
|
|
174
|
-
|
175
|
-
to
|
239
|
+
Splits the command string by shell operators and validates each part individually.
|
240
|
+
If all parts are valid, returns the original command to be executed with shell=True.
|
176
241
|
|
177
242
|
Args:
|
178
|
-
|
243
|
+
command_string (str): The command string containing shell operators.
|
244
|
+
shell_operators (List[str]): List of shell operators to split by.
|
179
245
|
|
180
246
|
Returns:
|
181
|
-
|
182
|
-
|
247
|
+
tuple[str, List[str]]: A tuple containing the command and empty args list
|
248
|
+
(since the command will be executed with shell=True)
|
183
249
|
|
184
|
-
|
250
|
+
Raises:
|
251
|
+
CommandSecurityError: If any part of the command fails validation.
|
185
252
|
"""
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
253
|
+
# Create a regex pattern to split by any of the shell operators
|
254
|
+
# We need to escape special regex characters in the operators
|
255
|
+
escaped_operators = [re.escape(op) for op in shell_operators]
|
256
|
+
pattern = "|".join(escaped_operators)
|
257
|
+
|
258
|
+
# Split the command string by shell operators, keeping the operators
|
259
|
+
parts = re.split(f"({pattern})", command_string)
|
260
|
+
|
261
|
+
# Filter out empty parts and whitespace-only parts
|
262
|
+
parts = [part.strip() for part in parts if part.strip()]
|
263
|
+
|
264
|
+
# Group commands and operators
|
265
|
+
commands = []
|
266
|
+
i = 0
|
267
|
+
while i < len(parts):
|
268
|
+
if i + 1 < len(parts) and parts[i + 1] in shell_operators:
|
269
|
+
# If next part is an operator, current part is a command
|
270
|
+
if parts[i]: # Skip empty commands
|
271
|
+
commands.append(parts[i])
|
272
|
+
i += 2 # Skip the operator
|
273
|
+
else:
|
274
|
+
# If no operator follows, this is the last command
|
275
|
+
if (
|
276
|
+
parts[i] and parts[i] not in shell_operators
|
277
|
+
): # Skip if it's an operator
|
278
|
+
commands.append(parts[i])
|
279
|
+
i += 1
|
280
|
+
|
281
|
+
# Validate each command individually
|
282
|
+
for cmd in commands:
|
283
|
+
try:
|
284
|
+
# Use the extracted validation method for each command
|
285
|
+
self._validate_single_command(cmd)
|
286
|
+
except CommandSecurityError as e:
|
287
|
+
raise CommandSecurityError(f"Invalid command part '{cmd}': {str(e)}")
|
288
|
+
except ValueError as e:
|
289
|
+
raise CommandSecurityError(
|
290
|
+
f"Invalid command format in '{cmd}': {str(e)}"
|
291
|
+
)
|
190
292
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
return False
|
293
|
+
# If we get here, all commands passed validation
|
294
|
+
# Return the original command string to be executed with shell=True
|
295
|
+
return command_string, []
|
195
296
|
|
196
297
|
def execute(self, command_string: str) -> subprocess.CompletedProcess:
|
197
298
|
"""
|
@@ -210,12 +311,11 @@ class CommandExecutor:
|
|
210
311
|
Raises:
|
211
312
|
CommandSecurityError: If the command:
|
212
313
|
- Exceeds maximum length
|
213
|
-
- Contains invalid shell operators
|
214
314
|
- Fails security validation
|
215
315
|
- Fails during execution
|
216
316
|
|
217
317
|
Notes:
|
218
|
-
-
|
318
|
+
- Uses shell=True for commands with shell operators, shell=False otherwise
|
219
319
|
- Uses timeout and working directory constraints
|
220
320
|
- Captures both stdout and stderr
|
221
321
|
"""
|
@@ -227,14 +327,38 @@ class CommandExecutor:
|
|
227
327
|
try:
|
228
328
|
command, args = self.validate_command(command_string)
|
229
329
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
330
|
+
# Check if this is a command with shell operators
|
331
|
+
shell_operators = ["&&", "||", "|", ">", ">>", "<", "<<", ";"]
|
332
|
+
use_shell = any(operator in command_string for operator in shell_operators)
|
333
|
+
|
334
|
+
# Double-check that shell operators are allowed if they are present
|
335
|
+
if use_shell and not self.security_config.allow_shell_operators:
|
336
|
+
for operator in shell_operators:
|
337
|
+
if operator in command_string:
|
338
|
+
raise CommandSecurityError(
|
339
|
+
f"Shell operator '{operator}' is not supported. Set ALLOW_SHELL_OPERATORS=true to enable."
|
340
|
+
)
|
341
|
+
|
342
|
+
if use_shell:
|
343
|
+
# For commands with shell operators, execute with shell=True
|
344
|
+
return subprocess.run(
|
345
|
+
command, # command is the full command string in this case
|
346
|
+
shell=True,
|
347
|
+
text=True,
|
348
|
+
capture_output=True,
|
349
|
+
timeout=self.security_config.command_timeout,
|
350
|
+
cwd=self.allowed_dir,
|
351
|
+
)
|
352
|
+
else:
|
353
|
+
# For regular commands, execute with shell=False
|
354
|
+
return subprocess.run(
|
355
|
+
[command] + args,
|
356
|
+
shell=False,
|
357
|
+
text=True,
|
358
|
+
capture_output=True,
|
359
|
+
timeout=self.security_config.command_timeout,
|
360
|
+
cwd=self.allowed_dir,
|
361
|
+
)
|
238
362
|
except subprocess.TimeoutExpired:
|
239
363
|
raise CommandTimeoutError(
|
240
364
|
f"Command timed out after {self.security_config.command_timeout} seconds"
|
@@ -262,18 +386,23 @@ def load_security_config() -> SecurityConfig:
|
|
262
386
|
- command_timeout: Maximum execution time in seconds
|
263
387
|
- allow_all_commands: Whether all commands are allowed
|
264
388
|
- allow_all_flags: Whether all flags are allowed
|
389
|
+
- allow_shell_operators: Whether shell operators (&&, ||, |, etc.) are allowed
|
265
390
|
|
266
391
|
Environment Variables:
|
267
392
|
ALLOWED_COMMANDS: Comma-separated list of allowed commands or 'all' (default: "ls,cat,pwd")
|
268
393
|
ALLOWED_FLAGS: Comma-separated list of allowed flags or 'all' (default: "-l,-a,--help")
|
269
394
|
MAX_COMMAND_LENGTH: Maximum command string length (default: 1024)
|
270
395
|
COMMAND_TIMEOUT: Command timeout in seconds (default: 30)
|
396
|
+
ALLOW_SHELL_OPERATORS: Whether to allow shell operators like &&, ||, |, >, etc. (default: false)
|
397
|
+
Set to "true" or "1" to enable, any other value to disable.
|
271
398
|
"""
|
272
399
|
allowed_commands = os.getenv("ALLOWED_COMMANDS", "ls,cat,pwd")
|
273
400
|
allowed_flags = os.getenv("ALLOWED_FLAGS", "-l,-a,--help")
|
401
|
+
allow_shell_operators_env = os.getenv("ALLOW_SHELL_OPERATORS", "false")
|
274
402
|
|
275
403
|
allow_all_commands = allowed_commands.lower() == "all"
|
276
404
|
allow_all_flags = allowed_flags.lower() == "all"
|
405
|
+
allow_shell_operators = allow_shell_operators_env.lower() in ("true", "1")
|
277
406
|
|
278
407
|
return SecurityConfig(
|
279
408
|
allowed_commands=(
|
@@ -284,6 +413,7 @@ def load_security_config() -> SecurityConfig:
|
|
284
413
|
command_timeout=int(os.getenv("COMMAND_TIMEOUT", "30")),
|
285
414
|
allow_all_commands=allow_all_commands,
|
286
415
|
allow_all_flags=allow_all_flags,
|
416
|
+
allow_shell_operators=allow_shell_operators,
|
287
417
|
)
|
288
418
|
|
289
419
|
|
@@ -312,7 +442,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
312
442
|
f"Allows command (CLI) execution in the directory: {executor.allowed_dir}\n\n"
|
313
443
|
f"Available commands: {commands_desc}\n"
|
314
444
|
f"Available flags: {flags_desc}\n\n"
|
315
|
-
"
|
445
|
+
f"Shell operators (&&, ||, |, >, >>, <, <<, ;) are {'supported' if executor.security_config.allow_shell_operators else 'not supported'}. Set ALLOW_SHELL_OPERATORS=true to enable."
|
316
446
|
),
|
317
447
|
inputSchema={
|
318
448
|
"type": "object",
|