مشاركة عبر


التعليم: نشر خادم Java MCP على Azure Container Apps

في هذا الدرس، تبني خادم بروتوكول السياق النموذجي (MCP) الذي يعرض أدوات إدارة المهام باستخدام Spring Boot وحزمة تطوير MCP Java. تقوم بنشر الخادم إلى Azure Container Apps وتتصل به من GitHub Copilot Chat في VS Code.

في هذا البرنامج التعليمي، سوف تتعلّم:

  • أنشئ تطبيق Spring Boot يعرض أدوات MCP
  • اختبار خادم MCP محليا باستخدام GitHub Copilot
  • Containerize ونشر التطبيق في Azure Container Apps
  • توصيل GitHub Copilot بخادم MCP المنشور

المتطلبات المسبقه

أنشئ منصة التطبيق

في هذا القسم، تنشئ مشروع Spring Boot جديد باستخدام حزمة تطوير MCP Java.

  1. إنشاء دليل المشروع:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    
  2. إنشاء 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 Spring Boot باعتمادين رئيسيين: spring-boot-starter-web لإطار الويب ولل mcp-spring-webmvc MCP SDK. تضيف إضافة Spring Boot Maven التطبيق كملف JAR قابل للتنفيذ.

    ‏‫ملاحظة‬

    مجموعة تطوير MCP Java SDK قيد التطوير النشط. تحقق من إصدارات MCP Java SDK للاطلاع على أحدث إصدار وقم بتحديث <version> الإصدار وفقا لذلك.

  3. إنشاء هيكل الدليل:

    mkdir -p src/main/java/com/example/tasksmcp
    mkdir -p src/main/resources
    
  4. إنشاء src/main/resources/application.properties:

    server.port=8080
    

تعريف نموذج البيانات والتخزين

في هذا القسم، تحدد نموذج بيانات المهام ومخزن في الذاكرة.

  1. إنشاء 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; }
    }
    

    تعرف الفئة TaskItem نموذج البيانات مع معادلات قياسية ومحدد لحالة الإكمال. يقوم المصمم بتهيئة createdAt الطابع الزمني تلقائيا.

  2. إنشاء 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 يدير مكون الربيع قائمة في الذاكرة مجهزة مسبقا ببيانات عينات. يستخدم AtomicInteger لتوليد معرف آمن للخيوط ويوفر طرقا لعمليات CRUD القياسية.

تعريف أدوات MCP

في هذا القسم، تحدد أدوات MCP التي يمكن لنموذج الذكاء الاصطناعي استدعاءها وتكوين خادم MCP في تطبيق Spring Boot الخاص بك.

  1. إنشاء 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);
        }
    }
    

    ‏‫ملاحظة‬

    واجهة برمجة تطبيقات MCP Java SDK تتطور. نمط تسجيل الأدوات المعروض هنا يستخدم SyncToolSpecification للأدوات المتزامنة. تحقق من وثائق MCP Java SDK للحصول على أحدث التعابير والتعليقات التي يمكن أن تسهل تسجيل الأدوات.

  2. إنشاء 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;
        }
    }
    

    النقاط الرئيسية:

    • WebMvcSseServerTransportProvider يسجل نقل SSE عند المسار /mcp .
    • McpServer.sync(transport) يضبط قدرات الأداة ويسجل كل مواصفات كل أداة.
    • يتم تفعيل CORS لأن GitHub Copilot في VS Code يقوم بتقديم طلبات عبر المصادر إلى خوادم MCP.

    ‏‫ملاحظة‬

    يستخدم WebMvcSseServerTransportProvider هذا الدرس (نقل SSE) لأن حزمة تطوير جافا MCP لا تقدم بعد نقل HTTP قابل للتدفق ومستقر. الدروس اللغوية الأخرى (.NET، بايثون، Node.js) تستخدم HTTP قابل للانطلاق. عندما تضيف حزمة تطوير جافا دعم HTTP القابلة للبث، قم بتحديث مزود النقل وفقا لذلك. نقل SSE متوافق بالكامل مع VS Code Copilot وعملاء MCP الآخرين.

  3. إنشاء 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";
        }
    }
    

    الفئة الرئيسية تقوم بإقلاع تطبيق Spring Boot وتوفر نقطة /health نهاية لمجسات صحة تطبيقات الحاويات. تعيد نقاط نهاية MCP JSON-RPC الاستجابات، لذا هناك حاجة إلى نقطة نهاية صحية منفصلة للفحوصات الصحية.

اختبار خادم MCP محليا

