Partager via


Tutoriel : Déployer un serveur MCP Java sur Azure Container Apps

Dans ce tutoriel, vous créez un serveur MCP (Model Context Protocol) qui expose des outils de gestion des tâches à l’aide de Spring Boot et du KIT de développement logiciel (SDK) Java MCP. Vous déployez le serveur sur Azure Container Apps et vous y connectez à partir de GitHub Copilot Chat dans VS Code.

Dans ce tutoriel, vous allez :

  • Créer une application Spring Boot qui expose les outils MCP
  • Tester le serveur MCP localement avec GitHub Copilot
  • Conteneuriser et déployer l’application sur Azure Container Apps
  • Connecter GitHub Copilot au serveur MCP déployé

Prerequisites

Créer la structure de l’application

Dans cette section, vous allez créer un projet Spring Boot avec le Kit de développement logiciel (SDK) Java MCP.

  1. Créez le répertoire du projet :

    mkdir tasks-mcp-server && cd tasks-mcp-server
    
  2. Créer pom.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.3.0</version>
        </parent>
    
        <groupId>com.example</groupId>
        <artifactId>tasks-mcp-server</artifactId>
        <version>1.0.0</version>
        <name>tasks-mcp-server</name>
        <description>MCP server for task management on Azure Container Apps</description>
    
        <properties>
            <java.version>17</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>io.modelcontextprotocol.sdk</groupId>
                <artifactId>mcp-spring-webmvc</artifactId>
                <version>0.10.0</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Définit pom.xml une application Spring Boot avec deux dépendances clés : spring-boot-starter-web pour l’infrastructure web et mcp-spring-webmvc pour le Kit de développement logiciel (SDK) MCP. Le plug-in Spring Boot Maven empaque l’application en tant que fichier JAR exécutable.

    Note

    Le SDK Java MCP est en cours de développement actif. Vérifiez les versions du Kit de développement logiciel (SDK) Java MCP pour obtenir la dernière version et mettez à jour la <version> version en conséquence.

  3. Créez la structure de répertoires :

    mkdir -p src/main/java/com/example/tasksmcp
    mkdir -p src/main/resources
    
  4. Créer src/main/resources/application.properties:

    server.port=8080
    

Définir le modèle de données et le stockage

Dans cette section, vous définissez le modèle de données de tâche et un magasin en mémoire.

  1. Créer src/main/java/com/example/tasksmcp/TaskItem.java:

    package com.example.tasksmcp;
    
    import java.time.Instant;
    
    public class TaskItem {
        private int id;
        private String title;
        private String description;
        private boolean isComplete;
        private Instant createdAt;
    
        public TaskItem(int id, String title, String description, boolean isComplete) {
            this.id = id;
            this.title = title;
            this.description = description;
            this.isComplete = isComplete;
            this.createdAt = Instant.now();
        }
    
        // Getters and setters
        public int getId() { return id; }
        public String getTitle() { return title; }
        public String getDescription() { return description; }
        public boolean isComplete() { return isComplete; }
        public void setComplete(boolean complete) { isComplete = complete; }
        public Instant getCreatedAt() { return createdAt; }
    }
    

    La TaskItem classe définit le modèle de données avec des getters standard et un setter pour l’état d’achèvement. Le constructeur initialise automatiquement l’horodatage createdAt .

  2. Créer src/main/java/com/example/tasksmcp/TaskStore.java:

    package com.example.tasksmcp;
    
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;
    import java.util.concurrent.atomic.AtomicInteger;
    
    @Component
    public class TaskStore {
    
        private final List<TaskItem> tasks = new ArrayList<>();
        private final AtomicInteger nextId = new AtomicInteger(3);
    
        public TaskStore() {
            tasks.add(new TaskItem(1, "Buy groceries", "Milk, eggs, bread", false));
            tasks.add(new TaskItem(2, "Write docs", "Draft the MCP tutorial", true));
        }
    
        public List<TaskItem> getAll() {
            return List.copyOf(tasks);
        }
    
        public Optional<TaskItem> getById(int id) {
            return tasks.stream().filter(t -> t.getId() == id).findFirst();
        }
    
        public TaskItem create(String title, String description) {
            TaskItem task = new TaskItem(nextId.getAndIncrement(), title, description, false);
            tasks.add(task);
            return task;
        }
    
        public Optional<TaskItem> toggleComplete(int id) {
            return getById(id).map(task -> {
                task.setComplete(!task.isComplete());
                return task;
            });
        }
    
        public boolean delete(int id) {
            return tasks.removeIf(t -> t.getId() == id);
        }
    }
    

    Le TaskStore composant Spring gère une liste en mémoire préremplie avec des exemples de données. Il utilise AtomicInteger pour la génération d’ID thread-safe et fournit des méthodes pour les opérations CRUD standard.

