📌 Version de spec couverte
Cette note décrit le modèle de programmation MCP, valable de la révision 2025-03-26 à aujourd'hui. La révision 2026-07-28 a changé le transport, l'autorisation et le cycle de vie : plus de handshake, plus de session, notifications sur abonnement. Les sections 4️⃣, 5️⃣ et 7️⃣ intègrent ces changements ; le détail complet est dans mcp-2026-07-28-coeur-stateless.
🎯 OBJECTIF
Comprendre comment :
🧠 MODÈLE MENTAL
Avant MCP, connecter un LLM à un outil externe signifiait écrire une intégration custom par outil, par LLM, par application. Le résultat : une explosion combinatoire de glue code fragile, impossible à maintenir et non-réutilisable.
MCP (Model Context Protocol, Anthropic 2024) est le "USB de l'IA" : un protocole standard qui sépare proprement le client (l'app qui orchestre le LLM) du server (le service qui expose des capacités). Un MCP server écrit une fois est consommable par n'importe quel agent compatible, sans modification. C'est exactement le même pari que LSP a réussi pour les éditeurs de code. Il n'est qu'un levier parmi d'autres pour donner du contexte à une IA : voir outiller-developpement-ia-skills-agents-instructions.
flowchart LR
subgraph Host ["🖥️ Host (application LLM)"]
LLM["🤖 LLM"]
C1["Client 1"]
C2["Client 2"]
LLM <--> C1
LLM <--> C2
end
subgraph S1 ["MCP Server A\n(stdio — local)"]
T1["Tool: read_file"]
T2["Tool: write_file"]
end
subgraph S2 ["MCP Server B\n(Streamable HTTP — distant)"]
T3["Tool: search_docs"]
R1["Resource: docs://index"]
P1["Prompt: summarize"]
end
C1 -- "stdin/stdout" --> S1
C2 -- "HTTP" --> S2mermaidLe LLM ne parle jamais directement aux Servers. Il passe toujours par le Client, qui sérialise en JSON-RPC 2.0.
🔑 Conclusion clé
Un Client est en vis-à-vis avec exactement un Server, et un Host peut avoir plusieurs Clients. Cette topologie fixe est la garantie que le LLM ne peut pas appeler un Server sans passer par la couche de contrôle du Host. Elle reste vraie même depuis que le protocole est sans session : ce qui a disparu, c'est la connexion persistante, pas le cloisonnement.
sequenceDiagram
actor User
participant Host
participant LLM
participant Client
participant Server
User->>Host: "Cherche les documents ouverts"
Host->>LLM: user message + liste des tools disponibles
LLM-->>Host: tool_use { name: "search_docs", input: { status: "open" } }
Host->>Client: invoke tool
Client->>Server: tools/call (JSON-RPC)
Server-->>Client: { content: [...documents] }
Client-->>Host: tool result
Host->>LLM: tool result → continue
LLM-->>Host: réponse finale en langage naturel
Host-->>User: affichagemermaidLe LLM décide d'appeler un Tool, il ne l'exécute pas lui-même. Le Server retourne un résultat, le LLM synthétise.
| Critère | stdio | Streamable HTTP |
|---|---|---|
| Usage | Dev local, CLI tools, processus enfants | Servers distants, multi-clients, prod |
| Communication | stdin/stdout JSON-RPC | HTTP POST, chaque requête autonome |
| Auth | N/A, même machine | Bearer token, OAuth 2.1 |
| Déploiement | Lancé par le Host comme subprocess | Service autonome, dockerisé, scalable horizontalement |
| État | Un processus par client | Aucun état de transport à conserver |
🚨 Le modèle POST + SSE permanent est déprécié
Jusqu'à la révision 2025-11-25, Streamable HTTP combinait un POST pour les requêtes client vers serveur et un flux SSE maintenu ouvert pour les notifications serveur vers client, le tout épinglé par un header Mcp-Session-Id. Depuis la 2026-07-28, la session et le handshake sont retirés, et les notifications passent par un abonnement explicite subscriptions/listen. Le transport HTTP+SSE historique est officiellement déprécié avec une fenêtre de douze mois. Ne pas construire un nouveau serveur dessus.
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: "docs-mcp", // ✓ identifiant du server
version: "1.0.0",
});
server.tool(
"search_docs",
"Recherche dans un index documentaire et retourne les extraits pertinents",
{
query: z.string().describe("Termes de recherche en langage naturel"),
limit: z.number().int().min(1).max(20).optional().describe("Nombre max de résultats"),
},
async ({ query, limit = 5 }) => {
const results = await searchIndex(query, limit); // ⚠️ appel async réel
return {
content: [{ type: "text", text: JSON.stringify(results) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);typescriptPour un serveur distant, on branche un transport Streamable HTTP sur un endpoint POST unique. La signature exacte du transport a changé avec le SDK aligné sur la 2026-07-28 (disparition du générateur de session, de l'endpoint GET SSE) : se référer aux notes de migration du SDK plutôt qu'à un exemple figé. Ce qui ne change pas, c'est la forme de la requête reçue :
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search_docs
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search_docs","arguments":{"query":"retry policy"},
"_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}
Les en-têtes Mcp-Method et Mcp-Name sont obligatoires et doivent rester cohérents avec le corps JSON-RPC.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>xml@Configuration
public class McpToolsConfig {
@Bean
public ToolCallbackProvider docTools(DocumentService documentService) {
return MethodToolCallbackProvider.builder()
.toolObjects(documentService) // ✓ expose les @Tool de DocumentService
.build();
}
}
@Service
public class DocumentService {
@Tool(description = "Recherche dans un index documentaire")
public SearchResult searchDocs(
@ToolParam(description = "Termes de recherche") String query,
@ToolParam(description = "Nombre max de résultats") Integer limit) {
// ...
}
}javaspring:
ai:
mcp:
server:
name: docs-mcp
version: 1.0.0
protocol: STATELESS # STDIO | STREAMABLE | STATELESS
streamable-http:
mcp-endpoint: /mcpyaml🚨 @Tool ≠ @Bean Spring
Les méthodes annotées @Tool ne sont pas des beans Spring ordinaires : elles sont découvertes par réflexion par MethodToolCallbackProvider. Injecter des dépendances dans la classe @Service est correct, mais ne pas annoter @Tool sur des méthodes @Bean de config, ça ne marche pas.
⚠️ Java n'est pas un SDK Tier 1
Les quatre SDK maintenus par le projet sont TypeScript, Python, Go et C#. L'implémentation Java suit avec un décalage. Attention au faux ami : protocol: STATELESS côté Spring AI signifie "aucun état de session conservé entre requêtes", ce qui n'implique pas automatiquement que le serveur parle la révision 2026-07-28 sur le fil. Vérifier la version de protocole annoncée dans les notes de version avant de s'engager.
Le transport HTTP s'appuie sur OAuth 2.1. Pour un usage interne, un Bearer token statique avec scope sur les tools autorisés reste suffisant en V1.
Client → Authorization Server : demande token (PKCE flow)
Client → MCP Server : Bearer token dans Authorization header
MCP Server → Authorization Server : introspection/validation
Quatre durcissements sont arrivés avec la 2026-07-28 :
iss (RFC 9207) avant d'échanger le code d'autorisation.application_type est déclaré à l'enregistrement, ce qui débloque les redirects localhost des clients desktop et CLI.Bonne pratique — scopes par tool
Déclarer des scopes fins (docs:read, index:write) plutôt qu'un token global. Ça permet de restreindre ce qu'un agent peut faire sans toucher au code du Server. Avec les en-têtes Mcp-Method et Mcp-Name, ce filtrage peut même remonter au niveau du gateway.
Dans la majorité des cas d'intégration backend, Tools seuls suffisent. Resources et Prompts valent l'investissement quand le LLM a besoin d'un contexte stable (config, référentiels) ou de templates réutilisables standardisés.
// Resource — données stables exposées sans tool call
server.resource(
"config://index-settings",
"Paramètres de l'index documentaire",
async (uri) => ({
contents: [{ uri: uri.href, text: JSON.stringify(indexSettings) }],
})
);
// Prompt — template réutilisable pour tâches répétitives
server.prompt(
"summarize-document",
"Résume un document et en extrait les points d'action",
{ document: z.string() },
({ document }) => ({
messages: [{
role: "user",
content: { type: "text", text: `Résume le document suivant et liste les actions:\n${document}` }
}]
})
);typescriptUn serveur MCP est aussi un bon point d'exposition pour d'autres outils d'assistance au code, par exemple un graphe de connaissances de repo comme graphify-graphe-de-connaissances-pour-ia-de-code.
⚡ TL;DR — chaque concept en une ligne
Host ✓ L'application qui contient le LLM et orchestre tout. ⚠ Il peut y avoir plusieurs Clients dans un Host, mais un Client est en vis-à-vis avec un seul Server.
Client ✓ Le composant inside le Host qui dialogue avec un MCP Server. ⚠ N'est pas l'utilisateur final, c'est une abstraction interne au Host.
Server ✓ Processus léger qui expose Tools/Resources/Prompts via le protocole MCP. ⚠ Ne pilote pas le LLM, ne prend pas de décision : il répond uniquement quand le Client l'appelle.
Tool ✓ Action exécutable par le LLM (effet de bord : écriture, API call, calcul). ⚠ Résultat retourné au LLM, pas directement à l'utilisateur.
Resource ✓ Données en lecture seule exposées au LLM (fichiers, DB rows, pages web). ⚠ Pas interactif : le LLM lit, mais ne "call" pas une Resource comme un Tool.
Prompt ✓ Template de message réutilisable, stocké côté Server, invocable par le Host. ⚠ Optionnel dans la majorité des implémentations, souvent sous-utilisé.
🎓 À retenir
"fait quelque chose") génère des appels mal formés ou des hallucinations. Chaque paramètre doit avoir une description précise avec le format attendu.@Tool Spring AI sont découverts par réflexion, pas par le contexte Spring — une méthode @Tool sur une classe non gérée par MethodToolCallbackProvider est silencieusement ignorée.