🎯 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 Claude Desktop, Cursor, n'importe quel agent compatible — sans modification. C'est exactement le même pari que LSP a réussi pour les éditeurs de code.
flowchart LR
subgraph Host ["🖥️ Host (ex: Claude Desktop)"]
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(HTTP Streamable — distant)"]
T3["Tool: search_jira"]
R1["Resource: jira://tickets"]
P1["Prompt: bug-report"]
end
C1 -- "stdin/stdout" --> S1
C2 -- "HTTP + SSE" --> 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 parle à exactement un Server. 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.
sequenceDiagram
actor User
participant Host
participant LLM
participant Client
participant Server
User->>Host: "Cherche les tickets Jira ouverts"
Host->>LLM: user message + liste des tools disponibles
LLM-->>Host: tool_use { name: "search_jira", input: { status: "open" } }
Host->>Client: invoke tool
Client->>Server: tools/call (JSON-RPC)
Server-->>Client: { content: [...tickets] }
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 | HTTP Streamable |
|---|---|---|
| Usage | Dev local, CLI tools, processes enfants | Servers distants, multi-clients, prod |
| Communication | stdin/stdout JSON-RPC | HTTP POST (requests) + SSE (notifications) |
| Auth | N/A — même machine | Bearer token, OAuth 2.1 |
| Déploiement | Lancé par le Host comme subprocess | Service autonome, Dockerisé |
🚨 HTTP Streamable ≠ SSE pur
Le transport HTTP Streamable utilise deux canaux : HTTP POST pour les requêtes client→server, SSE pour les notifications server→client (progress, logs). Ne pas confondre avec un endpoint SSE unidirectionnel — c'est bidirectionnel avec des canaux asymétriques.
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: "scdp-mcp", // ✓ identifiant du server
version: "1.0.0",
});
server.tool(
"get_stock_level",
"Retourne le niveau de stock d'un article en zone SSB/COB/GTAB",
{
sku: z.string().describe("Code article"),
zone: z.enum(["SSB", "COB", "GTAB"]).optional(),
},
async ({ sku, zone }) => {
const stock = await fetchStockFromBigQuery(sku, zone); // ⚠️ appel async réel
return {
content: [{ type: "text", text: JSON.stringify(stock) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);typescriptPour HTTP Streamable, remplacer le transport :
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use(express.json());
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
app.post("/mcp", (req, res) => transport.handleRequest(req, res));
app.get("/mcp", (req, res) => transport.handleRequest(req, res)); // ✓ SSE endpoint
await server.connect(transport);
app.listen(3000);typescript<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>xml@Configuration
public class McpToolsConfig {
@Bean
public ToolCallbackProvider stockTools(StockService stockService) {
return MethodToolCallbackProvider.builder()
.toolObjects(stockService) // ✓ expose les @Tool de StockService
.build();
}
}
@Service
public class StockService {
@Tool(description = "Retourne le niveau de stock d'un article")
public StockResult getStockLevel(
@ToolParam(description = "Code article") String sku,
@ToolParam(description = "Zone SSB/COB/GTAB") String zone) {
// ...
}
}javaspring:
ai:
mcp:
server:
name: scdp-mcp
version: 1.0.0
transport: STDIO # ou HTTP
sse-message-endpoint: /mcp/messages # si HTTPyaml🚨 @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.
MCP 2025-03-26 introduit OAuth 2.1 comme mécanisme d'auth standard pour les transports HTTP. Pour un usage interne (SCDP), un Bearer token statique avec scope sur les tools autorisés est suffisant en V1.
Client → Authorization Server : demande token (PKCE flow)
Client → MCP Server : Bearer token dans Authorization header
MCP Server → Authorization Server : introspection/validation
Bonne pratique — scopes par tool
Déclarer des scopes fins (stock:read, picking:write) plutôt qu'un token global. Ça permet de restreindre ce qu'un agent peut faire sans toucher au code du Server.
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://warehouse-zones",
"Liste des zones et leurs capacités",
async (uri) => ({
contents: [{ uri: uri.href, text: JSON.stringify(warehouseConfig) }],
})
);
// Prompt — template réutilisable pour tâches répétitives
server.prompt(
"analyze-receiving-document",
"Analyse un bon de réception et extrait les lignes articles",
{ document: z.string() },
({ document }) => ({
messages: [{
role: "user",
content: { type: "text", text: `Extrais les lignes du document suivant:\n${document}` }
}]
})
);typescript⚡ TL;DR — chaque concept en une ligne
Host ✓ L'application qui contient le LLM et orchestre tout (ex: Claude Desktop, app Next.js). ⚠ Il peut y avoir plusieurs Clients dans un Host, mais un Client parle à un seul Server.
Client ✓ Le composant inside le Host qui maintient une connexion 1
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.