Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
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
- Sebuah akun Azure dengan langganan aktif. Buat akun gratis.
- Azure CLI versi 2.62.0 atau yang lebih baru.
- Java 17 atau yang lebih baru.
- Maven 3.8 atau yang lebih baru.
- Visual Studio Code dengan ekstensi GitHub Copilot .
- Docker Desktop (opsional - hanya diperlukan untuk menguji kontainer secara lokal).
Membuat perancah aplikasi
Di bagian ini, Anda membuat proyek Spring Boot baru dengan MCP Java SDK.
Buat direktori proyek:
mkdir tasks-mcp-server && cd tasks-mcp-serverBuat
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.xmlaplikasi Spring Boot dengan dua dependensi utama:spring-boot-starter-webuntuk kerangka kerja web danmcp-spring-webmvcuntuk 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.Buat struktur direktori:
mkdir -p src/main/java/com/example/tasksmcp mkdir -p src/main/resourcesBuat
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.
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
TaskItemmenentukan model data dengan getter standar dan setter untuk status penyelesaian. Konstruktor menginisialisasicreatedAttanda waktu secara otomatis.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
TaskStoreSpring mengelola daftar dalam memori yang telah diisi sebelumnya dengan data sampel. Ini menggunakanAtomicIntegeruntuk 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.
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
SyncToolSpecificationuntuk alat sinkron. Periksa dokumentasi MCP Java SDK untuk idiom dan anotasi terbaru yang dapat menyederhanakan pendaftaran alat.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:
-
WebMvcSseServerTransportProvidermendaftarkan 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.-
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
/healthtitik 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.
Bangun dan jalankan:
mvn spring-boot:runBuka Visual Studio Code, lalu buka Obrolan Copilot dan pilih Mode agen .
Pilih tombol Alat , lalu Tambahkan Alat Lainnya...>Tambahkan Server MCP.
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.
Masukkan URL server:
http://localhost:8080/mcpMasukkan ID server:
tasks-mcpPilih Pengaturan Ruang Kerja.
Uji dengan: "Tampilkan saya semua tugas"
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.
Buat
Dockerfiledengan 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.
Verifikasi secara lokal:
docker build -t tasks-mcp-server . docker run -p 8080:8080 tasks-mcp-serverPastikan 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.
Atur variabel lingkungan:
RESOURCE_GROUP="mcp-tutorial-rg" LOCATION="eastus" ENVIRONMENT_NAME="mcp-env" APP_NAME="tasks-mcp-server-java"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 $LOCATIONSebarkan aplikasi kontainer:
az containerapp up \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --environment $ENVIRONMENT_NAME \ --source . \ --ingress external \ --target-port 8080Mengonfigurasi 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.
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.
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.Di Visual Studio Code, buka Copilot Chat dalam mode Agent.
Verifikasi
tasks-mcp-servermuncul di daftar Alat. Pilih Mulai jika diperlukan.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