Aracılığıyla paylaş


Öğretici: Azure Container Apps'e Java MCP sunucusu dağıtma

Bu öğreticide, Spring Boot ve MCP Java SDK'sını kullanarak görev yönetimi araçlarını kullanıma sunan bir Model Bağlam Protokolü (MCP) sunucusu oluşturacaksınız. Sunucuyu Azure Container Apps'e dağıtır ve VS Code'da GitHub Copilot Sohbeti'nden bu sunucuya bağlanırsınız.

Bu eğitimde, siz:

  • MCP araçlarını kullanıma sunan bir Spring Boot uygulaması oluşturma
  • MCP sunucusunu GitHub Copilot ile yerel olarak test etme
  • Uygulamayı kapsayıcıya alma ve Azure Container Apps'e dağıtma
  • GitHub Copilot'ı dağıtılan MCP sunucusuna bağlama

Önkoşullar

Uygulama iskelesini oluşturma

Bu bölümde, MCP Java SDK'sı ile yeni bir Spring Boot projesi oluşturacaksınız.

  1. Proje dizinini oluşturun:

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

    pom.xml iki temel bağımlılığa sahip bir Spring Boot uygulamasını tanımlar: web çerçevesi için spring-boot-starter-web ve MCP SDK'sı için mcp-spring-webmvc. Spring Boot Maven eklentisi, uygulamayı yürütülebilir JAR olarak paketler.

    Uyarı

    MCP Java SDK'sı etkin geliştirme aşamasındadır. En son sürüm için MCP Java SDK sürümlerini denetleyin ve uygun şekilde güncelleştirin <version> .

  3. Dizin yapısını oluşturun:

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

    server.port=8080
    

Veri modelini ve depoyu tanımlama

Bu bölümde, görev veri modelini ve bellek içi depoyu tanımlarsınız.

  1. Oluştur 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; }
    }
    

    sınıfı, TaskItem veri modelini standart alıcılarla ve tamamlanma durumu için bir ayarlayıcıyla tanımlar. Oluşturucu, zaman damgasının createdAt ilk değerini otomatik olarak atar.

  2. Oluşturma 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);
        }
    }
    

    TaskStore Spring bileşeni, örnek verilerle önceden doldurulmuş bir bellek içi listeyi yönetir. AtomicInteger iş parçacığı güvenli kimlik oluşturma için kullanılır ve standart CRUD işlemleri için yöntemler sağlar.

MCP araçlarını tanımlama

Bu bölümde, Spring Boot uygulamanızda yapay zeka modelinin MCP sunucusunu çağırabileceği ve yapılandırabileceği MCP araçlarını tanımlarsınız.

  1. Oluşturma 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);
        }
    }
    

    Uyarı

    MCP Java SDK API'sinin yüzeyi gelişiyor. Burada gösterilen araç kayıt düzeni, zaman uyumlu araçlar için SyncToolSpecification kullanır. Araç kaydını basitleştirebilecek en son deyimler ve ek açıklamalar için MCP Java SDK belgelerine bakın.

  2. Oluştur 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;
        }
    }
    

    Önemli noktalar:

    • WebMvcSseServerTransportProvider SSE aktarımını /mcp yola kaydeder.
    • McpServer.sync(transport) araç özelliklerini yapılandırıp her araç belirtimlerini kaydeder.
    • VS Code'daki GitHub Copilot, MCP sunucularına çıkış noktaları arası isteklerde bulunduğundan CORS etkinleştirilir.

    Uyarı

    MCP Java SDK henüz kararlı bir akışlı HTTP taşıması sunmadığından, bu öğreticide WebMvcSseServerTransportProvider(SSE taşıması) kullanılır. Diğer dil öğreticilerinde (.NET, Python, Node.js) akışla aktarılabilir HTTP kullanılır. Java SDK'sı akışla aktarılabilir HTTP desteği eklediğinde, aktarım sağlayıcısını uygun şekilde güncelleştirin. SSE aktarımı VS Code Copilot ve diğer MCP istemcileri ile tam olarak uyumludur.

  3. Oluştur 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";
        }
    }
    

    Ana sınıf, Spring Boot uygulamasını başlatır ve Container Apps sistem durumu yoklamaları için bir /health uç noktası sunar. MCP uç noktaları JSON-RPC yanıtları döndürür, bu nedenle sistem durumu denetimleri için ayrı bir sistem durumu uç noktası gerekir.

MCP sunucusunu yerel olarak test edin

Azure'a dağıtmadan önce MCP sunucusunun yerel olarak çalıştırıp GitHub Copilot'tan bağlanarak çalıştığını doğrulayın.

  1. Derleme ve çalıştırma:

    mvn spring-boot:run
    
  2. VS Code'ı açın, ardından Copilot Sohbet'i açın ve Aracı modu'nu seçin.

  3. Araçlar düğmesini ve ardından Daha Fazla Araç Ekle... öğesini seçin.>MCP Sunucusu ekleyin.

  4. HTTP (HTTP veya Server-Sent Olayları) öğesini seçin ve aktarım türü sorulduğunda Server-Sent Olaylar'ı (SSE) seçin.

    Önemli

    Bu öğreticide akışla aktarılabilir HTTP değil SSE aktarımı kullanılır. Bağlantının düzgün çalışması için VS Code'da SSE seçeneğini belirlemeniz gerekir.

  5. Sunucu URL'sini girin: http://localhost:8080/mcp

  6. Bir sunucu kimliği girin: tasks-mcp

  7. Çalışma Alanı Ayarları'nı seçin.

  8. Şu şekilde test edin: "Tüm görevleri göster"

  9. Copilot MCP aracı onayı istediğinde Devam'ı seçin.

