Tutorial: Menyebarkan server Java MCP ke Azure Container Apps

Dalam tutorial ini, Anda membangun server Protokol Konteks Model (MCP) yang mengekspos alat manajemen tugas dengan menggunakan Spring Boot dan MCP Java SDK. Anda menyebarkan server ke Azure Container Apps dan menghubungkannya dari GitHub Copilot Chat di VS Code.

Di tutorial ini, Anda akan:

  • Membuat aplikasi Spring Boot yang mengekspos alat MCP
  • Menguji server MCP secara lokal dengan GitHub Copilot
  • Kontainerisasi dan sebarkan aplikasi ke Azure Container Apps
  • Menyambungkan GitHub Copilot ke server MCP yang disebarkan

Prasyarat

Membuat perancah aplikasi

Di bagian ini, Anda membuat proyek Spring Boot baru dengan MCP Java SDK.

  1. Buat direktori proyek:

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

    mendefinisikan pom.xml aplikasi Spring Boot dengan dua dependensi utama: spring-boot-starter-web untuk kerangka kerja web dan mcp-spring-webmvc untuk MCP SDK. Plugin Spring Boot Maven mengemas aplikasi sebagai JAR yang dapat dieksekusi.

    Nota

    MCP Java SDK sedang dalam pengembangan aktif. Periksa rilis MCP Java SDK untuk mendapatkan versi terbaru dan perbarui <version> sesuai dengan pembaruan terakhir.

  3. Buat struktur direktori:

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

    server.port=8080
    

Menentukan model data dan menyimpan

Di bagian ini, Anda menentukan model data tugas dan penyimpanan dalam memori.

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

    Kelas TaskItem menentukan model data dengan getter standar dan setter untuk status penyelesaian. Konstruktor menginisialisasi createdAt tanda waktu secara otomatis.

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

    Komponen TaskStore Spring mengelola daftar dalam memori yang telah diisi sebelumnya dengan data sampel. Ini menggunakan AtomicInteger untuk pembuatan ID aman utas dan menyediakan metode untuk operasi CRUD standar.

Tentukan alat MCP

Di bagian ini, Anda menentukan alat MCP yang dapat dipanggil model AI dan mengonfigurasi server MCP di aplikasi Spring Boot Anda.

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

    Nota

    Cakupan API MCP Java SDK sedang berkembang. Pola pendaftaran alat yang ditampilkan di sini menggunakan SyncToolSpecification untuk alat sinkron. Periksa dokumentasi MCP Java SDK untuk idiom dan anotasi terbaru yang dapat menyederhanakan pendaftaran alat.

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

    Poin utama:

    • WebMvcSseServerTransportProvider mendaftarkan protokol komunikasi SSE di jalur /mcp
    • McpServer.sync(transport) mengonfigurasi kemampuan alat dan mendaftarkan setiap spesifikasi alat.
    • CORS diaktifkan karena GitHub Copilot di VS Code mengajukan permintaan lintas asal ke server MCP.

    Nota

    Tutorial ini menggunakan WebMvcSseServerTransportProvider (transportasi SSE) karena MCP Java SDK belum menawarkan transportasi HTTP yang dapat dialirkan yang stabil. Tutorial bahasa lain (.NET, Python, Node.js) menggunakan HTTP yang dapat dialirkan. Saat Java SDK menambahkan dukungan HTTP yang dapat dialirkan, perbarui penyedia transportasi yang sesuai. Transportasi SSE sepenuhnya kompatibel dengan Vs Code Copilot dan klien MCP lainnya.

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

    Kelas utama menginisiasi aplikasi Spring Boot dan mengekspos /health titik akhir untuk pemeriksaan status kesehatan Aplikasi Kontainer. Titik akhir MCP mengembalikan respons JSON-RPC, sehingga titik akhir kesehatan terpisah diperlukan untuk pemeriksaan kesehatan.

Menguji server MCP secara lokal

Sebelum menyebarkan ke Azure, verifikasi server MCP berfungsi dengan menjalankannya secara lokal dan menyambungkan dari GitHub Copilot.

  1. Bangun dan jalankan:

    mvn spring-boot:run
    
  2. Buka Visual Studio Code, lalu buka Obrolan Copilot dan pilih Mode agen .

  3. Pilih tombol Alat , lalu Tambahkan Alat Lainnya...>Tambahkan Server MCP.

  4. Pilih HTTP (HTTP atau Peristiwa Server-Dikirim) dan pilih Peristiwa Server-Dikirim (SSE) ketika diminta untuk memilih jenis transportasi.

    Penting

    Tutorial ini menggunakan transport SSE, bukan HTTP streamable. Anda harus memilih opsi SSE di Visual Studio Code agar koneksi berfungsi dengan benar.

  5. Masukkan URL server: http://localhost:8080/mcp

  6. Masukkan ID server: tasks-mcp

  7. Pilih Pengaturan Ruang Kerja.

  8. Uji dengan: "Tampilkan saya semua tugas"

  9. Pilih Lanjutkan saat Copilot meminta konfirmasi alat MCP.

