Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- `mcts doctor` now reports the optional `[api]` extra on default runs, and `mcts serve` points missing REST API dependencies back to doctor preflight checks (#218).

## [0.1.4] - 2026-06-12

### Security
Expand Down
1 change: 1 addition & 0 deletions docs/platform/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Complete reference for every MCTS command and flag. Use this when you need to lo
## `mcts doctor`

Read-only preflight checks before your first scan (no live probes).
Reports optional extras for live MCP features (`[mcp]`) and the REST API (`[api]`).

```bash
mcts doctor .
Expand Down
36 changes: 28 additions & 8 deletions src/mcts/cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

console = Console()

_OPTIONAL_EXTRA_CHECKS = (("[api] extra", "fastapi", "mcp-mcts[api]"),)
_OPTIONAL_CLI_CHECKS = (
("semgrep CLI", "semgrep"),
("pip-audit CLI", "pip-audit"),
Expand Down Expand Up @@ -63,6 +62,8 @@ def run_doctor(
missing_detail='missing — install with `pip install "mcp-mcts[mcp]"` or `uv sync --extra mcp`',
):
warnings += 1
if _append_api_extra_check(checks):
warnings += 1

if root.is_dir():
checks.append(("pass", "Target", str(root)))
Expand Down Expand Up @@ -204,13 +205,6 @@ def _deep_import_check(config_path: Path, server_name: str, checks: list[tuple[s

def _check_optional_toolchain(checks: list[tuple[str, str, str]]) -> int:
warnings = 0
for label, module, extra in _OPTIONAL_EXTRA_CHECKS:
if importlib_util.find_spec(module):
checks.append(("pass", label, f"module {module!r} importable"))
else:
checks.append(("warn", label, f"module {module!r} not found; install {extra} to enable"))
warnings += 1

for label, executable in _OPTIONAL_CLI_CHECKS:
found = shutil.which(executable)
if found:
Expand Down Expand Up @@ -248,3 +242,29 @@ def _append_optional_extra_check(

checks.append(("pass", extra_label, available_detail))
return False


def _append_api_extra_check(checks: list[tuple[str, str, str]]) -> bool:
modules = ("fastapi", "uvicorn")
missing = [module for module in modules if importlib_util.find_spec(module) is None]
if missing:
quoted = ", ".join(f"{module!r}" for module in missing)
noun = "module" if len(missing) == 1 else "modules"
checks.append(
(
"warn",
"[api] extra",
f"{noun} {quoted} not found; install `mcp-mcts[api]` "
"or run `uv sync --extra api` to enable mcts serve",
)
)
return True

checks.append(
(
"pass",
"[api] extra",
"modules 'fastapi', 'uvicorn' importable — mcts serve available",
)
)
return False
1 change: 1 addition & 0 deletions src/mcts/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@ def serve_api(
import uvicorn
except ImportError as exc:
console.print("[red]REST API requires optional api extra: uv sync --extra api[/red]")
console.print("[dim]Run `mcts doctor .` to check optional extras before serving.[/dim]")
raise typer.Exit(code=2) from exc
from mcts.api.app import app as api_app
from mcts.api.startup import validate_serve_options
Expand Down
36 changes: 34 additions & 2 deletions tests/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import builtins
import json
from importlib.machinery import ModuleSpec
from pathlib import Path
Expand Down Expand Up @@ -84,7 +85,7 @@ def test_doctor_deep_missing_optional_tools_show_warnings(monkeypatch, tmp_path:

assert result.exit_code == 0
assert "Extra [mcp]: missing" in result.stdout
assert "[api] extra: module 'fastapi' not found" in result.stdout
assert "[api] extra: modules 'fastapi', 'uvicorn' not found" in result.stdout
assert "semgrep CLI: not found on PATH" in result.stdout
assert "pip-audit CLI: not found on PATH" in result.stdout
assert "opa CLI: not found on PATH" in result.stdout
Expand All @@ -100,7 +101,7 @@ def test_doctor_deep_present_optional_tools_show_pass_lines(monkeypatch, tmp_pat

assert result.exit_code == 0
assert "Extra [mcp]: installed" in result.stdout
assert "[api] extra: module 'fastapi' importable" in result.stdout
assert "[api] extra: modules 'fastapi', 'uvicorn' importable" in result.stdout
assert "semgrep CLI: found at C:\\tools\\semgrep.exe" in result.stdout
assert "pip-audit CLI: found at C:\\tools\\pip-audit.exe" in result.stdout
assert "opa CLI: found at C:\\tools\\opa.exe" in result.stdout
Expand All @@ -123,6 +124,37 @@ def test_doctor_deep_missing_optional_extras_do_not_fail_core_only_install(
assert "[api] extra: module 'fastapi' not found" in result.stdout


def test_doctor_default_reports_api_extra_status(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setattr(
doctor_module.importlib_util,
"find_spec",
lambda module: None if module in {"fastapi", "uvicorn"} else SimpleNamespace(),
)

result = runner.invoke(app, ["doctor", str(tmp_path)])

assert result.exit_code == 0
assert "[api] extra: modules 'fastapi', 'uvicorn' not found" in result.stdout
assert "mcts serve" in result.stdout


def test_serve_missing_api_extra_references_doctor(monkeypatch) -> None:
real_import = builtins.__import__

def fail_uvicorn(name, *args, **kwargs):
if name == "uvicorn":
raise ImportError("blocked for test")
return real_import(name, *args, **kwargs)

monkeypatch.setattr(builtins, "__import__", fail_uvicorn)

result = runner.invoke(app, ["serve"])

assert result.exit_code == 2
assert "REST API requires optional api extra" in result.stdout
assert "mcts doctor ." in result.stdout


def test_doctor_deep_exits_zero(tmp_path: Path) -> None:
result = runner.invoke(app, ["doctor", "--deep", str(tmp_path)])

Expand Down