Oktatóanyag: Java MCP-kiszolgáló üzembe helyezése az Azure Container Appsben

Ebben az oktatóanyagban létrehoz egy Model Context Protocol (MCP) kiszolgálót, amely a Spring Boot és az MCP Java SDK használatával teszi elérhetővé a feladatkezelő eszközöket. Üzembe helyezheti a kiszolgálót az Azure Container Appsben, és csatlakozhat hozzá a GitHub Copilot Chatből a VS Code-ban.

Ebben az útmutatóban Ön:

  • McP-eszközöket elérhetővé tevő Spring Boot-alkalmazás létrehozása
  • Az MCP-kiszolgáló helyi tesztelése a GitHub Copilottal
  • Az alkalmazás tárolóba helyezése és üzembe helyezése az Azure Container Appsben
  • A GitHub Copilot csatlakoztatása az üzembe helyezett MCP-kiszolgálóhoz

Előfeltételek

Az alkalmazásállvány létrehozása

Ebben a szakaszban egy új Spring Boot-projektet hoz létre az MCP Java SDK-val.

  1. Hozza létre a projektkönyvtárat:

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

    A pom.xml rendszer két fő függőséggel rendelkező Spring Boot-alkalmazást határoz meg: spring-boot-starter-web a webes keretrendszerhez és mcp-spring-webmvc az MCP SDK-hoz. A Spring Boot Maven beépülő modul végrehajtható JAR-ként csomagolja be az alkalmazást.

    Megjegyzés:

    Az MCP Java SDK aktív fejlesztés alatt áll. Ellenőrizze az MCP Java SDK legújabb verzióját, és frissítse ennek <version> megfelelően.

  3. Hozza létre a címtárstruktúrát:

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

    server.port=8080
    

Az adatmodell és az adattár definiálása

Ebben a szakaszban definiálja a tevékenység adatmodellt és egy memórián belüli tárolót.

  1. Létrehoz 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; }
    }
    

    Az TaskItem osztály az adatmodellt standard getterekkel és egy beállítóval határozza meg a befejezési állapothoz. A konstruktor automatikusan inicializálja az createdAt időbélyeget.

  2. Létrehoz 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);
        }
    }
    

    A TaskStore Spring-összetevő a mintaadatokkal előre feltöltött memóriabeli listát kezeli. Szálbiztos azonosítók létrehozására szolgál AtomicInteger , és szabványos CRUD-műveletekhez biztosít metódusokat.

Az MCP-eszközök definiálása

Ebben a szakaszban definiálja azokat az MCP-eszközöket, amelyeket az AI-modell meghívhat és konfigurálhat az MCP-kiszolgálót a Spring Boot-alkalmazásban.

  1. Létrehoz 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);
        }
    }
    

    Megjegyzés:

    Az MCP Java SDK API felülete folyamatosan fejlődik. Az itt látható eszközregisztrációs minta szinkron eszközökhöz használható SyncToolSpecification . Tekintse meg az MCP Java SDK dokumentációjában azokat a legújabb kifejezéseket és széljegyzeteket, amelyek egyszerűsíthetik az eszközregisztrációt.

  2. Létrehoz 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;
        }
    }
    

    Összefoglalás:

    • WebMvcSseServerTransportProvider regisztrálja az SSE-átvitelt az /mcp útvonalon.
    • McpServer.sync(transport) konfigurálja az eszköz képességeit, és regisztrálja az egyes eszközspecifikációkat.
    • A CORS engedélyezve van, mert a VS Code-ban a GitHub Copilot kereszt-forrás kéréseket küld az MCP-kiszolgálóknak.

    Megjegyzés:

    Ez az oktatóanyag azért használ WebMvcSseServerTransportProvider (SSE-átvitelt), mert az MCP Java SDK még nem kínál stabil streamelhető HTTP-átvitelt. A többi nyelvi oktatóanyag (.NET, Python, Node.js) streamelhető HTTP-t használ. Amikor a Java SDK streamelhető HTTP-támogatást ad hozzá, ennek megfelelően frissítse az átviteli szolgáltatót. Az SSE-átvitel teljes mértékben kompatibilis a VS Code Copilot és más MCP-ügyfelekkel.

  3. Létrehoz 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";
        }
    }
    

    A főosztály elindítja a Spring Boot-alkalmazást, és elérhetővé tesz egy /health végpontot a Container Apps állapottesztjeihez. Az MCP-végpontok JSON-RPC válaszokat ad vissza, ezért az állapot-ellenőrzésekhez külön állapotvégpontra van szükség.

Az MCP-kiszolgáló helyi tesztelése

Az Azure-ban való üzembe helyezés előtt ellenőrizze, hogy az MCP-kiszolgáló működik-e helyileg futtatva és a GitHub Copilotból való csatlakozással.

  1. Építés és futtatás:

    mvn spring-boot:run
    
  2. Nyissa meg a VS Code-ot, majd nyissa meg a Copilot-csevegést , és válassza az Ügynök módot.

  3. Válassza az Eszközök gombot, majd a További eszközök hozzáadása...>Adja hozzá az MCP-kiszolgálót.

  4. Válassza az HTTP (HTTP vagy Server-Sent Események) lehetőséget, és válassza a Server-Sent Események (SSE) lehetőséget, amikor a rendszer kéri az átviteli típust.

    Fontos

    Ez az oktatóanyag az SSE-átvitelt használja, nem streamelhető HTTP-t. Ahhoz, hogy a kapcsolat megfelelően működjön, ki kell választania az SSE lehetőséget a VS Code-ban.

  5. Adja meg a kiszolgáló URL-címét: http://localhost:8080/mcp

  6. Adjon meg egy kiszolgálóazonosítót: tasks-mcp

  7. Válassza a Munkaterület beállításai lehetőséget.

  8. Tesztelés a következővel: "Az összes feladat megjelenítése"

  9. Válassza a Folytatás lehetőséget, amikor a Copilot az MCP eszköz megerősítését kéri.