Définir les outils MCP

Dans cette section, vous définissez les outils MCP que le modèle IA peut appeler et configurer le serveur MCP dans votre application Spring Boot.

  1. Créer src/main/java/com/example/tasksmcp/TasksMcpTools.java:

    package com.example.tasksmcp;
    
    import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
    import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
    import io.modelcontextprotocol.spec.McpSchema.TextContent;
    import io.modelcontextprotocol.spec.McpSchema.Tool;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.node.ObjectNode;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    import java.util.Map;
    
    @Component
    public class TasksMcpTools {
    
        private final TaskStore store;
        private final ObjectMapper objectMapper = new ObjectMapper();
    
        public TasksMcpTools(TaskStore store) {
            this.store = store;
        }
    
        public List<SyncToolSpecification> getToolSpecifications() {
            return List.of(
                listTasksTool(),
                getTaskTool(),
                createTaskTool(),
                toggleTaskCompleteTool(),
                deleteTaskTool()
            );
        }
    
        private SyncToolSpecification listTasksTool() {
            var tool = new Tool(
                "list_tasks",
                "List all tasks with their ID, title, description, and completion status.",
                emptySchema()
            );
            return new SyncToolSpecification(tool, (exchange, request) -> {
                try {
                    String json = objectMapper.writeValueAsString(store.getAll());
                    return new CallToolResult(List.of(new TextContent(json)), false);
                } catch (Exception e) {
                    return errorResult(e.getMessage());
                }
            });
        }
    
        private SyncToolSpecification getTaskTool() {
            var tool = new Tool(
                "get_task",
                "Get a single task by its numeric ID.",
                objectSchema(Map.of(
                    "task_id", propertySchema("integer", "The numeric ID of the task to retrieve")
                ), List.of("task_id"))
            );
            return new SyncToolSpecification(tool, (exchange, request) -> {
                int taskId = ((Number) request.arguments().get("task_id")).intValue();
                return store.getById(taskId)
                    .map(task -> {
                        try {
                            return new CallToolResult(
                                List.of(new TextContent(objectMapper.writeValueAsString(task))), false);
                        } catch (Exception e) {
                            return errorResult(e.getMessage());
                        }
                    })
                    .orElse(textResult("Task with ID " + taskId + " not found."));
            });
        }
    
        private SyncToolSpecification createTaskTool() {
            var tool = new Tool(
                "create_task",
                "Create a new task with the given title and description. Returns the created task.",
                objectSchema(Map.of(
                    "title", propertySchema("string", "A short title for the task"),
                    "description", propertySchema("string", "A detailed description of what the task involves")
                ), List.of("title", "description"))
            );
            return new SyncToolSpecification(tool, (exchange, request) -> {
                String title = (String) request.arguments().get("title");
                String description = (String) request.arguments().get("description");
                TaskItem task = store.create(title, description);
                try {
                    return new CallToolResult(
                        List.of(new TextContent(objectMapper.writeValueAsString(task))), false);
                } catch (Exception e) {
                    return errorResult(e.getMessage());
                }
            });
        }
    
        private SyncToolSpecification toggleTaskCompleteTool() {
            var tool = new Tool(
                "toggle_task_complete",
                "Toggle a task's completion status between complete and incomplete.",
                objectSchema(Map.of(
                    "task_id", propertySchema("integer", "The numeric ID of the task to toggle")
                ), List.of("task_id"))
            );
            return new SyncToolSpecification(tool, (exchange, request) -> {
                int taskId = ((Number) request.arguments().get("task_id")).intValue();
                return store.toggleComplete(taskId)
                    .map(task -> textResult(
                        "Task " + task.getId() + " is now " + (task.isComplete() ? "complete" : "incomplete") + "."))
                    .orElse(textResult("Task with ID " + taskId + " not found."));
            });
        }
    
        private SyncToolSpecification deleteTaskTool() {
            var tool = new Tool(
                "delete_task",
                "Delete a task by its numeric ID.",
                objectSchema(Map.of(
                    "task_id", propertySchema("integer", "The numeric ID of the task to delete")
                ), List.of("task_id"))
            );
            return new SyncToolSpecification(tool, (exchange, request) -> {
                int taskId = ((Number) request.arguments().get("task_id")).intValue();
                boolean deleted = store.delete(taskId);
                return textResult(deleted
                    ? "Task " + taskId + " deleted."
                    : "Task with ID " + taskId + " not found.");
            });
        }
    
        // Helper methods for JSON Schema construction
        private String emptySchema() {
            return "{\"type\":\"object\",\"properties\":{}}";
        }
    
        private String objectSchema(Map<String, String> properties, List<String> required) {
            ObjectNode schema = objectMapper.createObjectNode();
            schema.put("type", "object");
    
            ObjectNode propsNode = objectMapper.createObjectNode();
            for (var entry : properties.entrySet()) {
                try {
                    propsNode.set(entry.getKey(), objectMapper.readTree(entry.getValue()));
                } catch (Exception e) {
                    propsNode.putObject(entry.getKey()).put("type", "string");
                }
            }
            schema.set("properties", propsNode);
            schema.set("required", objectMapper.valueToTree(required));
    
            return schema.toString();
        }
    
        private String propertySchema(String type, String description) {
            return "{\"type\":\"" + type + "\",\"description\":\"" + description + "\"}";
        }
    
        private CallToolResult textResult(String text) {
            return new CallToolResult(List.of(new TextContent(text)), false);
        }
    
        private CallToolResult errorResult(String message) {
            return new CallToolResult(List.of(new TextContent("Error: " + message)), true);
        }
    }
    

    Note

    La Surface de l'API du SDK Java MCP se développe. Le modèle d’inscription d’outil présenté ici utilise SyncToolSpecification pour les outils synchrones. Consultez la documentation du Kit de développement logiciel (SDK) Java MCP pour connaître les dernières idiomes et annotations qui peuvent simplifier l’inscription des outils.

  2. Créer src/main/java/com/example/tasksmcp/McpConfig.java:

    package com.example.tasksmcp;
    
    import io.modelcontextprotocol.server.McpServer;
    import io.modelcontextprotocol.server.McpSyncServer;
    import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
    import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class McpConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            // For production, restrict allowedOrigins to specific trusted domains
            registry.addMapping("/**")
                    .allowedOrigins("*")
                    .allowedMethods("GET", "POST", "DELETE", "OPTIONS")
                    .allowedHeaders("*");
        }
    
        @Bean
        public WebMvcSseServerTransportProvider mcpTransportProvider() {
            return new WebMvcSseServerTransportProvider(new com.fasterxml.jackson.databind.ObjectMapper(), "/mcp");
        }
    
        @Bean
        public McpSyncServer mcpServer(WebMvcSseServerTransportProvider transport, TasksMcpTools tools) {
            McpSyncServer server = McpServer.sync(transport)
                .serverInfo("TasksMCP", "1.0.0")
                .capabilities(ServerCapabilities.builder().tools(true).build())
                .build();
    
            tools.getToolSpecifications().forEach(server::addTool);
    
            return server;
        }
    }
    

    Points clés :

    • WebMvcSseServerTransportProvider enregistre le transport SSE au chemin /mcp .
    • McpServer.sync(transport) configure les fonctionnalités de l’outil et inscrit chaque spécification d’outil.
    • CORS est activé, car GitHub Copilot dans VS Code effectue des demandes d’origine croisée aux serveurs MCP.

    Note

    Ce tutoriel utilise WebMvcSseServerTransportProvider (transport SSE) car le SDK Java MCP n’offre pas encore de transport HTTP pouvant être diffusé en continu stable. Les autres didacticiels de langage (.NET, Python, Node.js) utilisent http streamable. Lorsque le Kit de développement logiciel (SDK) Java ajoute la prise en charge HTTP streamable, mettez à jour le fournisseur de transport en conséquence. Le transport SSE est entièrement compatible avec VS Code Copilot et d’autres clients MCP.

  3. Créer src/main/java/com/example/tasksmcp/TasksMcpApplication.java:

    package com.example.tasksmcp;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @SpringBootApplication
    @RestController
    public class TasksMcpApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(TasksMcpApplication.class, args);
        }
    
        @GetMapping("/health")
        public String health() {
            return "healthy";
        }
    }
    

    La classe principale démarre l’application Spring Boot et expose un point de terminaison /health pour les sondes d’intégrité Container Apps. Les points de terminaison MCP retournent des réponses JSON-RPC. Il est donc nécessaire d'avoir un point de terminaison distinct pour la vérification de l'intégrité.

