Tutorial: Implantar um servidor MCP Java nos Aplicativos de Contêiner do Azure

Neste tutorial, você criará um servidor MCP (Protocolo de Contexto de Modelo) que expõe as ferramentas de gerenciamento de tarefas usando o Spring Boot e o SDK de Java do MCP. Implante o servidor no Aplicativos de Contêiner do Azure e conecte-o ao GitHub Copilot Chat no VS Code.

Neste tutorial, você:

  • Criar um aplicativo Spring Boot que expõe ferramentas MCP
  • Testar o servidor MCP localmente com o GitHub Copilot
  • Conteinerizar e implantar o aplicativo nos Aplicativos de Contêiner do Azure
  • Conectar o GitHub Copilot ao servidor MCP implantado

Pré-requisitos

Criar a estrutura do aplicativo

Nesta seção, você criará um novo projeto do Spring Boot com o SDK do Java do MCP.

  1. Crie o diretório do projeto:

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

    Define pom.xml um aplicativo Spring Boot com duas dependências principais: spring-boot-starter-web para a estrutura da Web e mcp-spring-webmvc para o SDK do MCP. O plug-in do Spring Boot Maven empacota o aplicativo como um JAR executável.

    Observação

    O SDK do Java do MCP está em desenvolvimento ativo. Confira as versões de SDK do Java do MCP para obter a versão mais recente e atualize a <version> adequadamente.

  3. Crie a estrutura do diretório:

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

    server.port=8080
    

Definir o modelo de dados e o repositório

Nesta seção, você definirá o modelo de dados da tarefa e um repositório na memória.

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

    A classe TaskItem estabelece o modelo de dados com acessadores padrão e um definidor para o status de conclusão. O construtor inicializa o carimbo de data/hora createdAt automaticamente.

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

    O TaskStore componente Spring gerencia uma lista na memória pré-preenchida com dados de exemplo. Ele usa AtomicInteger para a geração de IDs seguras para threads e fornece métodos para operações CRUD padrão.

Definir as ferramentas do MCP

Nesta seção, você define as ferramentas MCP que o modelo de IA pode invocar e configurar o servidor MCP em seu aplicativo Spring Boot.

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

    Observação

    A superfície da API do MCP SDK Java está evoluindo. O padrão de registro de ferramenta mostrado aqui usa SyncToolSpecification para ferramentas síncronas. Verifique a documentação do SDK Java do MCP para obter os idiomas e as anotações mais recentes que podem simplificar o registro de ferramentas.

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

    Pontos principais:

    • WebMvcSseServerTransportProvider registra o transporte SSE no caminho /mcp.
    • McpServer.sync(transport) configura os recursos da ferramenta e registra cada especificação de ferramenta.
    • O CORS está habilitado porque o GitHub Copilot no VS Code faz solicitações entre origens para servidores MCP.

    Observação

    Este tutorial usa WebMvcSseServerTransportProvider (transporte SSE) porque o SDK do Java do MCP ainda não oferece um transporte HTTP estável que pode ser transmitido. Os outros tutoriais de idioma (.NET, Python, Node.js) usam HTTP em streaming. Quando o SDK do Java incluir suporte HTTP transmissível, atualize o provedor de transporte de acordo. O transporte SSE é totalmente compatível com o VS Code Copilot e outros clientes MCP.

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

    A classe principal inicializa o aplicativo do Spring Boot e expõe um ponto de extremidade /health para investigações de integridade de Aplicativos de Contêiner. Os pontos de extremidade MCP retornam respostas JSON-RPC, por isso, um ponto de extremidade de integridade separado é necessário para verificações de integridade.

Testar o servidor MCP localmente

Antes de implantar no Azure, verifique se o servidor MCP funciona executando-o localmente e conectando-se do GitHub Copilot.

  1. Compilar e executar:

    mvn spring-boot:run
    
  2. Abra o VS Code e, em seguida, abra o Chat do Copilot e selecione o modo Agente .

  3. Selecione o botão Ferramentas e, em seguida, adicione mais ferramentas...>Adicionar servidor MCP.

  4. Selecione HTTP (HTTP ou Server-Sent Events) e escolha Server-Sent Events (SSE) quando solicitado o tipo de transporte.

    Importante

    Este tutorial usa o transporte SSE, não o HTTP com streaming. Você deve selecionar a opção SSE no VS Code para que a conexão funcione corretamente.

  5. Insira a URL do servidor: http://localhost:8080/mcp

  6. Insira uma ID do servidor: tasks-mcp

  7. Selecione Configurações do Workspace.

  8. Teste com: "Mostrar-me todas as tarefas"

  9. Selecione Continuar quando o Copilot solicitar a confirmação da ferramenta MCP.

