From c4cb801c96651458f674cea15f9565e4585b1b7b Mon Sep 17 00:00:00 2001 From: luohui1 <3053763193@qq.com> Date: Sun, 14 Jun 2026 12:42:04 +0800 Subject: [PATCH] fix: show api extra in doctor preflight --- CHANGELOG.md | 4 ++++ docs/platform/cli.md | 1 + src/mcts/cli/doctor.py | 36 ++++++++++++++++++++++++++++-------- src/mcts/cli/main.py | 1 + tests/test_doctor.py | 36 ++++++++++++++++++++++++++++++++++-- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c81d031..311e784 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 + +- `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 diff --git a/docs/platform/cli.md b/docs/platform/cli.md index 407af2e..35bee6c 100644 --- a/docs/platform/cli.md +++ b/docs/platform/cli.md @@ -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 . diff --git a/src/mcts/cli/doctor.py b/src/mcts/cli/doctor.py index eef2cd9..85f1ad3 100644 --- a/src/mcts/cli/doctor.py +++ b/src/mcts/cli/doctor.py @@ -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"), @@ -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))) @@ -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: @@ -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 diff --git a/src/mcts/cli/main.py b/src/mcts/cli/main.py index 8c92d7c..4277574 100644 --- a/src/mcts/cli/main.py +++ b/src/mcts/cli/main.py @@ -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 diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 1e84f8a..0a3901e 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -2,6 +2,7 @@ from __future__ import annotations +import builtins import json from importlib.machinery import ModuleSpec from pathlib import Path @@ -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 @@ -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 @@ -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)])