cli-mcp-server 0.2.2__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 CHANGED
@@ -62,16 +62,20 @@ class CommandExecutor:
62
62
  """
63
63
  Normalizes a path and ensures it's within allowed directory.
64
64
  """
65
- try:
65
+ try:
66
66
  if os.path.isabs(path):
67
67
  # If absolute path, check directly
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(os.path.realpath(os.path.join(self.allowed_dir, path)))
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(f"Path '{path}' is outside of allowed directory: {self.allowed_dir}")
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(f"Shell operator '{operator}' is not supported")
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,25 +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 not self.security_config.allow_all_commands and command not in self.security_config.allowed_commands:
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 not self.security_config.allow_all_flags and arg not in self.security_config.allowed_flags:
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
- if "/" in arg or "\\" in arg or os.path.isabs(arg) or arg == ".":
140
+ if "/" in arg or "\\" in arg or os.path.isabs(arg) or arg == ".":
129
141
  if self._is_url_path(arg):
130
142
  # If it's a URL, we don't need to normalize it
131
143
  validated_args.append(arg)
132
144
  continue
133
-
145
+
134
146
  normalized_path = self._normalize_path(arg)
135
147
  validated_args.append(normalized_path)
136
148
  else:
@@ -152,9 +164,8 @@ class CommandExecutor:
152
164
  Returns:
153
165
  bool: True if the path is a URL, False otherwise.
154
166
  """
155
- url_pattern = re.compile(r"^(http|https)://")
167
+ url_pattern = re.compile(r"^https?://")
156
168
  return bool(url_pattern.match(path))
157
-
158
169
 
159
170
  def _is_path_safe(self, path: str) -> bool:
160
171
  """
@@ -209,7 +220,9 @@ class CommandExecutor:
209
220
  - Captures both stdout and stderr
210
221
  """
211
222
  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}")
223
+ raise CommandSecurityError(
224
+ f"Command exceeds maximum length of {self.security_config.max_command_length}"
225
+ )
213
226
 
214
227
  try:
215
228
  command, args = self.validate_command(command_string)
@@ -223,7 +236,9 @@ class CommandExecutor:
223
236
  cwd=self.allowed_dir,
224
237
  )
225
238
  except subprocess.TimeoutExpired:
226
- raise CommandTimeoutError(f"Command timed out after {self.security_config.command_timeout} seconds")
239
+ raise CommandTimeoutError(
240
+ f"Command timed out after {self.security_config.command_timeout} seconds"
241
+ )
227
242
  except CommandError:
228
243
  raise
229
244
  except Exception as e:
@@ -256,12 +271,14 @@ def load_security_config() -> SecurityConfig:
256
271
  """
257
272
  allowed_commands = os.getenv("ALLOWED_COMMANDS", "ls,cat,pwd")
258
273
  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
-
274
+
275
+ allow_all_commands = allowed_commands.lower() == "all"
276
+ allow_all_flags = allowed_flags.lower() == "all"
277
+
263
278
  return SecurityConfig(
264
- allowed_commands=set() if allow_all_commands else set(allowed_commands.split(",")),
279
+ allowed_commands=(
280
+ set() if allow_all_commands else set(allowed_commands.split(","))
281
+ ),
265
282
  allowed_flags=set() if allow_all_flags else set(allowed_flags.split(",")),
266
283
  max_command_length=int(os.getenv("MAX_COMMAND_LENGTH", "1024")),
267
284
  command_timeout=int(os.getenv("COMMAND_TIMEOUT", "30")),
@@ -270,14 +287,24 @@ def load_security_config() -> SecurityConfig:
270
287
  )
271
288
 
272
289
 
273
- executor = CommandExecutor(allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config())
290
+ executor = CommandExecutor(
291
+ allowed_dir=os.getenv("ALLOWED_DIR", ""), security_config=load_security_config()
292
+ )
274
293
 
275
294
 
276
295
  @server.list_tools()
277
296
  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
-
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
+
281
308
  return [
282
309
  types.Tool(
283
310
  name="run_command",
@@ -300,7 +327,9 @@ async def handle_list_tools() -> list[types.Tool]:
300
327
  ),
301
328
  types.Tool(
302
329
  name="show_security_rules",
303
- description=("Show what commands and operations are allowed in this environment.\n"),
330
+ description=(
331
+ "Show what commands and operations are allowed in this environment.\n"
332
+ ),
304
333
  inputSchema={
305
334
  "type": "object",
306
335
  "properties": {},
@@ -310,10 +339,14 @@ async def handle_list_tools() -> list[types.Tool]:
310
339
 
311
340
 
312
341
  @server.call_tool()
313
- async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> List[types.TextContent]:
342
+ async def handle_call_tool(
343
+ name: str, arguments: Optional[Dict[str, Any]]
344
+ ) -> List[types.TextContent]:
314
345
  if name == "run_command":
315
346
  if not arguments or "command" not in arguments:
316
- return [types.TextContent(type="text", text="No command provided", error=True)]
347
+ return [
348
+ types.TextContent(type="text", text="No command provided", error=True)
349
+ ]
317
350
 
318
351
  try:
319
352
  result = executor.execute(arguments["command"])
@@ -322,7 +355,9 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
322
355
  if result.stdout:
323
356
  response.append(types.TextContent(type="text", text=result.stdout))
324
357
  if result.stderr:
325
- response.append(types.TextContent(type="text", text=result.stderr, error=True))
358
+ response.append(
359
+ types.TextContent(type="text", text=result.stderr, error=True)
360
+ )
326
361
 
327
362
  response.append(
328
363
  types.TextContent(
@@ -334,7 +369,11 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
334
369
  return response
335
370
 
336
371
  except CommandSecurityError as e:
337
- return [types.TextContent(type="text", text=f"Security violation: {str(e)}", error=True)]
372
+ return [
373
+ types.TextContent(
374
+ type="text", text=f"Security violation: {str(e)}", error=True
375
+ )
376
+ ]
338
377
  except subprocess.TimeoutExpired:
339
378
  return [
340
379
  types.TextContent(
@@ -347,9 +386,17 @@ async def handle_call_tool(name: str, arguments: Optional[Dict[str, Any]]) -> Li
347
386
  return [types.TextContent(type="text", text=f"Error: {str(e)}", error=True)]
348
387
 
349
388
  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
-
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
+
353
400
  security_info = (
354
401
  "Security Configuration:\n"
355
402
  f"==================\n"
@@ -383,4 +430,4 @@ async def main():
383
430
  experimental_capabilities={},
384
431
  ),
385
432
  ),
386
- )
433
+ )
@@ -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.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.1.0
12
+ Requires-Dist: mcp>=1.6.0
13
13
  Description-Content-Type: text/markdown
14
14
 
15
15
  # CLI MCP Server
@@ -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=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,,