Поделиться через


Руководство по развертыванию сервера Java MCP в приложениях контейнеров Azure

В этом руководстве вы создадите сервер протокола Model Context Protocol (MCP), предоставляющий инструменты управления задачами с помощью Spring Boot и Java SDK для MCP. Вы развертываете сервер в приложениях контейнеров Azure и подключаетесь к нему из чата GitHub Copilot в VS Code.

Изучив это руководство, вы:

  • Создание приложения Spring Boot, которое предоставляет средства MCP
  • Тестирование сервера MCP локально с помощью GitHub Copilot
  • Контейнеризация и развертывание приложения в приложениях контейнеров Azure
  • Подключите GitHub Copilot к развернутому серверу MCP

Предпосылки

Создайте каркас приложения

В этом разделе описано, как создать проект Spring Boot с помощью пакета SDK для 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 для SDK MCP. Плагин Spring Boot Maven упаковывает приложение как исполняемый JAR-файл.

    Замечание

    Пакет SDK для Java ДЛЯ MCP находится в активной разработке. Проверьте выпуски Java SDK для MCP, чтобы узнать последнюю версию и обновите <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 Spring управляет списком в памяти, предварительно заполненным примерами данных. Он использует 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);
        }
    }
    

    Замечание

    Поверхность API SDK для Java в MCP развивается. Шаблон регистрации инструмента, показанный здесь, используется SyncToolSpecification для синхронных инструментов. Ознакомьтесь с документацией по Java SDK для MCP, чтобы узнать о последних идиомах и аннотациях, которые могут упростить регистрацию инструментов.

  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), так как SDK для Java для MCP пока не предлагает стабильный потоковый транспорт HTTP. Другие учебники по языку (.NET, Python, Node.js) используют потоковый HTTP. Когда в пакет SDK Java добавляется поддержка потоковой передачи 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 (HTTP или Server-Sent Events) и выберите Server-Sent Events (SSE) при появлении запроса на тип передачи данных.

    Это важно

    В этом руководстве используется транспорт SSE, недоступный для потоковой передачи HTTP. Чтобы подключение работало правильно, необходимо выбрать параметр SSE в VS Code.

  5. Введите URL-адрес сервера: 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

После контейнеризации приложения разверните его в приложениях контейнеров Azure с помощью 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> полным доменным именем из выходных данных развертывания.

    Это важно

    "type" должен быть "sse", потому что в этом учебнике используется транспортный протокол SSE. Использование "http" (потоковая передача HTTP) приводит к сбоям подключения.

  2. В VS Code откройте чат Copilot в режиме агента.

  3. Убедитесь, что tasks-mcp-server отображается в списке инструментов. При необходимости нажмите кнопку "Пуск ".

  4. Проверьте запрос, например "Какие задачи у меня есть?"

Настройка масштабирования для интерактивного использования

Приложения Java имеют более длительное время холодного запуска. Задайте минимальное количество реплик, чтобы сохранить JVM теплым:

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

Подсказка

Рассмотрим параметры ресурсов, подходящие для JVM. Для приложений Spring Boot рекомендуется не менее 1 виртуальных ЦП и 2-ГиБ памяти.

Вопросы безопасности

В этом руководстве для простоты используется неавтоентизованный сервер MCP. Перед запуском сервера MCP в рабочей среде ознакомьтесь со следующими рекомендациями. Если сервер MCP вызывается агентом, управляемым большими языковыми моделями (LLMs), следует учитывать атаки введения команд.

  • Проверка подлинности и авторизация. Защита сервера MCP с помощью идентификатора Microsoft Entra. См. раздел "Безопасные серверы MCP" в приложениях контейнеров.
  • Проверка входных данных: используйте проверку параметров bean (@Valid, @NotNull, ) @Sizeдля проверки параметров средства. См. Проверку в Spring Boot.
  • HTTPS: приложения контейнеров Azure по умолчанию применяют ПРОТОКОЛ HTTPS с автоматическими сертификатами TLS.
  • Минимальные привилегии: предоставляйте только те инструменты, которые требуются для вашего случая. Избегайте средств, выполняющих разрушительные операции без подтверждения.
  • CORS: ограничение разрешенных источников доверенным доменам в рабочей среде.
  • Ведение журнала и мониторинг: Запись вызовов инструментов MCP для аудита с использованием SLF4J и Azure Monitor.

Очистите ресурсы

Если вы не собираетесь продолжать использовать это приложение, удалите группу ресурсов, чтобы удалить все ресурсы, созданные в этом руководстве:

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

Следующий шаг