قبل النشر على Azure، تحقق من عمل خادم MCP عن طريق تشغيله محليا والاتصال من GitHub Copilot.

  1. البناء والتشغيل:

    mvn spring-boot:run
    
  2. افتح VS Code، ثم افتح دردشة Copilot واختر وضع الوكيل .

  3. اختر زر الأدوات ، ثم أضف المزيد من الأدوات...>أضف خادم MCP.

  4. اختر HTTP (أحداث Server-Sent أو HTTP) واختر Server-Sent الأحداث (SSE) عند طلب نوع النقل.

    مهم

    يستخدم هذا الدرس نقل SSE، وليس HTTP قابل للبث. يجب عليك اختيار خيار SSE في كود VS لكي يعمل الاتصال بشكل صحيح.

  5. أدخل رابط الخادم: http://localhost:8080/mcp

  6. أدخل معرف الخادم: tasks-mcp

  7. حدد إعدادات مساحة العمل.

  8. اختبر ب: "أرني جميع المهام"

  9. اختر متابعة عندما يطلب Copilot تأكيد أداة MCP.

يجب أن ترى قائمة المهام التي تم إرجاعها من مخزن الذاكرة الخاص بك.

نصيحة

جرب تعليمات أخرى مثل "إنشاء مهمة لمراجعة الPR"، "وسم المهمة 1 كمكتملة"، أو "حذف المهمة 2".

ضع التطبيق في حاوية

قم بتغليف التطبيق كحاوية Docker حتى تتمكن من اختباره محليا قبل نشره على Azure.

  1. أنشئ بناء Dockerfile متعدد المراحل:

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

    يحافظ البناء متعدد المراحل على صغر حجم الصورة النهائية من خلال فصل بناء Maven عن صورة التشغيل.

  2. تحقق محليا:

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

    تأكد من أن نقطة النهاية الصحية تستجيب: curl http://localhost:8080/health

التوزيع إلى Azure Container Apps

بعد أن تقوم بتحويل التطبيق إلى حاوية، قم بنشره في Azure Container Apps باستخدام Azure CLI. تقوم هذه الأوامر az containerapp up ببناء صورة الحاوية في السحابة، لذا لا تحتاج إلى Docker على جهازك لهذه الخطوة.

  1. حدد متغيرات البيئة:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-java"
    
  2. إنشاء مجموعة موارد وبيئة تطبيقات الحاويات:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. نشر تطبيق الحاوية:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  4. تكوين CORS:

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

    ‏‫ملاحظة‬

    للإنتاج، استبدل الأصول الوايلدكارد بأصول موثوقة محددة. انظر خوادم MCP الآمنة على تطبيقات الحاويات.

  5. تحقق من النشر:

    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 بالخادم المنشور

الآن بعد أن يعمل خادم MCP في Azure، قم بتكوين VS Code لربط GitHub Copilot بنقطة النهاية المنشورة.

  1. إنشاء أو تحديث .vscode/mcp.json:

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

    استبدلها <your-app-fqdn> ب FQDN من مخرجات النشر.

    مهم

    لابد أن "type" السبب هو "sse" أن هذا الدرس يستخدم نقل SSE. استخدام "http" (HTTP القابل للبث) يسبب أعطال في الاتصال.

  2. في VS Code، افتح دردشة Copilot في وضع الوكيل.

  3. يظهر التحقق tasks-mcp-server في قائمة الأدوات. اختر Start إذا لزم الأمر.

  4. اختبر بطلب مثل "ما هي المهام التي لدي؟"

تكوين التوسع للاستخدام التفاعلي

تطبيقات جافا لها أوقات تشغيل باردة أطول. حدد الحد الأدنى لعدد النسخ للحفاظ على دفء JVM:

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

نصيحة

ضع في اعتبارك إعدادات الموارد المناسبة لجهاز JVM. يوصى باستخدام ذاكرة بحد أدنى 1 vCPU وذاكرة 2 جيجابايت لتطبيقات Spring Boot.

اعتبارات الأمان

يستخدم هذا الدرس خادم MCP غير مصادق للبساطة. قبل تشغيل خادم MCP في الإنتاج، راجع التوصيات التالية. عندما يتم استدعاء خادم MCP الخاص بك بواسطة وكيل مدعوم بنماذج لغوية كبيرة (LLMs)، كن حذرا من هجمات حقن الطلبات .

  • المصادقة والتفويض: قم بتأمين خادم MCP الخاص بك باستخدام Microsoft Entra ID. انظر خوادم MCP الآمنة على تطبيقات الحاويات.
  • التحقق من صحة الإدخال: استخدم التحقق من صحة الفاصوليا (@Valid, @NotNull, @Size) للتحقق من صحة معلمات الأداة. انظر التحقق في Spring Boot.
  • HTTPS: Azure Container Apps يفرض HTTPS بشكل افتراضي مع شهادات TLS تلقائية.
  • أقل امتياز: اعرض فقط الأدوات التي تتطلبها حالتك. تجنب الأدوات التي تقوم بعمليات مدمرة دون تأكيد.
  • CORS: تقييد المصادر المسموح بها على النطاقات الموثوقة في الإنتاج.
  • التسجيل والمراقبة: سجل استدعاءات أدوات MCP للتدقيق باستخدام SLF4J وAzure Monitor.

تنظيف الموارد

إذا لم تكن ستستمر في استخدام هذا التطبيق، قم بحذف مجموعة الموارد لإزالة جميع الموارد التي تم إنشاؤها في هذا الدرس:

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

الخطوة التالية