Serveur MCP¶
Le serveur MCP (security-review-mcp) expose les prompts, outils et ressources utilisés par
l'hôte. Il partage la base avec le contrôleur mais ne l'appelle jamais. Son code est dans
server.py (déclarations), prompts.py (texte des prompts) et les modules métier
(reports/services.py, rules/, deps/…) : server.py reste une couche mince qui valide les
entrées et appelle ces modules.
uv run security-review-mcp local # stdio, comme le lance un hôte
uv run security-review-mcp serve --port 8000 # HTTP streamable (défaut)
uv run pytest tests/test_server.py # tests de bout en bout de la couche MCP
Outils¶
Chaque outil porte annotations=READ_ONLY ou WRITE et le décorateur @_tool_errors, qui
transforme ValueError et services.NotFoundError en ToolError lisible par le modèle (le SDK
masque le message de toute autre exception). Levez ces exceptions pour les erreurs attendues ;
ne renvoyez jamais de dictionnaire d'erreur.
@mcp.tool(annotations=READ_ONLY)
@_tool_errors
def scan_code(
content: Annotated[str, Field(description="Source or configuration text to scan")],
language: Annotated[
str, Field(description=f"One of: {', '.join(scanner.SUPPORTED_LANGUAGES)}")
],
) -> dict:
"""Quick pattern scan that returns suspicious lines to verify (not a full SAST)."""
limit = get_settings().max_content_chars
if len(content) > limit:
raise ValueError(f"Content too large ({len(content)} chars); the limit is {limit}")
...
- Décrivez chaque paramètre avec
Annotated[..., Field(description=...)]et l'outil par sa docstring : c'est ce que lit le modèle de l'hôte. - Bornez toute entrée texte ou liste par un paramètre de
Settings(max_content_chars,max_findings_per_report,max_list_limit…). - Un nouvel outil ou prompt qui change le déroulé d'une revue est cité dans
INSTRUCTIONS(server.py). - Aucun outil ne change le statut d'un finding :
propose_finding_statusenregistre seulement une proposition, validée par une personne dans l'interface web.
Prompts et ressources¶
- Un prompt se déclare avec
@mcp.prompt(name=..., title=..., description=...); son texte est construit dansprompts.pyet ses arguments sont documentés dans la sectionArgs:de la docstring. - Le contenu fourni par l'utilisateur (code, diff, contexte) est encadré avec
prompts.fenced, pour qu'il ne puisse pas fermer le bloc et passer pour des instructions. - Les prompts de revue se terminent par l'appel unique à
save_security_report. - Les ressources utilisent
@_resource_errors, l'équivalent de@_tool_errors.
Invariants de sécurité¶
Le processus MCP tourne à côté du code du développeur, souvent en stdio : un écart ici est un bug bloquant.
- stdout est réservé au protocole MCP : aucun
print, journaux sur stderr vialogging.getLogger("security_review_mcp"). - Aucun accès disque à partir d'un chemin du client : les outils travaillent sur le contenu reçu.
- Aucun appel réseau sortant : la base de vulnérabilités est importée par le contrôleur.
- Aucun module du contrôleur n'est chargé, même indirectement (
controller,deps/advisory_sync,auth/oidc,observability/audit_syslog,connectors,update_check,web_api) :test_mcp_server_does_not_load_the_controller_modulesimporteserverdans un nouvel interpréteur et le vérifie. Un module partagé les importe dans la fonction qui s'en sert. - Les secrets sont masqués (
scanner.redact_secrets) avant stockage et dans les extraits renvoyés ; un jeton, un mot de passe ou une URL de base avec identifiants n'est jamais journalisé. - Le serveur MCP HTTP ne migre jamais le schéma et n'expose que
/mcp, l'enrôlement public et/healthz.
Règles de scan¶
Les règles et checklists par défaut sont dans src/security_review_mcp/rules/default_rules.toml,
jamais dans le code Python. Elles sont copiées en base au premier lancement, et une règle
ajoutée ou corrigée dans ce fichier atteint les bases existantes au démarrage suivant, sauf si
un administrateur l'a modifiée.
[[rules]]
id = "GEN-001"
title = "Hard-coded credential"
severity = "high"
cwe = "CWE-798"
languages = ["*"]
pattern = "(password|passwd|pwd|secret|api[_-]?key|token)\\s*[:=]\\s*[\\\"'][^\\\"'\\s]{6,}[\\\"']"
advice = "Move the secret to a vault or environment variable and rotate it."
- Identifiants
PRÉFIXE-NNNpar famille (PY,JV,NG,CFG,GEN,FA,TS…) : ne réutilisez et ne renumérotez jamais un identifiant, les utilisateurs les citent danssecreview:ignoreetdisabled_rules. Indiquez la CWE quand elle existe. - Évitez le backtracking catastrophique : pas de quantificateurs imbriqués (
rules_loadervalide les règles internes). - Documentez la règle dans
docs/usage/rules(ouusage/checklistspour une checklist), dans les deux langues.
Tests¶
-
Outils, prompts et ressources : test de bout en bout avec un vrai client MCP en mémoire, avec un cas d'erreur représentatif.
pytestmark = pytest.mark.anyio async def test_invalid_input_returns_a_tool_error(): async with Client(mcp) as client: result = await client.call_tool("get_security_report", {"report_id": 424242}) assert result.is_error assert "not found" in text_of(result).lower()Une
ToolErrorparvient au client sous la forme d'un résultat avecis_error, et son message dans le texte du résultat. -
Règle de scan : au moins un cas positif (le motif vulnérable est détecté) et un cas négatif (le code sûr équivalent ne l'est pas), dans
tests/test_scanner.py. tests/conftest.pyfournit à chaque test une base SQLite en mémoire et un registre de règles vide ; la fixtureimport_packcharge des règles internes.
Voir aussi les prompts et outils tels que les utilisateurs les voient, et les règles.