Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En este tutorial, creará un servidor de Protocolo de contexto de modelo (MCP) que expone herramientas de administración de tareas mediante Spring Boot y el SDK de Java de MCP. Implemente el servidor en Azure Container Apps y conéctese a él desde GitHub Copilot Chat en VS Code.
En este tutorial, usted hará lo siguiente:
- Creación de una aplicación de Spring Boot que expone herramientas de MCP
- Probar el servidor MCP localmente con GitHub Copilot
- Containerización y despliegue de la aplicación en Azure Container Apps
- Conexión de GitHub Copilot al servidor MCP implementado
Prerrequisitos
- Una cuenta de Azure con una suscripción activa. cree una de forma gratuita.
- Cli de Azure versión 2.62.0 o posterior.
- Java 17 o posterior.
- Maven 3.8 o posterior.
- Visual Studio Code con la extensión GitHub Copilot.
- Docker Desktop (opcional: solo es necesario para probar el contenedor localmente).
Crea el esqueleto de la aplicación
En esta sección, creará un nuevo proyecto de Spring Boot con el SDK de Java de MCP.
Cree el directorio del proyecto:
mkdir tasks-mcp-server && cd tasks-mcp-serverCrear
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.xmldefine una aplicación de Spring Boot con dos dependencias clave:spring-boot-starter-webpara el marco web ymcp-spring-webmvcpara el SDK de MCP. El complemento Maven de Spring Boot empaqueta la aplicación como un archivo JAR ejecutable.Nota:
El SDK de Java de MCP está en desarrollo activo. Compruebe las versiones del SDK de Java de MCP para obtener la versión más reciente y actualice la
<version>correspondiente.Cree la estructura de directorios:
mkdir -p src/main/java/com/example/tasksmcp mkdir -p src/main/resourcesCrear
src/main/resources/application.properties:server.port=8080
Definir el modelo de datos y el almacén
En esta sección, definirá el modelo de datos de tareas y un almacén en memoria.
Crear
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; } }La
TaskItemclase define el modelo de datos con captadores estándar y un establecedor para el estado de finalización. El constructor inicializa lacreatedAtmarca de tiempo automáticamente.Crear
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); } }El
TaskStorecomponente Spring administra una lista en memoria rellenada previamente con datos de ejemplo. UtilizaAtomicIntegerpara la generación de ID seguras para subprocesos y proporciona métodos para operaciones CRUD estándar.
Definición de las herramientas de MCP
En esta sección, definirá las herramientas de MCP que el modelo de IA puede invocar y configurar el servidor MCP en la aplicación spring Boot.
Crear
src/main/java/com/example/tasksmcp/TasksMcpTools.java:package com.example.tasksmcp; import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.Tool; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @Component public class TasksMcpTools { private final TaskStore store; private final ObjectMapper objectMapper = new ObjectMapper(); public TasksMcpTools(TaskStore store) { this.store = store; } public List<SyncToolSpecification> getToolSpecifications() { return List.of( listTasksTool(), getTaskTool(), createTaskTool(), toggleTaskCompleteTool(), deleteTaskTool() ); } private SyncToolSpecification listTasksTool() { var tool = new Tool( "list_tasks", "List all tasks with their ID, title, description, and completion status.", emptySchema() ); return new SyncToolSpecification(tool, (exchange, request) -> { try { String json = objectMapper.writeValueAsString(store.getAll()); return new CallToolResult(List.of(new TextContent(json)), false); } catch (Exception e) { return errorResult(e.getMessage()); } }); } private SyncToolSpecification getTaskTool() { var tool = new Tool( "get_task", "Get a single task by its numeric ID.", objectSchema(Map.of( "task_id", propertySchema("integer", "The numeric ID of the task to retrieve") ), List.of("task_id")) ); return new SyncToolSpecification(tool, (exchange, request) -> { int taskId = ((Number) request.arguments().get("task_id")).intValue(); return store.getById(taskId) .map(task -> { try { return new CallToolResult( List.of(new TextContent(objectMapper.writeValueAsString(task))), false); } catch (Exception e) { return errorResult(e.getMessage()); } }) .orElse(textResult("Task with ID " + taskId + " not found.")); }); } private SyncToolSpecification createTaskTool() { var tool = new Tool( "create_task", "Create a new task with the given title and description. Returns the created task.", objectSchema(Map.of( "title", propertySchema("string", "A short title for the task"), "description", propertySchema("string", "A detailed description of what the task involves") ), List.of("title", "description")) ); return new SyncToolSpecification(tool, (exchange, request) -> { String title = (String) request.arguments().get("title"); String description = (String) request.arguments().get("description"); TaskItem task = store.create(title, description); try { return new CallToolResult( List.of(new TextContent(objectMapper.writeValueAsString(task))), false); } catch (Exception e) { return errorResult(e.getMessage()); } }); } private SyncToolSpecification toggleTaskCompleteTool() { var tool = new Tool( "toggle_task_complete", "Toggle a task's completion status between complete and incomplete.", objectSchema(Map.of( "task_id", propertySchema("integer", "The numeric ID of the task to toggle") ), List.of("task_id")) ); return new SyncToolSpecification(tool, (exchange, request) -> { int taskId = ((Number) request.arguments().get("task_id")).intValue(); return store.toggleComplete(taskId) .map(task -> textResult( "Task " + task.getId() + " is now " + (task.isComplete() ? "complete" : "incomplete") + ".")) .orElse(textResult("Task with ID " + taskId + " not found.")); }); } private SyncToolSpecification deleteTaskTool() { var tool = new Tool( "delete_task", "Delete a task by its numeric ID.", objectSchema(Map.of( "task_id", propertySchema("integer", "The numeric ID of the task to delete") ), List.of("task_id")) ); return new SyncToolSpecification(tool, (exchange, request) -> { int taskId = ((Number) request.arguments().get("task_id")).intValue(); boolean deleted = store.delete(taskId); return textResult(deleted ? "Task " + taskId + " deleted." : "Task with ID " + taskId + " not found."); }); } // Helper methods for JSON Schema construction private String emptySchema() { return "{\"type\":\"object\",\"properties\":{}}"; } private String objectSchema(Map<String, String> properties, List<String> required) { ObjectNode schema = objectMapper.createObjectNode(); schema.put("type", "object"); ObjectNode propsNode = objectMapper.createObjectNode(); for (var entry : properties.entrySet()) { try { propsNode.set(entry.getKey(), objectMapper.readTree(entry.getValue())); } catch (Exception e) { propsNode.putObject(entry.getKey()).put("type", "string"); } } schema.set("properties", propsNode); schema.set("required", objectMapper.valueToTree(required)); return schema.toString(); } private String propertySchema(String type, String description) { return "{\"type\":\"" + type + "\",\"description\":\"" + description + "\"}"; } private CallToolResult textResult(String text) { return new CallToolResult(List.of(new TextContent(text)), false); } private CallToolResult errorResult(String message) { return new CallToolResult(List.of(new TextContent("Error: " + message)), true); } }Nota:
La superficie de la API del SDK de Java de MCP está evolucionando. El patrón de registro de herramientas que se muestra aquí usa
SyncToolSpecificationpara herramientas sincrónicas. Consulte la documentación del SDK de Java de MCP para ver las expresiones y anotaciones más recientes que pueden simplificar el registro de herramientas.Crear
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; } }Puntos clave:
-
WebMvcSseServerTransportProviderregistra el transporte SSE en la ruta/mcp. -
McpServer.sync(transport)configura las funcionalidades de las herramientas y registra cada especificación de herramienta. - CORS está habilitado porque GitHub Copilot en VS Code realiza solicitudes de origen cruzado a los servidores MCP.
Nota:
En este tutorial se usa
WebMvcSseServerTransportProvider(transporte SSE) porque el SDK de Java de MCP aún no ofrece un transporte HTTP estable que se puede transmitir. Los otros tutoriales de lenguaje (.NET, Python, Node.js) usan HTTP que se puede transmitir. Cuando el SDK de Java agregue compatibilidad con HTTP transmisible, actualice el proveedor de transporte en consecuencia. El transporte SSE es totalmente compatible con VS Code Copilot y otros clientes MCP.-
Crear
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"; } }La clase principal inicia la aplicación Spring Boot y expone un punto de conexión
/healthpara sondeos de estado de Container Apps. Los endpoints de MCP devuelven respuestas JSON-RPC, por lo que se necesita un endpoint de estado independiente para las comprobaciones de salud.
Probar el servidor MCP localmente
Antes de implementar en Azure, compruebe que el servidor MCP funciona ejecutandolo localmente y conectándose desde GitHub Copilot.
Compilación y ejecución:
mvn spring-boot:runAbra VS Code y, a continuación, abra Chat de Copilot y seleccione Modo de agente .
Seleccione el botón Herramientas y, a continuación, Agregar más herramientas...>Agregue el servidor MCP.
Seleccione HTTP (HTTP o Server-Sent Events) y elija Server-Sent Events (SSE) cuando se le solicite el tipo de transporte.
Importante
En este tutorial se usa el transporte SSE, no HTTP transmisible. Debe seleccionar la opción SSE en VS Code para que la conexión funcione correctamente.
Escriba la dirección URL del servidor:
http://localhost:8080/mcpEscriba un identificador de servidor:
tasks-mcpSeleccione Configuración del área de trabajo.
Prueba con: "Mostrarme todas las tareas"
Seleccione Continuar cuando Copilot solicite confirmación de la herramienta MCP.
Debería ver la lista de tareas devuelta desde su almacén en memoria.
Sugerencia
Pruebe otras indicaciones como "Crear una tarea para revisar la PR", "Marcar la tarea 1 como completada" o "Eliminar la tarea 2".
Contenerizar la aplicación
Empaquete la aplicación como contenedor de Docker para que pueda probarla localmente antes de implementarla en Azure.
Cree un
Dockerfilecon una construcción de varias fases: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"]La compilación en varias fases mantiene el tamaño reducido de la imagen final al separar la compilación de Maven de la imagen en tiempo de ejecución.
Compruebe localmente:
docker build -t tasks-mcp-server . docker run -p 8080:8080 tasks-mcp-serverConfirme que el punto de conexión de mantenimiento responde:
curl http://localhost:8080/health
Implementación en Azure Container Apps
Después de incluir la aplicación en contenedores, impleméntela en Azure Container Apps mediante la CLI de Azure. El az containerapp up comando compila la imagen de contenedor en la nube, por lo que no necesita Docker en la máquina para este paso.
Establecer variables de entorno:
RESOURCE_GROUP="mcp-tutorial-rg" LOCATION="eastus" ENVIRONMENT_NAME="mcp-env" APP_NAME="tasks-mcp-server-java"Cree un grupo de recursos y un entorno de Container Apps:
az group create --name $RESOURCE_GROUP --location $LOCATION az containerapp env create \ --name $ENVIRONMENT_NAME \ --resource-group $RESOURCE_GROUP \ --location $LOCATIONImplemente la aplicación contenedora:
az containerapp up \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --environment $ENVIRONMENT_NAME \ --source . \ --ingress external \ --target-port 8080Configurar CORS:
az containerapp ingress cors enable \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --allowed-origins "*" \ --allowed-methods "GET,POST,DELETE,OPTIONS" \ --allowed-headers "*"Nota:
Para producción, reemplace los orígenes comodín por orígenes de confianza específicos. Consulte Protección de servidores MCP en Container Apps.
Compruebe la implementació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
Conexión de GitHub Copilot al servidor implementado
Ahora que el servidor MCP se ejecuta en Azure, configure VS Code para conectar GitHub Copilot al punto de conexión implementado.
Crear o actualizar
.vscode/mcp.json:{ "servers": { "tasks-mcp-server": { "type": "sse", "url": "https://<your-app-fqdn>/mcp" } } }Reemplace
<your-app-fqdn>por el FQDN de la salida de la implementación.Importante
"type"debe ser"sse"porque en este tutorial se usa el transporte SSE. El uso de"http"(HTTP transmitible) provoca fallos de conexión.En VS Code, abra El chat de Copilot en modo agente.
Compruebe que
tasks-mcp-serveraparece en la lista de herramientas. Seleccione Iniciar si es necesario.Pruebe con un mensaje como "¿Qué tareas tengo?"
Configuración del escalado para uso interactivo
Las aplicaciones java tienen tiempos de inicio en frío más largos. Establezca un número mínimo de réplicas para mantener la JVM en caliente:
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--min-replicas 1
Sugerencia
Considere la configuración de recursos adecuada para JVM. Se recomienda un mínimo de 1 vCPU y 2 GiB para aplicaciones de Spring Boot.
Consideraciones de seguridad
En este tutorial se usa un servidor MCP no autenticado para simplificar. Antes de ejecutar un servidor MCP en producción, revise las siguientes recomendaciones. Cuando su servidor MCP sea llamado por un agente impulsado por modelos de lenguaje grandes (LLM), tenga cuidado con los ataques de inyección de solicitudes.
- Autenticación y autorización: proteja el servidor MCP con el identificador de Microsoft Entra. Consulte Protección de servidores MCP en Container Apps.
-
Validación de entrada: use Bean Validation (
@Valid,@NotNull,@Size) para la validación de parámetros de la herramienta. Consulte Validación en Spring Boot. - HTTPS: Azure Container Apps aplica HTTPS de forma predeterminada con certificados TLS automáticos.
- Privilegios mínimos: exponga solo las herramientas que requiere su caso de uso. Evite herramientas que realicen operaciones destructivas sin confirmación.
- CORS: restringir orígenes permitidos a dominios de confianza en producción.
- Registro y supervisión: Registrar las invocaciones de la herramienta MCP para la auditoría utilizando SLF4J y Azure Monitor.
Limpieza de recursos
Si no va a seguir usando esta aplicación, elimine el grupo de recursos para quitar todos los recursos creados en este tutorial:
az group delete --resource-group $RESOURCE_GROUP --yes --no-wait
Paso siguiente
Contenido relacionado
- Introducción a los servidores MCP en Azure Container Apps
- Implementación de un servidor MCP en Container Apps (.NET)
- Implementación de un servidor MCP en Container Apps (Python)
- Implementación de un servidor MCP en Container Apps (Node.js)
- Solución de problemas de servidores MCP en Container Apps
- MCP Java SDK