Tester le serveur MCP localement

Avant de déployer sur Azure, vérifiez que le serveur MCP fonctionne en l’exécutant localement et en vous connectant à partir de GitHub Copilot.

  1. Générez et exécutez :

    mvn spring-boot:run
    
  2. Ouvrez VS Code, puis ouvrez Copilot Chat et sélectionnez le mode Agent .

  3. Sélectionnez le bouton Outils , puis Ajoutez d’autres outils...>Ajoutez le serveur MCP.

  4. Sélectionnez HTTP (HTTP ou événements envoyés par le serveur) et choisissez événements envoyés par le serveur (SSE) lorsqu'on vous demande de choisir le type de transport.

    Important

    Ce tutoriel utilise le transport SSE, et n’est pas accessible en continu HTTP. Vous devez sélectionner l’option SSE dans VS Code pour que la connexion fonctionne correctement.

  5. Entrez l’URL du serveur : http://localhost:8080/mcp

  6. Entrez un ID de serveur : tasks-mcp

  7. Sélectionnez Paramètres de l’espace de travail.

  8. Tester avec : « Afficher toutes les tâches »

  9. Sélectionnez Continuer lorsque Copilot demande la confirmation de l’outil MCP.

Vous devez voir la liste des tâches retournée par votre mémoire vive.

