Annexes · Claude
Aide-mémoire MCP
Architecture du Model Context Protocol, primitives, transports, cycle de vie, sécurité, écriture de serveurs en Python et TypeScript, et quand utiliser MCP plutôt que ses alternatives.
Architecture
┌──────────────── Host (Claude Desktop / Claude Code / your app) ────────────────┐│ ││ ┌─────────────┐ 1:1 ┌─────────────┐ ┌─────────────┐ ││ │ MCP Client │◄──────────►│ MCP Server │ │ MCP Server │ ││ │ (per server)│ JSON-RPC │ github │ │ postgres │ ││ └─────────────┘ └─────────────┘ └─────────────┘ ││ ▲ transport: stdio transport: Streamable HTTP ││ │ (local subprocess) (remote, OAuth 2.1) ││ Claude model ── sees tools/resources/prompts exposed by all servers │└────────────────────────────────────────────────────────────────────────────────┘- Le host embarque un client par serveur ; les clients parlent JSON-RPC 2.0 aux serveurs.
- Les serveurs exposent des capacités ; le host décide ce que le modèle voit et applique les permissions.
Primitives
| Primitive | Contrôlée par | Ce que c’est | Exemple |
|---|---|---|---|
| Tools | Modèle | Fonctions appelables avec entrées en JSON Schema | create_issue, query_db |
| Resources | Application | Données en lecture seule adressées par URI | file:///docs/spec.md, db://schema |
| Prompts | Utilisateur | Modèles de prompt réutilisables avec arguments | /summarise-pr |
| Sampling | Serveur → client | Le serveur demande au modèle du host de compléter quelque chose | Résumé côté serveur |
| Roots | Client → serveur | Frontières filesystem/URI dans lesquelles le serveur peut opérer | Répertoire de projet |
| Logging / progress | Serveur → client | Diagnostics et progression de tâches longues | Progression d’indexation |
Cycle de vie
client ──initialize (protocolVersion, capabilities, clientInfo)──► serverclient ◄──result (capabilities, serverInfo)────────────────────── serverclient ──notifications/initialized──────────────────────────────► serverclient ──tools/list ──► server client ──resources/list ──► serverclient ──tools/call {name, arguments} ──► server ──► result {content[], isError}La négociation des capacités à initialize indique à chaque partie quelles primitives (tools, resources, prompts, sampling, logging) sont supportées.
Transports
| Transport | Où | Auth | Notes |
|---|---|---|---|
stdio | Sous-processus local | Permissions processus/utilisateur | Le plus simple ; défaut de Claude Desktop/Code pour les serveurs locaux |
| Streamable HTTP | Serveur distant | OAuth 2.1 (PKCE), bearer tokens | Standard distant actuel ; supporte les réponses en streaming |
| HTTP + SSE | Distant (ancien) | OAuth / tokens | Remplacé par Streamable HTTP ; encore rencontré |
Écrire un serveur
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()def get_order(order_id: str) -> dict: """Look up one order by ID. Use when the user references an order number. Returns status, eta and items. Read-only.""" order = db.fetch_order(order_id) if order is None: raise ValueError(f"not_found: no order {order_id}") # surfaces as isError with message return order
@mcp.resource("orders://schema")def schema() -> str: """Orders table schema (read-only reference).""" return open("schema.sql").read()
@mcp.prompt()def triage(order_id: str) -> str: return f"Triage order {order_id}: check status, delays and next action."
if __name__ == "__main__": mcp.run(transport="stdio") # or transport="streamable-http"import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { z } from 'zod';
const server = new McpServer({ name: 'orders', version: '1.0.0' });
server.tool( 'get_order', 'Look up one order by ID. Use when the user references an order number. Returns status, eta and items. Read-only.', { order_id: z.string().describe('Order ID, e.g. ORD-12345') }, async ({ order_id }) => { const order = await db.fetchOrder(order_id); if (!order) { return { isError: true, content: [{ type: 'text', text: JSON.stringify({ category: 'not_found', retryable: false, message: `No order ${order_id}` }) }] }; } return { content: [{ type: 'text', text: JSON.stringify(order) }] }; },);
server.resource('schema', 'orders://schema', async () => ({ contents: [{ uri: 'orders://schema', text: await fs.readFile('schema.sql', 'utf8') }],}));
await server.connect(new StdioServerTransport());Checklist de conception de serveur
- Un objectif étroit par outil ; 4–8 outils par serveur est typique. Au-delà de ~10 exposés à un agent, appuyez-vous sur la recherche d’outils /
defer_loading. - Les descriptions énoncent le quoi, le quand, le quand-pas, et la forme de retour.
- Erreurs structurées (
isError: trueavec category, retryable, message) — jamais de succès vide. - Idempotence pour tout ce qui écrit ; acceptez une clé d’idempotence.
- Pagination pour les opérations de liste ; plafonnez la taille des pages.
- Moindre privilège : exposez les outils de lecture par défaut ; placez les outils destructifs derrière des serveurs séparés ou une confirmation.
- Propagation d’identité : pour les applications multi-utilisateurs, les outils doivent agir en tant qu’utilisateur final (OAuth pour le compte de), pas en tant que super-utilisateur partagé.
- Traitez la sortie d’outil comme non fiable en aval (injection indirecte).
- Versionnez le serveur ; ajoutez des outils plutôt que de changer la sémantique.
Se connecter depuis Claude
claude mcp add orders -- python orders_server.py # stdio, local scopeclaude mcp add --scope project orders -- python orders_server.py # .mcp.json, sharedclaude mcp add --transport http linear https://mcp.linear.app/mcp # remote/mcp # inspect, authenticate{ "mcpServers": { "orders": { "command": "python", "args": ["/abs/path/orders_server.py"] }} }r = client.messages.create( model="claude-opus-5", max_tokens=1024, mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp", "name": "orders", "authorization_token": token}], messages=[{"role": "user", "content": "Where is ORD-12345?"}],)Aucun harnais client nécessaire — les serveurs d’Anthropic appellent le serveur MCP distant pour vous.
MCP vs alternatives
| Besoin | Préférer | Raison |
|---|---|---|
| Connecteur réutilisable vers un système externe utilisé par plusieurs agents/hosts | Serveur MCP | Interface standard, découvrable, permissions gérées par le host |
| Fonction ponctuelle dans une seule application | Outil personnalisé dans la requête API | Moins d’infrastructure |
| Une procédure/connaissance que Claude doit suivre | Skill | Divulgation progressive, aucun runtime |
| Étape scriptée déterministe sans jugement du modèle | Simple appel API/CLI dans le code | Moins cher, testable |
| Deux systèmes autonomes qui négocient | Protocole agent-à-agent / couche d’orchestration | MCP est modèle↔outil, pas agent↔agent |
Liste rapide de sécurité
| Risque | Contrôle |
|---|---|
| Outils trop larges (delete/refund exposés) | Les supprimer ; serveurs séparés ; hooks |
| Injection indirecte via les tool results | Frontières (XML), traiter comme des données, validation de sortie |
| Identifiants partagés | OAuth par utilisateur ; tokens éphémères ; scopes |
| Fuite de secrets | Env/gestionnaire de secrets ; jamais dans les prompts, CLAUDE.md ou logs |
| Serveurs non fiables | Allowlist de serveurs ; épingler les versions ; relire la source |
| Agence excessive | Approbation humaine pour les actions irréversibles ; permissions.ask |
Négociation des capacités (le JSON)
À initialize, chaque partie annonce ce qu’elle supporte. Le host n’expose que les primitives négociées des deux côtés.
// client → server{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "claude-code", "version": "2.1.0" } } }
// server → client{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "prompts": { "listChanged": true }, "logging": {} }, "serverInfo": { "name": "orders", "version": "1.4.0" } } }
// client → server (handshake complete){ "jsonrpc": "2.0", "method": "notifications/initialized" }Si le serveur n’annonce pas sampling, le host ne lui routera pas de requêtes de sampling — et inversement pour les roots du client.
Resources, prompts et sampling — exemples détaillés
Resources (contrôlées par l’application, en lecture seule)
// list{ "jsonrpc": "2.0", "id": 2, "method": "resources/list" }{ "jsonrpc": "2.0", "id": 2, "result": { "resources": [ { "uri": "orders://schema", "name": "Orders schema", "mimeType": "text/sql" }, { "uri": "orders://policy/refunds", "name": "Refund policy", "mimeType": "text/markdown" } ] } }
// read{ "jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": { "uri": "orders://schema" } }{ "jsonrpc": "2.0", "id": 3, "result": { "contents": [ { "uri": "orders://schema", "mimeType": "text/sql", "text": "CREATE TABLE orders (…)" } ] } }Les resources sont des données, pas des actions : aucun effet de bord, adressées par URI, choisies par l’application (pas le modèle). Utilisez-les pour les schémas, les politiques et les docs de référence.
Prompts (modèles contrôlés par l’utilisateur)
{ "jsonrpc": "2.0", "id": 4, "method": "prompts/get", "params": { "name": "triage", "arguments": { "order_id": "ORD-12345" } } }{ "jsonrpc": "2.0", "id": 4, "result": { "messages": [ { "role": "user", "content": { "type": "text", "text": "Triage order ORD-12345: check status, delays and next action." } } ] } }Les prompts apparaissent comme des slash commands (/triage) — c’est l’utilisateur qui les invoque, contrairement aux tools (invoqués par le modèle) ou aux resources (sélectionnées par l’application).
Sampling (le serveur sollicite le modèle du host)
Le serveur peut demander une complétion au modèle du host — p. ex. pour résumer avant de renvoyer. Le host garde le contrôle et peut refuser, caviarder ou limiter le débit.
// server → client{ "jsonrpc": "2.0", "id": 5, "method": "sampling/createMessage", "params": { "messages": [{ "role": "user", "content": { "type": "text", "text": "Summarise: <500 rows>" } }], "maxTokens": 300, "modelPreferences": { "intelligencePriority": 0.3, "speedPriority": 0.8 } } }
// client → server (after the host runs its model, with user approval){ "jsonrpc": "2.0", "id": 5, "result": { "role": "assistant", "content": { "type": "text", "text": "12 orders delayed, avg 3 days…" }, "model": "claude-haiku-4-5", "stopReason": "endTurn" } }Le sampling est une frontière de confiance
Le sampling permet à un serveur de dépenser les tokens du host et de voir la sortie du modèle. Le host doit le contrôler (approbation, limites de débit) et ne jamais auto-approuver pour des serveurs non fiables.
Exemple de serveur Streamable HTTP
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()def get_order(order_id: str) -> dict: """Look up one order by ID. Read-only.""" return db.fetch_order(order_id)
if __name__ == "__main__": # Serves POST /mcp with streaming responses; put OAuth 2.1 in front (reverse proxy / gateway) mcp.run(transport="streamable-http", host="0.0.0.0", port=8080, path="/mcp")Client Reverse proxy / gateway MCP server │ POST /mcp (Bearer token) │ │ ├────────────────────────────────► validate token, scopes │ │ ├────────────────────────────────► initialize / tools/call │ │ │ │ ◄─────── streamed JSON-RPC responses (chunked) ─────────────────┤Streamable HTTP est un endpoint unique qui supporte requête/réponse et messages streamés par le serveur. Il remplace l’ancien transport HTTP+SSE à deux endpoints.
Flux OAuth 2.1 (serveurs distants)
┌── Host (MCP client) ──┐ ┌── Authorization server ──┐ ┌── MCP server ──┐│ 1. discover metadata │────────►│ /.well-known/oauth-* │ │ (resource) ││ 2. PKCE: code_verifier│ │ │ │ ││ + code_challenge │ │ │ │ ││ 3. authorize (browser)│────────►│ user logs in, consents │ │ ││ 4. redirect w/ code │◄────────│ auth code │ │ ││ 5. token (code + │────────►│ /token │ │ ││ code_verifier) │◄────────│ access + refresh token │ │ ││ 6. call with Bearer │───────────────────────────────────┼─────►│ validate scope ││ 7. refresh on expiry │────────►│ /token (refresh_token) │ │ │└───────────────────────┘ └──────────────────────────┘ └────────────────┘- PKCE est obligatoire dans OAuth 2.1 (pas de flux implicite).
- Les tokens sont éphémères ; rafraîchissez silencieusement.
- Les scopes correspondent aux permissions d’outils ; le token porte l’identité de l’utilisateur final pour que les outils agissent en tant que cet utilisateur.
- Le serveur MCP est un serveur de ressources ; un serveur d’autorisation distinct émet les tokens.
Formes d’erreur
MCP distingue les erreurs de protocole (niveau JSON-RPC) des erreurs d’exécution d’outil (un appel réussi dont le résultat dit qu’il a échoué).
// Protocol / JSON-RPC error (method missing, bad params){ "jsonrpc": "2.0", "id": 7, "error": { "code": -32602, "message": "Invalid params: order_id required" } }
// Tool execution error (call succeeded, tool failed) — isError on the result{ "jsonrpc": "2.0", "id": 8, "result": { "isError": true, "content": [{ "type": "text", "text": "{\"category\":\"not_found\",\"retryable\":false,\"message\":\"No order ORD-999\"}" }] } }| Code JSON-RPC | Signification |
|---|---|
-32700 | Erreur de parsing |
-32600 | Requête invalide |
-32601 | Méthode non trouvée |
-32602 | Paramètres invalides |
-32603 | Erreur interne |
Règle : les échecs métier utilisent isError: true sur le résultat (le modèle peut les voir et réagir), pas une erreur JSON-RPC. Réservez les erreurs JSON-RPC aux appels réellement malformés.
Tester avec le MCP Inspector
# Launch the inspector against a local stdio servernpx @modelcontextprotocol/inspector python orders_server.py
# Against a remote Streamable HTTP server (walks the OAuth flow)npx @modelcontextprotocol/inspector --transport http https://mcp.example.com/mcpChecklist dans l’inspector : initialize renvoie les capacités attendues ; tools/list affiche les bons noms, descriptions et schémas ; chaque appel d’outil renvoie du contenu structuré ; les chemins d’erreur positionnent isError ; les resources se lisent proprement ; les prompts s’affichent avec les arguments. Testez avant de câbler le serveur dans Claude — la plupart des bugs « le modèle n’appelle pas mon outil » sont des bugs de description/schéma que l’inspector fait remonter immédiatement.
Versionnement
| Changement | Compatibilité | Faire |
|---|---|---|
| Ajouter un nouvel outil | Rétrocompatible | Livrez librement ; incrémentez la version mineure |
| Ajouter un champ optionnel | Rétrocompatible | Livrez ; documentez |
| Renommer/supprimer un outil ou un champ requis | Cassant | Nouveau nom d’outil ; gardez l’ancien déprécié un temps |
| Changer silencieusement la sémantique d’un outil | Dangereux | Jamais — les agents ont encodé l’ancien comportement ; versionnez et communiquez |
| Version du protocole | Négociée à initialize | Supportez une plage ; annoncez la plus haute que vous parlez |
Préférez une évolution additive. Comme les agents et les prompts encodent les noms et comportements d’outils, un changement sémantique silencieux casse les appelants sans erreur — le versionnement et les fenêtres de dépréciation sont l’approche correcte pour l’examen.
Idées reçues courantes
| Idée reçue | Réalité | Pourquoi cela compte à l’examen |
|---|---|---|
| « Tools, resources et prompts sont interchangeables » | Tools = actions invoquées par le modèle ; resources = données sélectionnées par l’application ; prompts = modèles invoqués par l’utilisateur | Distracteur de confusion de primitives |
| « MCP est agent-à-agent » | MCP est modèle↔outil ; utilisez une couche d’orchestration/A2A pour agent↔agent | Distracteur de mauvais protocole |
« Un outil qui ne trouve rien devrait renvoyer {} » | Renvoyez isError avec une category ; jamais de succès vide | Anti-pattern de silent-failure |
| « Exposer tout ce que le serveur peut faire » | Moindre privilège ; séparez les outils destructifs ; supprimez les inutiles | Distracteur d’outils trop larges |
| « OAuth est optionnel pour les serveurs distants » | Les serveurs distants Streamable HTTP nécessitent OAuth 2.1 + PKCE ; propagez l’identité de l’utilisateur | Faille d’authz de super-utilisateur partagé |
| « 20 outils sur un serveur, ça va » | 4–8 typique ; au-delà de ~10 utilisez la recherche d’outils + defer_loading | Anti-pattern de trop d’outils |
| « Les erreurs métier devraient être des erreurs JSON-RPC » | Utilisez isError sur le résultat ; les erreurs JSON-RPC sont pour les appels malformés | Distracteur de forme d’erreur |
Analyse de scénario
Une entreprise SaaS veut que Claude, embarqué dans plusieurs applications internes, lise et agisse sur les enregistrements CRM des clients. Multi-tenant : chaque utilisateur final ne peut voir que ses propres comptes. Le CRM dispose déjà d’un fournisseur OAuth. Comment concevoir l’intégration MCP ?
- Serveur MCP, pas des outils personnalisés par application — le connecteur est réutilisé sur plusieurs hosts/applications ; un serveur standard est découvrable et géré par les permissions du host.
- Transport Streamable HTTP — distant, multi-utilisateur ; stdio est réservé aux sous-processus locaux.
- OAuth 2.1 + PKCE, tokens par utilisateur — le token porte l’identité de l’utilisateur final pour que les outils appliquent les permissions CRM de cet utilisateur. Un identifiant de super-utilisateur partagé est le distracteur de faille d’authz.
- Outils étroits —
search_accounts,get_account,create_note(4–8). Les opérations destructives (delete_account) vivent derrière un serveur séparé ou une confirmation, ou sont omises (moindre privilège). - Erreurs structurées —
isErroravec category/retryable ; jamais de succès vide. - Traiter la sortie d’outil comme non fiable — les notes CRM pourraient porter une injection indirecte ; gardez les frontières et validez en aval.
- Versionner de façon additive — ajoutez des outils au fil du temps ; ne changez jamais silencieusement la sémantique.
Alternatives rejetées : stdio (pas distant/multi-utilisateur), une clé API partagée (casse l’authz par utilisateur), exposer chaque verbe CRM (trop large), et renvoyer {} sur « aucun compte trouvé » (silent failure).
Dernière mise à jour le 18 sept. 2026