Você deverá ver a lista de tarefas retornada do repositório na memória.

Tip

Tente outros prompts como "Criar uma tarefa para examinar a PR", "Marcar tarefa 1 como concluída" ou "Excluir tarefa 2".

Colocar o aplicativo em um contêiner

Empacote o aplicativo como um contêiner do Docker para que você possa testá-lo localmente antes de implantar no Azure.

  1. Crie um Dockerfile com uma construção de múltiplos estágios.

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

    O build de vários estágios mantém a imagem final pequena separando o build do Maven da imagem de runtime.

  2. Verifique localmente:

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

    Confirme se o endpoint de saúde responde: curl http://localhost:8080/health

Implantar nos Aplicativos de Contêiner do Azure

Depois de colocar o aplicativo em contêiner, implante-o nos Aplicativos de Contêiner do Azure usando a CLI do Azure. O az containerapp up comando cria a imagem de contêiner na nuvem, portanto, você não precisa do Docker em seu computador para esta etapa.

  1. Definir variáveis de ambiente:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-java"
    
  2. Criar um grupo de recursos e um ambiente de Aplicativos de Contêiner:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. Implante o aplicativo de contêiner:

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

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

    Observação

    Para produção, substitua as origens curinga por origens confiáveis específicas. Consulte servidores MCP seguros em Aplicativos de Contêiner.

  5. Verifique a implantação:

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

Conectar o GitHub Copilot ao servidor implantado

Agora que o servidor MCP está em execução no Azure, configure o VS Code para conectar o GitHub Copilot ao ponto de extremidade implantado.

  1. Criar ou atualizar .vscode/mcp.json:

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

    Substitua <your-app-fqdn> pelo FQDN da saída de implantação.

    Importante

    Deve "type" ser "sse" porque este tutorial usa o transporte SSE. O uso "http" (HTTP transmitível) causa falhas de conexão.

  2. Abra o Copilot Chat no modo Agente dentro do VS Code.

  3. Verifique se tasks-mcp-server está na lista de ferramentas. Selecione Iniciar , se necessário.

  4. Teste com um prompt como "Quais tarefas eu tenho?"

Configurar o dimensionamento para uso interativo

Os aplicativos Java apresentam tempos de inicialização a frio mais longos. Defina uma contagem mínima de réplicas para manter a JVM aquecida:

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

Tip

Considere as configurações de recurso apropriadas para a JVM. Um mínimo de 1 vCPU e 2 GiB de memória é recomendado para aplicativos Spring Boot.

Considerações de segurança

Este tutorial usa um servidor MCP não autenticado para simplificar. Antes de executar um servidor MCP em produção, examine as recomendações a seguir. Quando o servidor MCP for chamado por um agente alimentado por LLMs (grandes modelos de linguagem), esteja ciente dos ataques de injeção de prompt.

  • Autenticação e autorização: proteja o servidor MCP com a ID do Microsoft Entra. Consulte servidores MCP seguros em Aplicativos de Contêiner.
  • Validação de entrada: Use Bean Validation (@Valid, @NotNull, @Size) para validação de parâmetro de ferramenta. Consulte a validação no Spring Boot.
  • HTTPS: Os Aplicativos de Contêiner do Azure impõem HTTPS por padrão com certificados TLS automáticos.
  • Privilégio mínimo: exponha apenas as ferramentas necessárias para seu caso de uso. Evite ferramentas que executam operações destrutivas sem confirmação.
  • CORS: restringir as origens permitidas a domínios confiáveis na produção.
  • Registro em log e monitoramento: registre invocações de ferramentas MCP para auditoria usando SLF4J e Azure Monitor.

Limpar os recursos

Se você não quiser continuar a usar este aplicativo, exclua o grupo de recursos para remover todos os recursos criados neste tutorial:

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

Próxima etapa