Notes pratiques : Spring AI + MCP : Mettez vos outils Java à la disposition de n’importe quel client d’IA (même
Guide pas à pas fonctionnel des notes pratiques : Spring AI + MCP : Exposer vos outils Java à n’importe quel client d’IA (y compris des contrats, des vérifications et des emplacements de code intégrables pour les équipes utilisant ce modèle).
Ce guide reconstitue le parcours allant des matières premières à un système fonctionnel pour : Spring AI + MCP : Exposer vos outils Java à n’importe quel client d’IA (même Claude Desktop). L’accent est mis sur des étapes opérationnelles, des vérifications explicites, ainsi que du code que vous pouvez intégrer directement dans un dépôt sans avoir à deviner son intention.
Un guide pratique sur le Model Context Protocol (MCP) avec Spring AI 2.0. Créez un serveur MCP qui expose des outils de paiement via SSE, puis un client d’IA distinct qui les découvre et les invoque — aucune définition d’outil nécessaire du côté client.
Pour le guide pratique A, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans avoir à lire l’ensemble du système. Préférez des sorties structurées avec validation de schéma plutôt que du texte libre lorsque l’étape suivante consiste en du code ou une appel à outil.
Le problème
Pour l’étape « Le Problème », définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez ensemble le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie du produit, et non d’une mise en forme ultérieure. Authentifiez au niveau du gateway et réautorisez au niveau du plan de données. Un token porteur seul ne constitue pas une frontière entre les tenants.
Qu’est-ce que MCP ?
Pour l’étape « Qu’est-ce que MCP ? », définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables aux scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Authentifiez au niveau du gateway et réautorisez au niveau du plan de données. Un token porteur seul ne constitue pas une frontière entre les tenants.
Ce que nous construisons
Pour l’étape « Ce que nous construisons », définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définissez des vérifications de succès et refusez toute exécution partielle silencieuse. Authentifiez-vous au niveau du gateway et réautorisez-vous au niveau du plan de données. Un token porteur seul ne constitue pas une frontière entre les tenants.
Prérequis
Pendant l’étape des Prérequis, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût des jetons ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le parcours passe de l’environnement de démonstration à des environnements partagés. Authentifiez-vous au niveau du gateway et réautorisez-vous au niveau du plan de données. Un simple jeton porteur ne constitue pas une frontière entre les tenants. Pendant l’étape des Prérequis, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours optimal et le parcours de récupération. Les tentatives de répétition, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations ultérieures.
Mise en place du projet
Lors de la phase d’installation du projet, notez d’abord les éléments essentiels : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables à des scripts complexes. Lorsqu’une étape échoue, l’erreur doit indiquer une seule responsabilité et non un processus embrouillé. Enregistrez le nom de l’outil, le hash des arguments, la latence et le résultat de chaque appel. Sans ces traces, les boucles d’analyse prendront des heures inutilement.
POM parent
Lors de la phase du POM parent, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès, et refusez les terminations partielles silencieuses. Enregistrez le nom de l’outil, le hash des arguments, la latence et le résultat de chaque appel. Sans ce suivi, les boucles d’agent de débogage gaspillent des heures.
<groupId>com.anupam</groupId>
<artifactId>spring-ai-mcp</artifactId>
<packaging>pom</packaging>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<modules>
<module>mcp-server</module>
<module>mcp-client</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Partie 1 : Construction du serveur MCP
Lorsque vous travaillez sur la première partie relative à l’élaboration de la plateforme, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des jetons ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite des factures inattendues lorsque le processus passe de la démonstration aux environnements partagés. Conservez enregistré le nom de l’outil, son hash des arguments, la latence et le résultat de chaque appel. Sans ce suivi, le débogage de boucles d’agent peut coûter des heures.
Dépendances serveur
Lors de la phase des dépendances serveur, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les administrateurs peuvent auditer sans devoir lire l’ensemble du schéma. Enregistrez le nom de l’outil, le hash des arguments, la latence et le résultat de chaque appel. Sans cette trace, les boucles du agent de débogage entraînent des pertes de temps considérables.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
Configuration du serveur
Lors de la phase de configuration du serveur, notez d’abord les éléments essentiels : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez à la fois le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations apportées ultérieurement. Enregistrez le nom de l’outil, son hash des arguments, la latence et le résultat de chaque appel. Sans ces traces, les boucles d’analyse des erreurs perdent des heures précieuses.
spring:
ai:
mcp:
server:
name: payment-tools-server
version: 1.0.0
description: "MCP server exposing payment lookup and exchange rate tools"
server:
port: 8081
Définir des outils avec @Tool
Lors de l’étape Définir les outils avec des outils, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables à des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Enregistrez le nom de l’outil, le hash des arguments, la latence et le résultat de chaque appel. Sans cette trace, les boucles d’agent de débogage gaspillent des heures.
@Component
public class PaymentMcpTools {
@Tool(description = "Get the current status and details of a payment by its transaction ID")
public PaymentInfo getPaymentStatus(
@ToolParam(description = "Transaction ID, e.g. TXN-9042") String transactionId) {
PaymentInfo info = payments.get(transactionId);
if (info == null) {
throw new RuntimeException("Payment not found: " + transactionId);
}
return info;
}
@Tool(description = "Get the current exchange rate between two currencies. " +
"Supported: USD, EUR, GBP, JPY, INR, CAD, AUD")
public ExchangeRate getExchangeRate(
@ToolParam(description = "Source currency code, e.g. USD") String from,
@ToolParam(description = "Target currency code, e.g. EUR") String to) {
// ... conversion logic
return new ExchangeRate(from, to, rate, LocalDateTime.now());
}
@Tool(description = "Calculate the total amount in a target currency for a given payment")
public String convertPaymentAmount(
@ToolParam(description = "Transaction ID") String transactionId,
@ToolParam(description = "Target currency code") String targetCurrency) {
PaymentInfo payment = getPaymentStatus(transactionId);
ExchangeRate rate = getExchangeRate(payment.currency(), targetCurrency);
BigDecimal converted = payment.amount().multiply(rate.rate());
return String.format("%s %s = %s %s", payment.amount(), payment.currency(),
converted, targetCurrency);
}
}
Démarrer le serveur
Lors de l’étape « Démarrer le serveur », notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès, et refusez les terminations partielles silencieuses. Enregistrez le nom de l’outil, le hash des arguments, la latence et le résultat de chaque appel. Sans ce suivi, les boucles d’analyse des erreurs perdent des heures.
cd mcp-server
./mvnw spring-boot:run
Partie 2 : Création du client MCP
Lorsque vous travaillez sur la partie 2 relative à la construction de l’interface, notez d’abord les exigences : entrées requises, signal de succès, et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des jetons ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe de l’environnement de démonstration à des environnements partagés. Conservez le nom outil, l’hash des arguments, la latence et le résultat de chaque appel. Sans ce suivi, le débogage de boucles d’agent prend des heures inutilement.
Dépendances du client
Lors de la phase des dépendances du client, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les administrateurs peuvent auditer sans devoir lire l’ensemble du système. Enregistrez le nom outil, le hash des arguments, la latence et le résultat de chaque appel. Sans ces traces, le débogage devient une tâche fastidieuse qui consomme des heures.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
</dependencies>
Configuration du client
Lors de la phase de configuration du client, notez d’abord les éléments requis par le contrat : les données nécessaires, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez en même temps le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations apportées ultérieurement. Enregistrez le nom de l’outil, le hash des arguments, la latence et le résultat de chaque appel. Sans ces traces, le débogage des boucles d’agent prend des heures inutilement.
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.3
mcp:
client:
sse:
connections:
payment-tools:
url: http://localhost:8081
server:
port: 8080
Intégrer les outils MCP dans ChatClient
Lorsque vous travaillez à intégrer les outils Wire MCP, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’erreur doit indiquer une seule responsabilité et non un processus embrouillé. Enregistrez le nom de l’outil, le hash des arguments, la latence et le résultat de chaque appel. Sans cette trace, le débogage de boucles d’agent prend des heures.
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel, SyncMcpToolCallbackProvider mcpTools) {
return ChatClient.builder(chatModel)
.defaultSystem("""
You are a helpful payments assistant. You have access to tools
provided by an MCP server. Use them to look up payment status,
get exchange rates, and convert payment amounts.
""")
.defaultTools(mcpTools)
.build();
}
}
Le contrôleur (aucune connaissance des outils nécessaire)
@RestController
@RequestMapping("/api/v1/assistant")
public class AssistantController {
private final ChatClient chatClient;
public AssistantController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@Valid @RequestBody ChatRequest request) {
String answer = chatClient.prompt()
.user(request.message())
.call()
.content();
return ResponseEntity.ok(ChatResponse.of(answer));
}
}
Tests
1. Démarrer les deux applications
# Terminal 1 — MCP Server
cd mcp-server
./mvnw spring-boot:run
# Terminal 2 - MCP Client
cd mcp-client
export OPENAI_API_KEY=sk-your-key-here
./mvnw spring-boot:run
2. Interroger via le client
curl -X POST http://localhost:8080/api/v1/assistant/chat \
-H "Content-Type: application/json" \
-d '{"message": "What is the status of payment TXN-9042?"}'
{
"answer": "Payment TXN-9042 has been completed. It was $250.00 USD sent from Alice Johnson to Bob Smith on August 20, 2026.",
"timestamp": "2026-08-22T11:00:00"
}
3. Utiliser un outil qui enchaîne d’autres outils
curl -X POST http://localhost:8080/api/v1/assistant/chat \
-H "Content-Type: application/json" \
-d '{"message": "Convert TXN-9043 amount to EUR"}'
{
"answer": "Payment TXN-9043 is $1,200.50 USD. At the current rate of 0.92, that equals approximately €1,104.46 EUR.",
"timestamp": "2026-08-22T11:00:05"
}
MCP contre outils locaux : quand en utiliser un ou l’autre
Combinaison d’outils locaux et MCP
chatClient.prompt()
.user(question)
.tools(localTools, mcpTools) // Both in one call
.call()
.content();
Options de transport
Exemplestdio (connexion à un serveur MCP basé sur npm) :
spring:
ai:
mcp:
client:
stdio:
connections:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
Considérations de sécurité
// Only allow specific tools from the MCP server
@Bean
McpToolFilter mcpToolFilter() {
return (serverName, toolName) ->
Set.of("getPaymentStatus", "getExchangeRate").contains(toolName);
}
Problèmes courants
Exemple complet fonctionnel
git clone https://github.com/AnupamSinha/spring-boot-examples/tree/main/06-ai-mcp
cd spring-ai-mcp
# Terminal 1 - Start server
cd mcp-server && ../mvnw spring-boot:run
# Terminal 2 - Start client
cd mcp-client && export OPENAI_API_KEY=sk-... && ../mvnw spring-boot:run