# 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.

import { Tabs, TabItem, Steps } from '@prosefly/astro-components';

## Architecture

```text
┌──────────────── 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

```text
client ──initialize (protocolVersion, capabilities, clientInfo)──► server
client ◄──result (capabilities, serverInfo)────────────────────── server
client ──notifications/initialized──────────────────────────────► server
client ──tools/list ──► server      client ──resources/list ──► server
client ──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

<Tabs>
  <TabItem label="Python (FastMCP)">
```python
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"
```
  </TabItem>
  <TabItem label="TypeScript">
```typescript
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());
```
  </TabItem>
</Tabs>

### 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: true` avec 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

<Tabs>
  <TabItem label="Claude Code">
```bash
claude mcp add orders -- python orders_server.py                     # stdio, local scope
claude mcp add --scope project orders -- python orders_server.py     # .mcp.json, shared
claude mcp add --transport http linear https://mcp.linear.app/mcp    # remote
/mcp                                                                 # inspect, authenticate
```
  </TabItem>
  <TabItem label="Claude Desktop">
```json
// claude_desktop_config.json
{ "mcpServers": {
  "orders": { "command": "python", "args": ["/abs/path/orders_server.py"] }
} }
```
  </TabItem>
  <TabItem label="Messages API (MCP connector)">
```python
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.
  </TabItem>
</Tabs>

## 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.

```json
// 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)

```json
// 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)

```json
{ "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.

```json
// 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" } }
```

:::caution[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

```python
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")
```

```text
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)

```text
┌── 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é).

```json
// 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

```bash
# Launch the inspector against a local stdio server
npx @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/mcp
```

Checklist 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 ?

<Steps>
1. **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.
2. **Transport Streamable HTTP** — distant, multi-utilisateur ; stdio est réservé aux sous-processus locaux.
3. **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.
4. **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).
5. **Erreurs structurées** — `isError` avec category/retryable ; jamais de succès vide.
6. **Traiter la sortie d'outil comme non fiable** — les notes CRM pourraient porter une injection indirecte ; gardez les frontières et validez en aval.
7. **Versionner de façon additive** — ajoutez des outils au fil du temps ; ne changez jamais silencieusement la sémantique.
</Steps>

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).
