Zelfstudie: Een Java MCP-server implementeren in Azure Container Apps

In deze zelfstudie bouwt u een MCP-server (Model Context Protocol) die hulpprogramma's voor taakbeheer beschikbaar maakt met behulp van Spring Boot en de MCP Java SDK. U implementeert de server in Azure Container Apps en maakt er verbinding mee vanuit GitHub Copilot Chat in VS Code.

In deze handleiding leert u:

  • Een Spring Boot-app maken die MCP-hulpprogramma's beschikbaar maakt
  • De MCP-server lokaal testen met GitHub Copilot
  • De app containeriseren en implementeren in Azure Container Apps
  • GitHub Copilot verbinden met de geïmplementeerde MCP-server

Vereiste voorwaarden

De scaffold van de app maken

In deze sectie maakt u een nieuw Spring Boot-project met de MCP Java SDK.

  1. Maak de projectmap:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    
  2. Maken 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>
    

    Hiermee pom.xml definieert u een Spring Boot-toepassing met twee belangrijke afhankelijkheden: spring-boot-starter-web voor het webframework en mcp-spring-webmvc voor de MCP SDK. De Spring Boot Maven-invoegtoepassing verpakt de app als een uitvoerbaar JAR-bestand.

    Opmerking

    De MCP Java SDK wordt actief ontwikkeld. Controleer de MCP Java SDK-releases voor de nieuwste versie en werk de versie <version> dienovereenkomstig bij.

  3. Maak de mapstructuur:

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

    server.port=8080
    

Het gegevensmodel definiëren en opslaan

In deze sectie definieert u het taakgegevensmodel en een in-memory archief.

  1. Maken 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; }
    }
    

    De TaskItem klasse definieert het gegevensmodel met standaard getters en een setter voor de voltooiingsstatus. De constructor initialiseert de createdAt tijdstempel automatisch.

  2. Maken 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);
        }
    }
    

    Het TaskStore Spring-onderdeel beheert een in-memory lijst die vooraf is ingevuld met voorbeeldgegevens. Het gebruikt AtomicInteger voor het genereren van thread-veilige id's en biedt methoden voor standaard CRUD-bewerkingen.

Definieer de MCP-hulpprogramma's

In deze sectie definieert u de MCP-hulpprogramma's die het AI-model kan aanroepen en configureren van de MCP-server in uw Spring Boot-toepassing.

  1. Maken 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);
        }
    }
    

    Opmerking

    Het MCP Java SDK API-oppervlak ontwikkelt zich. Het hier weergegeven patroon voor hulpprogrammaregistratie gebruikt SyncToolSpecification voor synchrone hulpprogramma's. Raadpleeg de MCP Java SDK-documentatie voor de nieuwste idiomen en aantekeningen die de registratie van hulpprogramma's kunnen vereenvoudigen.

  2. Maken 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;
        }
    }
    

    Belangrijke punten:

    • WebMvcSseServerTransportProvider registreert het SSE-transport op het /mcp pad.
    • McpServer.sync(transport) configureert hulpprogrammamogelijkheden en registreert elke hulpprogrammaspecificatie.
    • CORS is ingeschakeld omdat GitHub Copilot in VS Code cross-origin-aanvragen doet voor MCP-servers.

    Opmerking

    In deze zelfstudie wordt gebruikgemaakt WebMvcSseServerTransportProvider van (SSE-transport) omdat de MCP Java SDK nog geen stabiel streambaar HTTP-transport biedt. De andere tutorials (.NET, Python, Node.js) maken gebruik van HTTP-streaming. Wanneer de Java SDK streambare HTTP-ondersteuning toevoegt, werkt u de transportprovider dienovereenkomstig bij. Het SSE-transport is volledig compatibel met VS Code Copilot en andere MCP-clients.

  3. Maken 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";
        }
    }
    

    De hoofdklasse bootstrapt de Spring Boot-toepassing en maakt een /health eindpunt beschikbaar voor Container Apps-statustests. MCP-eindpunten retourneren JSON-RPC antwoorden, dus er is een afzonderlijk statuseindpunt nodig voor statuscontroles.

De MCP-server lokaal testen

Voordat u implementeert in Azure, controleert u of de MCP-server werkt door deze lokaal uit te voeren en verbinding te maken vanuit GitHub Copilot.

  1. Bouwen en uitvoeren:

    mvn spring-boot:run
    
  2. Open VS Code en open Copilot Chat en selecteer Agentmodus.

  3. Selecteer de knop Extra en voeg meer hulpprogramma's toe...>MCP-server toevoegen.

  4. Selecteer HTTP (HTTP of Server-Sent-gebeurtenissen) en kies Server-Sent gebeurtenissen (SSE) wanneer u om het transporttype wordt gevraagd.

    Belangrijk

    In deze zelfstudie wordt gebruikgemaakt van het SSE-transport in plaats van HTTP dat niet kan worden gestreamd. U moet de SSE-optie in VS Code selecteren om de verbinding correct te laten werken.

  5. Voer de server-URL in: http://localhost:8080/mcp

  6. Voer een server-id in: tasks-mcp

  7. Selecteer Werkruimte-instellingen.

  8. Test met: 'Toon alle taken'

  9. Selecteer Doorgaan wanneer Copilot de bevestiging van het MCP-hulpprogramma aanvraagt.