Bellek içi deponuzdan döndürülen görev listesini görmeniz gerekir.

Tavsiye

"PR'yi gözden geçirmek için bir görev oluştur", "Görev 1'i tamamlanmış olarak işaretle" veya "Görev 2'yi sil" gibi diğer istemleri deneyin.

Uygulamayı kapsayıcılı hale getirme

Uygulamayı Docker kapsayıcısı olarak paketleyerek Azure'a dağıtmadan önce yerel olarak test edebilirsiniz.

  1. Çok aşamalı derleme ile bir Dockerfile oluşturun:

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

    Çok aşamalı oluşturma, Maven derlemesini çalışma zamanı görüntüsünden ayırarak nihai görüntünün boyutunu küçük tutar.

  2. Yerel olarak doğrula:

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

    Sağlık uç noktasının yanıt verdiğini onaylayın: curl http://localhost:8080/health

Azure Container Apps'a dağıtım

Uygulamayı kapsayıcıya aldıktan sonra Azure CLI kullanarak Azure Container Apps'e dağıtın. komutu az containerapp up kapsayıcı görüntüsünü bulutta oluşturur, bu nedenle bu adım için makinenizde Docker'a ihtiyacınız yoktur.

  1. Ortam değişkenlerini ayarlama:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-java"
    
  2. Kaynak grubu ve Container Apps ortamı oluşturma:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. Kapsayıcı uygulamasını dağıtma:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  4. CORS'yi yapılandırma:

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

    Uyarı

    Üretim için joker karakter çıkış noktalarını belirli güvenilir kaynaklarla değiştirin. Bkz . Container Apps'te MCP sunucularının güvenliğini sağlama.

  5. Dağıtımı doğrulayın:

    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'ı dağıtılan sunucuya bağlama

MCP sunucusu Azure'da çalıştığına göre VS Code'u GitHub Copilot'ı dağıtılan uç noktaya bağlamak için yapılandırın.

  1. oluşturun veya güncelleştirin .vscode/mcp.json:

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

    <your-app-fqdn> değerini dağıtım çıktısından FQDN ile değiştirin.

    Önemli

    "type" olmalıdır çünkü bu öğretici SSE aktarımını kullanmaktadır "sse". ( "http" Akışla aktarılabilir HTTP) kullanılması bağlantı hatalarına neden olur.

  2. VS Code'da Aracı modunda Copilot Sohbet'i açın.

  3. Doğrula tasks-mcp-server , Araçlar listesinde görünür. Gerekirse Başlat'ı seçin.

  4. "Hangi görevlerim var?" gibi bir istemle test edin

Etkileşimli kullanım için ölçeklendirmeyi yapılandırma

Java uygulamalarının daha uzun soğuk başlangıç süreleri vardır. JVM'yi sıcak tutmak için en düşük çoğaltma sayısını ayarlayın:

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

Tavsiye

JVM için uygun kaynak ayarlarını göz önünde bulundurun. Spring Boot uygulamaları için en az 1 vCPU ve 2 GiB bellek önerilir.

Güvenlik konuları

Bu öğreticide basitlik için kimliği doğrulanmamış bir MCP sunucusu kullanılır. Üretimde bir MCP sunucusu çalıştırmadan önce aşağıdaki önerileri gözden geçirin. MCP sunucunuz, büyük dil modelleri (LLM'ler) tarafından desteklenen bir aracı tarafından çağrıldığında, kod ekleme saldırılarına dikkat edin.

  • Kimlik doğrulaması ve yetkilendirme: Microsoft Entra Id ile MCP sunucunuzun güvenliğini sağlayın. Bkz . Container Apps'te MCP sunucularının güvenliğini sağlama.
  • Giriş doğrulaması: Araç parametresi doğrulaması için Fasulye Doğrulama (@Valid, @NotNull, @Size) kullanın. Bkz . Spring Boot'ta Doğrulama.
  • HTTPS: Azure Container Apps, otomatik TLS sertifikaları ile varsayılan olarak HTTPS'nin zorunlu kılınmasını sağlar.
  • En az ayrıcalık: Yalnızca kullanım örneğinizin gerektirdiği araçları kullanıma sunma. Onay olmadan yıkıcı işlemler gerçekleştiren araçlardan kaçının.
  • CORS: İzin verilen çıkış noktalarını üretimdeki güvenilen etki alanlarıyla kısıtlayın.
  • Günlüğe kaydetme ve izleme: Denetim için SLF4J ve Azure İzleyici kullanarak MCP aracı çağrılarını günlüğe kaydedin.

Kaynakları temizle

Bu uygulamayı kullanmaya devam etmeyecekseniz, bu öğreticide oluşturulan tüm kaynakları kaldırmak için kaynak grubunu silin:

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

Sonraki adım