npx disponibles, et npx playwright install chromium lancé la veille45 minutes : 15 pour l'exercice 1, 12 pour le 2, 15 pour le 3, plus 8 pour l'exercice optionnel.
Le tag tp8-depart reconstruit l'état de sortie du TP 7 : la route GET /api/statistiques, la chaîne d'intégration, les deux hooks versionnés et le README complet. Le dépôt n'a encore aucun serveur MCP ni plugin.
cd ~/projets/quizops
outils/depart.sh 8
............................... [100%]
31 passed in 0.20s
Le TP a besoin de données et d'un serveur qui tourne. Le script du formateur remplit la base de douze scores connus :
$ uv run outils/graine.py
12 scores insérés
$ uv run python -c "import sqlite3; print(sqlite3.connect('quizops.db').execute('select count(*) from scores').fetchone()[0])"
12
Dans un second terminal, laissez l'application tourner pendant tout le TP :
uv run uvicorn app.main:app --reload
Note : le premier lancement du serveur Playwright télécharge le paquet npm et suppose un navigateur déjà installé (
npx playwright install chromium, environ 150 Mo). Sur un réseau filtré, faites-le avant la session.
Donner à l'agent un navigateur, lui faire jouer une partie complète, et prouver son passage par la base de données.
$ claude mcp add --transport stdio --scope project playwright -- npx @playwright/mcp@latest
Added stdio MCP server playwright with command: npx @playwright/mcp@latest to project config
File modified: ~/projets/quizops/.mcp.json
La portée project écrit un fichier versionné, donc toute l'équipe hérite du serveur au prochain git pull.
$ cat .mcp.json
{
"mcpServers": {
"playwright": {
"type": "stdio",
"command": "npx",
"args": [
"@playwright/mcp@latest"
],
"env": {}
}
}
}
Un serveur déclaré par le dépôt n'est pas actif tant que personne ne l'a approuvé sur ce poste.
$ claude mcp list
playwright: npx @playwright/mcp@latest - ⏸ Pending approval (run `claude` to approve)
Lancez claude : la session affiche une demande d'approbation pour les serveurs MCP du projet. Une fois acceptée, la même commande affiche ✔ Connected à la place de ⏸ Pending approval, et le menu /mcp de la session liste le serveur avec ses outils.
$ claude mcp get playwright
playwright:
Scope: Project config (shared via .mcp.json)
Type: stdio
Command: npx
Args: @playwright/mcp@latest
Ouvre http://127.0.0.1:8000 avec le navigateur. Saisis le pseudo
playwright, commence une partie, réponds à toutes les questions
(n'importe quelle proposition), et dis-moi le score affiché à la fin
ainsi que le premier du classement.
Voici ce que vous devez voir : l'agent charge les outils du serveur, prend une photographie de la page, et clique.
⏺ mcp__playwright__browser_navigate({"url": "http://127.0.0.1:8000"})
⎿ ### Ran Playwright code (+11 lignes)
⏺ mcp__playwright__browser_snapshot({})
⎿ ### Page (+19 lignes)
⏺ mcp__playwright__browser_type({"target": "e7", "element": "textbox \"Votre pseudo\"", "text": "play…)
⎿ ### Ran Playwright code (+3 lignes)
⏺ mcp__playwright__browser_click({"target": "e13", "element": "button \"Commencer la partie\""})
⎿ ### Ran Playwright code (+9 lignes)
⏺ mcp__playwright__browser_click({"target": "e18", "element": "button \"livenessProbe\" (answer option…)
⎿ ### Ran Playwright code (+9 lignes)
⏺ La partie est terminée. Score final : **30 %** pour playwright.
[fin] modèle(s) : claude-sonnet-5 ; entrée 64 + cache 1108906 ; sortie 2863 ; durée 61235 ms
Le score dépend des propositions choisies au hasard, il varie d'un poste à l'autre. La ligne [fin] rappelle le prix du navigateur : chaque photographie de page entre dans le contexte.
La partie jouée dans le navigateur a traversé l'API et atteint la base : le compteur passe de 12 à 13.
$ uv run python -c "import sqlite3; print(sqlite3.connect('quizops.db').execute('select count(*) from scores').fetchone()[0])"
13
Q: Pourquoi cette preuve porte-t-elle sur la base et non sur le score annoncé par l'agent ?
R: Le score varie à chaque partie, et rien n'oblige le texte de l'agent à correspondre à ce qui s'est passé. La ligne insérée dans scores, elle, ne peut venir que d'un POST /api/answers réellement exécuté par le navigateur.
Poser deux questions au même modèle, avec et sans serveur de documentation, et voir ce qu'un index de documentation sait faire et ne sait pas faire.
Quelle est la dernière version de FastAPI ? Réponds en une ligne.
Je ne peux pas garantir la version exacte à la date d'aujourd'hui (ma
connaissance s'arrête à janvier 2026) : vérifiez sur pypi.org/project/fastapi
pour le numéro précis.
$ claude mcp add --transport http --scope user context7 https://mcp.context7.com/mcp
Added HTTP MCP server context7 with URL: https://mcp.context7.com/mcp to user config
File modified: /Users/vous/.claude.json
$ claude mcp list
context7: https://mcp.context7.com/mcp (HTTP) - ✔ Connected
La portée user garde le serveur pour vous, dans tous vos projets, sans toucher au dépôt. C'est le bon choix pour un serveur de documentation, personnel et sans secret d'équipe.
Avec context7, quelle est la dernière version de FastAPI ? Réponds en
une ligne en citant la source.
⏺ mcp__context7__resolve-library-id({"query": "latest version", "libraryName": "FastAPI"})
⎿ Available Libraries: (+34 lignes)
⏺ mcp__context7__query-docs({"libraryId": "/websites/fastapi_tiangolo", "query": "latest release …)
⎿ ### 0.100.0 (2023-07-07) (+20 lignes)
⏺ Le contenu indexé par context7 (fastapi_tiangolo) ne remonte que jusqu'à la
version 0.100.0 (2023-07-07) dans ses notes de version, sans mention d'une
version plus récente.
$ curl -s https://pypi.org/pypi/fastapi/json | jq -r .info.version
0.141.1
❌ Aucune des deux réponses ne donne le bon numéro : le modèle situe honnêtement sa limite de connaissances, l'index de documentation cite une version vieille de deux ans. Un index de documentation répond sur l'usage d'une bibliothèque ; le numéro de la dernière version publiée se lit dans le registre du paquet.
Avec context7, comment déclare-t-on un paramètre de requête optionnel
avec Query dans FastAPI ? Donne l'exemple minimal et cite la page de
documentation utilisée.
La réponse arrive après un appel mcp__context7__query-docs, avec un extrait de code et la page citée. Le contenu varie selon l'index du jour, donc il ne sert pas de preuve : ce qui se vérifie, c'est que l'outil MCP a bien été appelé, et que la réponse renvoie à une page consultable.
$ claude mcp list
context7: https://mcp.context7.com/mcp (HTTP) - ✔ Connected
$ curl -s https://pypi.org/pypi/fastapi/json | jq -r .info.version
0.141.1
Retirez ensuite le serveur si vous ne le gardez pas :
$ claude mcp remove --scope user context7
Removed MCP server context7 from user config
quizops-kit (Libre)Empaqueter les deux skills, les hooks et le serveur MCP du projet dans un plugin, le valider, puis l'installer depuis une place de marché locale.
Tout l'outillage construit depuis le TP 6 vit dans .claude/ et ne suit que ce dépôt. Un plugin le rend installable ailleurs, en une commande, avec un numéro de version.
quizops-kit/.claude-plugin/plugin.json : nom quizops-kit, version 1.0.0, une description et un auteur.nouvelle-question (ses trois fichiers), la grille revue.md et le script garde-fou.sh, en gardant l'arborescence attendue (skills/, commands/, hooks/).quizops-kit/hooks/hooks.json à partir de la section hooks de .claude/settings.json, et copiez .mcp.json à la racine du plugin..claude-plugin/marketplace.json (nom quizops-local, une entrée pointant sur ./quizops-kit) et validez-la aussi..claude/settings.json./context que le skill du plugin est chargé.Contraintes à imposer dans votre prompt
hooks.json, le script se désigne par ${CLAUDE_PLUGIN_ROOT}.quizops-kit et quizops-local.💡 Indice : claude plugin validate lit les deux manifestes séparément ; validez le plugin avant d'écrire la place de marché, les messages d'erreur sont plus clairs dans cet ordre.
$ claude plugin validate ./quizops-kit
Validating plugin manifest: ~/projets/quizops/quizops-kit/.claude-plugin/plugin.json
✔ Validation passed
$ find quizops-kit -type f | sort
quizops-kit/.claude-plugin/plugin.json
quizops-kit/.mcp.json
quizops-kit/commands/revue.md
quizops-kit/hooks/garde-fou.sh
quizops-kit/hooks/hooks.json
quizops-kit/skills/nouvelle-question/SKILL.md
quizops-kit/skills/nouvelle-question/modele.json
quizops-kit/skills/nouvelle-question/verifier.py
$ claude plugin list
Installed plugins:
❯ quizops-kit@quizops-local
Version: 1.0.0
Scope: project
Status: ✔ enabled
1. Le manifeste du plugin
{
"name": "quizops-kit",
"description": "Outillage de l'équipe QuizOps : revue, ajout de question, hooks de formatage et de garde-fou, serveur MCP Playwright",
"version": "1.0.0",
"author": {
"name": "Formation Vibe Coding"
}
}
3. Les hooks du plugin
Le contenu est celui de .claude/settings.json, à une variable près : le plugin ne connaît pas la racine du projet, il connaît la sienne. Le hook de formatage est recopié tel quel, celui du garde-fou change de chemin.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/garde-fou.sh" }
]
}
]
}
}
4. La place de marché locale
{
"name": "quizops-local",
"description": "Place de marché locale de la formation Vibe Coding",
"owner": { "name": "Formation Vibe Coding" },
"plugins": [
{
"name": "quizops-kit",
"source": "./quizops-kit",
"description": "Outillage de l'équipe QuizOps : revue, ajout de question, hooks et serveur MCP Playwright"
}
]
}
5. L'ajout et l'installation
$ claude plugin marketplace add ./
Adding marketplace…✔ Successfully added marketplace: quizops-local (declared in user settings)
$ claude plugin install quizops-kit@quizops-local --scope project
Installing plugin "quizops-kit@quizops-local"...✔ Successfully installed plugin: quizops-kit@quizops-local (scope: project)
L'installation ajoute une clé au fichier de réglages du projet, qui se committe comme le reste :
"enabledPlugins": {
"quizops-kit@quizops-local": true
}
6. Le plugin dans le contexte
/context liste le skill du plugin à côté de celui du projet, avec son coût en tokens :
| Skill | Source | Tokens |
| nouvelle-question | Project | ~60 |
| quizops-kit:nouvelle-question| Plugin (quizops-kit) | ~70 |
Pour essayer un plugin sans l'installer, claude --plugin-dir ./quizops-kit le charge le temps d'une session. Le menu /plugin fait la même chose en interactif : parcourir les places de marché, installer, activer ou désactiver.
$ claude plugin uninstall quizops-kit@quizops-local --scope project
✔ Successfully uninstalled plugin: quizops-kit (scope: project)
$ claude plugin marketplace remove quizops-local
✔ Successfully removed marketplace: quizops-local
✅ Un plugin se valide avant de se distribuer, et son installation laisse une trace versionnée dans .claude/settings.json.
Installer un plugin qui ne vient pas de vous, et mesurer ce qu'il coûte en contexte.
security-guidance@claude-plugins-official depuis la session avec /plugin install, puis rechargez avec /reload-plugins./context et notez ce que le plugin ajoute aux sections Skills et Custom agents.$ claude plugin marketplace list
Configured marketplaces:
❯ claude-plugins-official
Source: GitHub (anthropics/claude-plugins-official)
Preuve attendue : claude plugin list --enabled contient l'entrée security-guidance@claude-plugins-official, et /context affiche les tokens qu'elle occupe. Les sorties de cet exercice ne sont pas reproduites ici : elles dépendent du contenu du marché au jour de la session.
💡 Indice : un plugin qui apporte des skills et des sous-agents pèse sur chaque requête de la session. /context avant et après donne le chiffre exact.
project versionne le serveur dans .mcp.json, user le garde pour vous, local reste au poste.⏸ Pending approval tant qu'elle n'est pas donnée.claude plugin validate, un numéro de version, et enabledPlugins dans le fichier de réglages du projet.Rendez-vous au TP 9 pour déléguer : deux sous-agents à privilèges restreints, une fonctionnalité complète livrée seul avec tout l'outillage, et les règles d'usage de l'IA du projet.
➡️ Module associé : MCP, Plugins et Outils Externes · TP suivant : TP 9 : Sous-Agents, Défi Final et Règles d'Usage