Anda akan melihat daftar tugas yang dikembalikan dari penyimpanan dalam memori Anda.

Petunjuk / Saran

Coba perintah lain seperti "Buat tugas untuk meninjau PR", "Tandai tugas 1 sebagai selesai", atau "Hapus tugas 2".

Menerapkan kontainer pada aplikasi

Kemas aplikasi sebagai kontainer Docker sehingga Anda dapat mengujinya secara lokal sebelum menyebarkan ke Azure.

  1. Buat Dockerfile dengan pembangunan multi-tahap:

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

    Pembangunan multi-tahap memastikan citra akhir tetap kecil dengan memisahkan pembangunan Maven dari citra runtime.

  2. Verifikasi secara lokal:

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

    Pastikan endpoint kesehatan merespons: curl http://localhost:8080/health

Menyebarkan ke Azure Container Apps

Setelah Anda membuat kontainer aplikasi, sebarkan ke Azure Container Apps dengan menggunakan Azure CLI. Perintah ini az containerapp up membangun gambar kontainer di cloud, sehingga Anda tidak memerlukan Docker di komputer Anda untuk langkah ini.

  1. Atur variabel lingkungan:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-java"
    
  2. Buat grup sumber daya dan lingkungan Container Apps:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. Sebarkan aplikasi kontainer:

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

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

    Nota

    Untuk produksi, ganti asal wildcard dengan asal tepercaya yang spesifik. Lihat Mengamankan server MCP di Container Apps.

  5. Verifikasi penyebaran:

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

Menyambungkan GitHub Copilot ke server yang disebarkan

Sekarang setelah server MCP berjalan di Azure, konfigurasikan VS Code untuk menyambungkan GitHub Copilot ke endpoint yang telah diterapkan.

  1. Buat atau perbarui .vscode/mcp.json:

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

    Ganti <your-app-fqdn> dengan FQDN dari output penyebaran.

    Penting

    "type" itu harus "sse" karena tutorial ini menggunakan transport SSE. Menggunakan "http" (HTTP yang dapat dialirkan) menyebabkan kegagalan koneksi.

  2. Di Visual Studio Code, buka Copilot Chat dalam mode Agent.

  3. Verifikasi tasks-mcp-server muncul di daftar Alat. Pilih Mulai jika diperlukan.

  4. Uji dengan perintah seperti "Tugas apa yang saya miliki?"

Mengonfigurasi penskalaan untuk penggunaan interaktif

Aplikasi Java memiliki waktu cold-start yang lebih lama. Atur jumlah replika minimum untuk menjaga JVM tetap hangat:

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

Petunjuk / Saran

Pertimbangkan pengaturan sumber daya yang sesuai untuk JVM. Minimal memori 1 vCPU dan 2-GiB direkomendasikan untuk aplikasi Spring Boot.

Pertimbangan keamanan

Tutorial ini menggunakan server MCP yang tidak diaauthenticated untuk kesederhanaan. Sebelum menjalankan server MCP dalam produksi, tinjau rekomendasi berikut. Ketika server MCP Anda dipanggil oleh agen yang didukung oleh model bahasa besar (LLMs), waspadai serangan injeksi prompt.

  • Autentikasi dan otorisasi: Amankan server MCP Anda dengan ID Microsoft Entra. Lihat Mengamankan server MCP di Container Apps.
  • Validasi input: Gunakan Bean Validation (@Valid, @NotNull, @Size) untuk validasi parameter alat. Lihat Validasi di Spring Boot.
  • HTTPS: Azure Container Apps memberlakukan HTTPS secara default dengan sertifikat TLS otomatis.
  • Hak istimewa terkecil: Hanya mengekspos alat yang diperlukan kasus penggunaan Anda. Hindari alat yang melakukan operasi destruktif tanpa konfirmasi.
  • CORS: Membatasi asal yang diizinkan ke domain tepercaya di lingkungan produksi.
  • Pencatatan dan pemantauan sistem: Mencatat pemanggilan alat MCP untuk audit menggunakan SLF4J dan Azure Monitor.

Membersihkan sumber daya

Jika Anda tidak akan terus menggunakan aplikasi ini, hapus grup sumber daya untuk menghapus semua sumber daya yang dibuat dalam tutorial ini:

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

Langkah selanjutnya