Coverage for mcp/cli_runner.py: 100%

22 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 15:07 +0000

1""" 

2CLI runner for the GCO MCP server. 

3 

4Provides ``_run_cli()`` which shells out to the ``gco`` CLI with 

5``--output json`` and returns the result. All arguments are passed as 

6separate list elements (shell=False) to prevent command injection. 

7""" 

8 

9import json 

10import subprocess 

11from pathlib import Path 

12 

13PROJECT_ROOT = Path(__file__).parent.parent 

14 

15 

16def _run_cli(*args: str) -> str: 

17 """Run a gco CLI command and return its output. 

18 

19 All args are passed as separate list elements to subprocess (shell=False), 

20 so shell metacharacters in user-provided values are treated as literals 

21 and cannot cause command injection. Path arguments are validated to prevent 

22 traversal outside the project root. 

23 """ 

24 # Validate any path-like arguments to prevent directory traversal. 

25 for arg in args: 

26 if arg.startswith("-"): 

27 continue # flag, not a path 

28 if ".." in arg.split("/"): 

29 return json.dumps({"error": f"Invalid argument: path traversal not allowed: {arg}"}) 

30 

31 cmd = ["gco", "--output", "json", *args] 

32 try: 

33 result = subprocess.run( # nosemgrep: dangerous-subprocess-use-audit - shell=False; args are validated above and passed as literal argv elements 

34 cmd, 

35 capture_output=True, 

36 text=True, 

37 timeout=120, 

38 cwd=str(PROJECT_ROOT), 

39 ) 

40 output = result.stdout.strip() 

41 if result.returncode != 0: 

42 error = result.stderr.strip() or output 

43 return json.dumps({"error": error, "exit_code": result.returncode}) 

44 return output if output else json.dumps({"status": "ok"}) 

45 except subprocess.TimeoutExpired: 

46 return json.dumps({"error": "Command timed out after 120 seconds"}) 

47 except FileNotFoundError: 

48 return json.dumps({"error": "gco CLI not found. Install with: pipx install -e ."})