U zou de takenlijst moeten zien die wordt geretourneerd uit uw in-memory opslag.

Aanbeveling

Probeer andere prompts zoals 'Creëer een taak om de PR te controleren', 'Taak 1 markeren als voltooid' of 'Taak 2 verwijderen'.

De toepassing in een container plaatsen

Verpak de toepassing als een Docker-container, zodat u deze lokaal kunt testen voordat u implementeert in Azure.

  1. Maak een Dockerfile met een build met meerdere fasen:

    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"]
    

    De meerfasige build houdt de uiteindelijke afbeelding klein door de Maven-build te scheiden van de runtime-afbeelding.

  2. Lokaal verifiëren:

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

    Controleer of het gezondheidseindpunt reageert: curl http://localhost:8080/health

Implementeren in Azure Container Apps

Nadat u de toepassing in een container hebt geplaatst, implementeert u deze in Azure Container Apps met behulp van de Azure CLI. De opdracht az containerapp up bouwt de containerafbeelding in de cloud, zodat u Docker niet nodig hebt op uw machine voor deze stap.

  1. Omgevingsvariabelen instellen:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-java"
    
  2. Maak een resourcegroep en een Container Apps-omgeving:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. De container-app implementeren:

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

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

    Opmerking

    Vervang wildcards voor productie door specifieke vertrouwde origins. Zie Beveiligde MCP-servers in Container Apps.

  5. Controleer de implementatie:

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

GitHub Copilot verbinden met de geïmplementeerde server

Nu de MCP-server wordt uitgevoerd in Azure, configureert u VS Code om GitHub Copilot te verbinden met het geïmplementeerde eindpunt.

  1. Maken of bijwerken .vscode/mcp.json:

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

    Vervang <your-app-fqdn> door de FQDN uit de implementatie-uitvoer.

    Belangrijk

    Dit "type" moet "sse" zijn omdat in deze handleiding gebruik wordt gemaakt van het SSE-transport. Het gebruik van "http" (streambare HTTP) veroorzaakt verbindingsfouten.

  2. Open Copilot Chat in Agent modus in VS Code.

  3. Controleer of tasks-mcp-server wordt weergegeven in de Hulpmiddelenlijst. Selecteer indien nodig Start.

  4. Test met een prompt zoals 'Welke taken heb ik?'

Configureer schaling voor interactief gebruik

Java-toepassingen hebben langere koude starttijden. Stel een minimumaantal replica's in om de JVM warm te houden:

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

Aanbeveling

Overweeg resource-instellingen die geschikt zijn voor de JVM. Minimaal 1 vCPU- en 2-GiB-geheugen wordt aanbevolen voor Spring Boot-toepassingen.

Beveiligingsoverwegingen

In deze zelfstudie wordt gebruikgemaakt van een niet-geverifieerde MCP-server voor het gemak. Voordat u een MCP-server in productie draait, bekijk de volgende aanbevelingen. Wanneer uw MCP-server wordt aangeroepen door een agent die wordt aangedreven door grote taalmodellen (LLM's), moet u rekening houden met promptinjectieaanvallen .

  • Verificatie en autorisatie: beveilig uw MCP-server met Microsoft Entra-id. Zie Beveiligde MCP-servers in Container Apps.
  • Invoervalidatie: Bean-validatie (@Valid, @NotNull, @Size) gebruiken voor validatie van hulpprogrammaparameters. Zie Validatie in Spring Boot.
  • HTTPS: Azure Container Apps dwingt HTTPS standaard af met automatische TLS-certificaten.
  • Minimale bevoegdheid: alleen de hulpprogramma's beschikbaar maken die uw use-case vereist. Vermijd hulpprogramma's die destructieve bewerkingen uitvoeren zonder bevestiging.
  • CORS: Toegestane oorsprongen beperken tot vertrouwde domeinen in productie.
  • Logboekregistratie en bewaking: MCP-hulpprogramma's aanroepen voor controle met behulp van SLF4J en Azure Monitor.

De hulpbronnen opschonen

Als u deze toepassing niet meer gaat gebruiken, verwijdert u de resourcegroep om alle resources te verwijderen die in deze zelfstudie zijn gemaakt:

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

Volgende stap