Látnia kell a memóriabeli tárolóból visszaadott feladatlistát.

Jótanács

Próbálkozzon más kérdésekkel, például a "Feladat létrehozása a kérelem áttekintéséhez", az "1. tevékenység megjelölése befejezettként" vagy a "2. tevékenység törlése" kérdésekkel.

Az alkalmazás tárolóba helyezése

Csomagolja be az alkalmazást Docker-tárolóként, hogy helyileg tesztelhesse, mielőtt üzembe helyezené az Azure-ban.

  1. Hozzon létre egy Dockerfile többfázisú buildet:

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

    A többfázisú build a Maven-buildnek a futásidejű képtől való elválasztásával kis méretűvé teszi a végső képet.

  2. Helyi ellenőrzés:

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

    Ellenőrizze, hogy az állapotvégpont válaszol-e: curl http://localhost:8080/health

Üzembe helyezés az Azure Container Appsben

Az alkalmazás tárolóba helyezése után helyezze üzembe az Azure Container Appsben az Azure CLI használatával. A az containerapp up parancs létrehozza a tárolórendszerképet a felhőben, így ehhez a lépéshez nincs szükség a Dockerre a gépen.

  1. Környezeti változók beállítása:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-java"
    
  2. Erőforráscsoport és Container Apps-környezet létrehozása:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. A tárolóalkalmazás üzembe helyezése:

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

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

    Megjegyzés:

    A gyártásban cserélje le a helyettesítő eredeteket meghatározott, megbízható eredetekre. Lásd: Biztonságos MCP-kiszolgálók a Container Appsben.

  5. Ellenőrizze az üzembe helyezést:

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

A GitHub Copilot csatlakoztatása az üzembe helyezett kiszolgálóhoz

Most, hogy az MCP-kiszolgáló az Azure-ban fut, konfigurálja a VS Code-ot a GitHub Copilot üzembe helyezett végponthoz való csatlakoztatásához.

  1. Létrehozás vagy frissítés .vscode/mcp.json:

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

    Cserélje le <your-app-fqdn> a központi telepítési kimenet teljes tartománynevét.

    Fontos

    "type"-nek "sse" kell lennie, mert ez az oktatóanyag az SSE-átvitelt használja. A "http" (streamelhető HTTP) használata csatlakozási hibákat okoz.

  2. A VS Code-ban nyissa meg a Copilot-csevegést ügynök módban.

  3. Az Ellenőrzés tasks-mcp-server megjelenik az Eszközök listában. Szükség esetén válassza a Start lehetőséget.

  4. Tesztelje a következőt: "Milyen feladatokkal rendelkezem?"

Skálázás konfigurálása interaktív használatra

A Java-alkalmazások hidegindítási ideje hosszabb. Állítsa be a minimális replikaszámot a JVM melegen tartásához:

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

Jótanács

Fontolja meg a JVM-hez megfelelő erőforrás-beállításokat. Spring Boot-alkalmazásokhoz legalább 1 vCPU és 2 GiB memória ajánlott.

Biztonsági szempontok

Ez az oktatóanyag egy nem hitelesített MCP-kiszolgálót használ az egyszerűség kedvéért. Mielőtt éles környezetben futtat egy MCP-kiszolgálót, tekintse át az alábbi javaslatokat. Ha az MCP-kiszolgálót nagy nyelvi modellek (LLM-ek) által működtetett ügynök hívja meg, vegye figyelembe a gyors injektálási támadásokat.

  • Hitelesítés és engedélyezés: Az MCP-kiszolgáló védelme a Microsoft Entra-azonosítóval. Lásd: Biztonságos MCP-kiszolgálók a Container Appsben.
  • Bemeneti ellenőrzés: Az eszközparaméter-ellenőrzéshez használja a Bean Validation (@Valid, @NotNull, ) @Sizeparancsot. Lásd : Érvényesítés a Spring Bootban.
  • HTTPS: Az Azure Container Apps alapértelmezés szerint automatikus TLS-tanúsítványokkal kényszeríti a HTTPS-t.
  • Minimális jogosultság: Csak azokat az eszközöket tegye elérhetővé, amelyet a használati eset igényel. Kerülje azokat az eszközöket, amelyek megerősítés nélkül végeznek romboló műveleteket.
  • CORS: Az engedélyezett források korlátozása az éles környezetben megbízható tartományokra.
  • Naplózás és figyelés: Az MCP-eszközök meghívásainak naplózása az SLF4J és az Azure Monitor használatával.

Erőforrások tisztítása

Ha nem folytatja az alkalmazás használatát, törölje az erőforráscsoportot az oktatóanyagban létrehozott összes erőforrás eltávolításához:

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

Következő lépés