Conseil / Astuce

Essayez d'autres invites comme « Créer une tâche pour examiner la demande de tirage », « Marquer la tâche 1 comme terminée » ou « Supprimer la tâche 2 ».

Conteneuriser l’application

Empaqueter l’application en tant que conteneur Docker afin de pouvoir la tester localement avant de le déployer sur Azure.

  1. Créez un Dockerfile avec une build multiphase :

    FROM maven:3.9-eclipse-temurin-17-alpine AS build
    WORKDIR /app
    COPY pom.xml .
    RUN mvn dependency:go-offline -B
    COPY src/ src/
    RUN mvn package -DskipTests -B
    
    FROM eclipse-temurin:17-jre-alpine
    WORKDIR /app
    COPY --from=build /app/target/*.jar app.jar
    EXPOSE 8080
    ENTRYPOINT ["java", "-jar", "app.jar"]
    

    La build multiphase maintient la taille réduite de l'image finale en séparant la build Maven de l'image d'exécution.

  2. Vérifiez localement :

    docker build -t tasks-mcp-server .
    docker run -p 8080:8080 tasks-mcp-server
    

    Confirmez que le point de terminaison d'intégrité répond : curl http://localhost:8080/health

Déployer sur Azure Container Apps

Après avoir conteneurisé l’application, déployez-la sur Azure Container Apps à l’aide d’Azure CLI. La az containerapp up commande génère l’image conteneur dans le cloud. Vous n’avez donc pas besoin de Docker sur votre machine pour cette étape.

  1. Définissez des variables d’environnement :

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-java"
    
  2. Créez un groupe de ressources et un environnement Container Apps :

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. Déployez l’application conteneur :

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  4. Configurer CORS :

    az containerapp ingress cors enable \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --allowed-origins "*" \
        --allowed-methods "GET,POST,DELETE,OPTIONS" \
        --allowed-headers "*"
    

    Note

    Pour la production, remplacez les origines génériques par des origines approuvées spécifiques. Consultez les serveurs MCP sécurisés sur Container Apps.

  5. Vérifiez le déploiement :

    APP_URL=$(az containerapp show \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --query "properties.configuration.ingress.fqdn" -o tsv)
    
    curl https://$APP_URL/health
    

Connecter GitHub Copilot au serveur déployé

Maintenant que le serveur MCP s’exécute dans Azure, configurez VS Code pour connecter GitHub Copilot au point de terminaison déployé.

  1. Créer ou mettre à jour .vscode/mcp.json:

    {
        "servers": {
            "tasks-mcp-server": {
                "type": "sse",
                "url": "https://<your-app-fqdn>/mcp"
            }
        }
    }
    

    Remplacez <your-app-fqdn> par le FQDN de la sortie du déploiement.

    Important

    Le "type" doit être "sse" car ce didacticiel utilise le transport SSE. L’utilisation de "http" (le HTTP diffusable) provoque des échecs de connexion.

  2. Dans VS Code, ouvrez Copilot Chat en mode Agent.

  3. Vérifiez que tasks-mcp-server apparaît dans la liste des outils. Sélectionnez Démarrer si nécessaire.

  4. Testez avec une invite telle que « Quelles tâches ai-je ? »

Configurer la mise à l’échelle pour une utilisation interactive

Les applications Java ont plus de temps de démarrage à froid. Définissez un nombre minimal de réplicas pour que la machine virtuelle JVM soit chaude :

az containerapp update \
    --name $APP_NAME \
    --resource-group $RESOURCE_GROUP \
    --min-replicas 1

Conseil / Astuce

Tenez compte des paramètres de ressources appropriés pour la machine virtuelle JVM. Un minimum de 1 processeur virtuel et 2 Gio de mémoire est recommandé pour les applications Spring Boot.

Considérations relatives à la sécurité

Ce tutoriel utilise un serveur MCP non authentifié pour plus de simplicité. Avant d’exécuter un serveur MCP en production, passez en revue les recommandations suivantes. Lorsque votre serveur MCP est appelé par un agent alimenté par de grands modèles de langage (LLMs), tenez compte des attaques par injection de requête.

  • Authentification et autorisation : Sécurisez votre serveur MCP avec l’ID Microsoft Entra. Consultez les serveurs MCP sécurisés sur Container Apps.
  • Validation d’entrée : Utilisez la validation bean (@Valid, , @NotNull@Size) pour la validation des paramètres d’outil. Consultez Validation dans Spring Boot.
  • HTTPS : Azure Container Apps applique HTTPS par défaut avec des certificats TLS automatiques.
  • Privilège minimum : exposez uniquement les outils dont votre cas d’usage a besoin. Évitez les outils qui effectuent des opérations destructrices sans confirmation.
  • CORS : Restreindre les origines autorisées aux domaines de confiance en production.
  • Journalisation et surveillance : appel d’outils MCP de journalisation pour l’audit à l’aide de SLF4J et d’Azure Monitor.

Nettoyer les ressources

Si vous ne souhaitez pas continuer à utiliser cette application, supprimez le groupe de ressources pour supprimer toutes les ressources créées dans ce tutoriel :

az group delete --resource-group $RESOURCE_GROUP --yes --no-wait

Étape suivante