From edbd717b78f02c2bf76546d7a026746a033b2ef7 Mon Sep 17 00:00:00 2001 From: Yurii Bakurov <45154988+Yurii201811@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:29:35 +0200 Subject: [PATCH 1/3] docs(cli): clarify config static scan semantics --- CHANGELOG.md | 4 +++ README.md | 20 +++++++++++++++ docs/platform/cli.md | 13 +++++++--- src/mcts/cli/main.py | 18 ++++++++++--- tests/test_cli_report.py | 55 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c81d031..03f1558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Clarify that `scan --config ... --server ...` is static by default, does not execute launch arguments, and needs `--live --i-understand-live-risk` for per-server runtime analysis. + ## [0.1.4] - 2026-06-12 ### Security diff --git a/README.md b/README.md index 014ab0e..ff45329 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,26 @@ uv run mcts scan ./server.py --theme cyber # default uv run mcts scan ./server.py --theme minimal --no-progress ``` +### Config-based scan: static versus live + +Selecting a server from an MCP client config is static by default. MCTS analyzes +the repository files but does not execute the selected entry's command or launch +arguments, so multiple config entries that point at the same source may receive +the same score: + +```bash +mcts scan . --config ~/.cursor/mcp.json --server ifd-prod +``` + +Add live mode when the launch arguments or runtime-exposed schemas differ by +server. Live mode starts the selected process and therefore requires explicit +consent: + +```bash +mcts scan . --config ~/.cursor/mcp.json --server ifd-prod \ + --live --i-understand-live-risk +``` + ## Architecture ``` diff --git a/docs/platform/cli.md b/docs/platform/cli.md index 407af2e..b88edbe 100644 --- a/docs/platform/cli.md +++ b/docs/platform/cli.md @@ -111,13 +111,13 @@ Valid **legacy** category keys: `permissions`, `injection`, `execution`, `data_l | Flag | Default | Description | |------|---------|-------------| | `--languages` | `python,typescript` | Comma-separated static discovery backends | -| `--live` | false | Connect to live stdio MCP server | +| `--live` | false | Execute and probe a live stdio MCP server; with `--config`, use its command and args | | `--url` | — | Remote MCP URL (streamable HTTP or SSE); implies live | | `--transport` | `streamable-http` | Remote transport: `streamable-http` or `sse` | | `--command` | — | Custom launch binary for live mode | | `--args` | — | Comma-separated args for `--command` | -| `--config` | — | MCP client config JSON path (JSON5/comments supported) | -| `--server` | — | Server name inside `mcpServers` (requires `--config`) | +| `--config` | — | MCP client config JSON path (JSON5/comments supported); static mode does not execute launch args | +| `--server` | — | Server name inside `mcpServers` (requires `--config`); add `--live` for runtime analysis | | `--expand-vars` | `auto` | Expand `$VAR` / `%VAR%` in config commands: `auto`, `linux`, `mac`, `windows`, `off` | | `--snapshot` | — | Static JSON snapshot (`tools/list` export); no live connection | | `--surfaces` | all four | Comma-separated: `tool`, `prompt`, `resource`, `instruction` | @@ -130,6 +130,13 @@ Valid **legacy** category keys: `permissions`, `injection`, `execution`, `data_l | `--i-understand-live-risk` | false | Consent for live/remote probe (or `MCTS_LIVE_OK=1`) | | `--stderr-file` | — | Capture live server stderr to file | +With `--config` and `--server` but without `--live`, MCTS uses the config as +metadata and scans the target files. It does not start the configured command or +interpret argument-dependent behavior. Config entries that point at the same +source can therefore produce identical static scores. Add `--live` and +`--i-understand-live-risk` to execute the selected entry and inspect its runtime +MCP surfaces. + ### Remote auth flags | Flag | Description | diff --git a/src/mcts/cli/main.py b/src/mcts/cli/main.py index 8c92d7c..68d9071 100644 --- a/src/mcts/cli/main.py +++ b/src/mcts/cli/main.py @@ -274,7 +274,13 @@ def scan( ] = "json", live: Annotated[ bool, - typer.Option("--live", help="Connect to a live stdio MCP server (requires consent)"), + typer.Option( + "--live", + help=( + "Execute and probe a live stdio MCP server; with --config, uses its " + "command and args (requires consent)" + ), + ), ] = False, command: Annotated[ str | None, @@ -286,11 +292,17 @@ def scan( ] = None, config: Annotated[ Path | None, - typer.Option("--config", help="MCP client config JSON (Cursor, Claude, VS Code)"), + typer.Option( + "--config", + help=("MCP client config JSON; static mode reads metadata only and does not execute launch args"), + ), ] = None, server: Annotated[ str | None, - typer.Option("--server", help="Server name inside --config mcpServers"), + typer.Option( + "--server", + help="Server name inside --config; add --live for per-server runtime analysis", + ), ] = None, understand_live_risk: Annotated[ bool, diff --git a/tests/test_cli_report.py b/tests/test_cli_report.py index 03c36da..ed6d573 100644 --- a/tests/test_cli_report.py +++ b/tests/test_cli_report.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from pathlib import Path from typer.testing import CliRunner @@ -74,6 +75,60 @@ def test_scan_scoring_both_prints_v2_summary(example_server_path: Path, tmp_path assert "absolute_risk" in result.stdout.lower() or "Absolute Risk" in result.stdout +def test_scan_help_explains_config_static_vs_live() -> None: + result = runner.invoke(app, ["scan", "--help"]) + output = " ".join(result.stdout.replace("│", " ").split()) + + assert result.exit_code == 0 + assert "static mode reads metadata only and does not execute launch args" in output + assert "add --live for per-server runtime analysis" in output + assert "with --config, uses its command and args" in output + + +def test_config_static_scan_warns_in_console_and_json(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + config = tmp_path / ".mcp.json" + config.write_text( + json.dumps( + { + "mcpServers": { + "prod": { + "command": "definitely-not-a-real-command", + "args": ["--sso-env", "prod"], + } + } + } + ) + ) + (tmp_path / "app.py").write_text("x = 1\n") + output_path = tmp_path / "scan-report.json" + + result = runner.invoke( + app, + [ + "scan", + str(tmp_path), + "--config", + str(config), + "--server", + "prod", + "--no-progress", + "--output", + str(output_path), + ], + ) + console_output = " ".join(result.stdout.split()) + + assert result.exit_code == 0, result.stdout + assert "did not execute the server command or args" in console_output + assert "All config servers may share the same score until --live is used" in console_output + + payload = json.loads(output_path.read_text()) + scan_notes = " ".join(payload["scan_notes"]) + assert "did not execute the server command or args" in scan_notes + assert "server=prod" in scan_notes + + def test_report_valid_json(tmp_path: Path) -> None: report_path = tmp_path / "report.json" report_path.write_text(_minimal_report().model_dump_json()) From 32840e4afe5fc706a6851a0f7348e6f280356602 Mon Sep 17 00:00:00 2001 From: Yurii Bakurov <45154988+Yurii201811@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:43:00 +0200 Subject: [PATCH 2/3] test(cli): stabilize help output in CI --- tests/test_cli_report.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_cli_report.py b/tests/test_cli_report.py index ed6d573..297f8e9 100644 --- a/tests/test_cli_report.py +++ b/tests/test_cli_report.py @@ -76,7 +76,12 @@ def test_scan_scoring_both_prints_v2_summary(example_server_path: Path, tmp_path def test_scan_help_explains_config_static_vs_live() -> None: - result = runner.invoke(app, ["scan", "--help"]) + result = runner.invoke( + app, + ["scan", "--help"], + color=False, + terminal_width=240, + ) output = " ".join(result.stdout.replace("│", " ").split()) assert result.exit_code == 0 From c629756aefa161730d6ce20e91cb96df13c633dd Mon Sep 17 00:00:00 2001 From: Yurii Bakurov <45154988+Yurii201811@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:45:15 +0200 Subject: [PATCH 3/3] test(cli): assert help metadata directly --- tests/test_cli_report.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/test_cli_report.py b/tests/test_cli_report.py index 297f8e9..86b6769 100644 --- a/tests/test_cli_report.py +++ b/tests/test_cli_report.py @@ -5,6 +5,8 @@ import json from pathlib import Path +from typer.core import TyperGroup, TyperOption +from typer.main import get_command from typer.testing import CliRunner from mcts.cli.main import app @@ -76,18 +78,21 @@ def test_scan_scoring_both_prints_v2_summary(example_server_path: Path, tmp_path def test_scan_help_explains_config_static_vs_live() -> None: - result = runner.invoke( - app, - ["scan", "--help"], - color=False, - terminal_width=240, - ) - output = " ".join(result.stdout.replace("│", " ").split()) + root_command = get_command(app) + assert isinstance(root_command, TyperGroup) + scan_command = root_command.commands["scan"] + help_by_name = {param.name: param.help for param in scan_command.params if isinstance(param, TyperOption)} - assert result.exit_code == 0 - assert "static mode reads metadata only and does not execute launch args" in output - assert "add --live for per-server runtime analysis" in output - assert "with --config, uses its command and args" in output + assert help_by_name["config"] == ( + "MCP client config JSON; static mode reads metadata only and does not execute launch args" + ) + assert help_by_name["server"] == ( + "Server name inside --config; add --live for per-server runtime analysis" + ) + assert help_by_name["live"] == ( + "Execute and probe a live stdio MCP server; with --config, " + "uses its command and args (requires consent)" + ) def test_config_static_scan_warns_in_console_and_json(tmp_path: Path, monkeypatch) -> None: