Compartir a través de


Tutorial: Implementación de un servidor MCP de Node.js en Azure Container Apps

En este tutorial, creará un servidor MCP (Model Context Protocol) que expone herramientas de administración de tareas usando Express y el SDK de MCP TypeScript. 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 Express 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

Crea el esqueleto de la aplicación

En esta sección, creará un nuevo proyecto de Node.js con Express y el SDK de TypeScript de MCP.

  1. Cree el directorio del proyecto e inicialícelo:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. Instale las dependencias:

    npm install @modelcontextprotocol/sdk express zod
    npm install -D typescript @types/node @types/express tsx
    
  3. Crear tsconfig.json:

    {
        "compilerOptions": {
            "target": "ES2022",
            "module": "Node16",
            "moduleResolution": "Node16",
            "outDir": "./dist",
            "rootDir": "./src",
            "strict": true,
            "esModuleInterop": true,
            "declaration": true
        },
        "include": ["src/**/*"]
    }
    

    Esta configuración está dirigida a ES2022 con resolución de módulos Node.js, genera archivos compilados en dist/ y habilita la comprobación estricta de tipos.

  4. Actualice package.json para habilitar los módulos de ES y agregue scripts de compilación e inicio. Agregue o reemplace los type campos y scripts :

    {
        "type": "module",
        "scripts": {
            "build": "tsc",
            "start": "node dist/index.js",
            "dev": "tsx watch src/index.ts"
        }
    }
    

    Importante

    Establezca "type": "module". El código de servidor MCP usa el nivel awaitsuperior , que solo se admite en los módulos ES.

  5. Cree src/taskStore.ts para el almacén de datos en memoria:

    export interface TaskItem {
        id: number;
        title: string;
        description: string;
        isComplete: boolean;
        createdAt: string;
    }
    
    class TaskStore {
        private tasks: TaskItem[] = [
            {
                id: 1,
                title: "Buy groceries",
                description: "Milk, eggs, bread",
                isComplete: false,
                createdAt: new Date().toISOString(),
            },
            {
                id: 2,
                title: "Write docs",
                description: "Draft the MCP tutorial",
                isComplete: true,
                createdAt: new Date(Date.now() - 86400000).toISOString(),
            },
        ];
        private nextId = 3;
    
        getAll(): TaskItem[] {
            return [...this.tasks];
        }
    
        getById(id: number): TaskItem | undefined {
            return this.tasks.find((t) => t.id === id);
        }
    
        create(title: string, description: string): TaskItem {
            const task: TaskItem = {
                id: this.nextId++,
                title,
                description,
                isComplete: false,
                createdAt: new Date().toISOString(),
            };
            this.tasks.push(task);
            return task;
        }
    
        toggleComplete(id: number): TaskItem | undefined {
            const task = this.tasks.find((t) => t.id === id);
            if (!task) return undefined;
            task.isComplete = !task.isComplete;
            return task;
        }
    
        delete(id: number): boolean {
            const index = this.tasks.findIndex((t) => t.id === id);
            if (index < 0) return false;
            this.tasks.splice(index, 1);
            return true;
        }
    }
    
    export const store = new TaskStore();
    

    La TaskItem interfaz define la forma de datos de la tarea. La TaskStore clase administra una matriz en memoria rellenada previamente con datos de ejemplo y proporciona métodos para enumerar, buscar, crear, alternar y eliminar tareas. Se exporta un singleton de nivel de módulo para su uso por las herramientas de MCP.

Definición de las herramientas de MCP

A continuación, defina el servidor MCP con registros de herramientas que exponen el almacén de tareas a los clientes de IA.

  1. Crear src/index.ts:

    import express, { Request, Response } from "express";
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    import { z } from "zod";
    import { store } from "./taskStore.js";
    
    const app = express();
    app.use(express.json());
    
    // Health endpoint for Container Apps probes
    app.get("/health", (_req: Request, res: Response) => {
        res.json({ status: "healthy" });
    });
    
    // Create the MCP server
    const mcpServer = new McpServer({
        name: "TasksMCP",
        version: "1.0.0",
    });
    
    // Register tools
    mcpServer.tool("list_tasks", "List all tasks with their ID, title, description, and completion status.", {}, async () => {
        return {
            content: [{ type: "text", text: JSON.stringify(store.getAll(), null, 2) }],
        };
    });
    
    mcpServer.tool(
        "get_task",
        "Get a single task by its numeric ID.",
        { task_id: z.number().describe("The numeric ID of the task to retrieve") },
        async ({ task_id }) => {
            const task = store.getById(task_id);
            return {
                content: [
                    {
                        type: "text",
                        text: task ? JSON.stringify(task, null, 2) : `Task with ID ${task_id} not found.`,
                    },
                ],
            };
        }
    );
    
    mcpServer.tool(
        "create_task",
        "Create a new task with the given title and description. Returns the created task.",
        {
            title: z.string().describe("A short title for the task"),
            description: z.string().describe("A detailed description of what the task involves"),
        },
        async ({ title, description }) => {
            const task = store.create(title, description);
            return {
                content: [{ type: "text", text: JSON.stringify(task, null, 2) }],
            };
        }
    );
    
    mcpServer.tool(
        "toggle_task_complete",
        "Toggle a task's completion status between complete and incomplete.",
        { task_id: z.number().describe("The numeric ID of the task to toggle") },
        async ({ task_id }) => {
            const task = store.toggleComplete(task_id);
            const msg = task
                ? `Task ${task.id} is now ${task.isComplete ? "complete" : "incomplete"}.`
                : `Task with ID ${task_id} not found.`;
            return { content: [{ type: "text", text: msg }] };
        }
    );
    
    mcpServer.tool(
        "delete_task",
        "Delete a task by its numeric ID.",
        { task_id: z.number().describe("The numeric ID of the task to delete") },
        async ({ task_id }) => {
            const deleted = store.delete(task_id);
            const msg = deleted ? `Task ${task_id} deleted.` : `Task with ID ${task_id} not found.`;
            return { content: [{ type: "text", text: msg }] };
        }
    );
    
    // Mount the MCP streamable HTTP transport
    const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
    
    app.post("/mcp", async (req: Request, res: Response) => {
        await transport.handleRequest(req, res, req.body);
    });
    
    app.get("/mcp", async (req: Request, res: Response) => {
        await transport.handleRequest(req, res);
    });
    
    app.delete("/mcp", async (req: Request, res: Response) => {
        await transport.handleRequest(req, res);
    });
    
    // Connect the transport to the MCP server
    await mcpServer.connect(transport);
    
    // Start the Express server
    const PORT = parseInt(process.env.PORT || "3000", 10);
    app.listen(PORT, () => {
        console.log(`MCP server running on http://localhost:${PORT}/mcp`);
    });
    

    Puntos clave:

    • McpServer define el servidor MCP desde el SDK de TypeScript junto con los registros de herramientas.
    • StreamableHTTPServerTransport controla el protocolo HTTP que se puede transmitir por secuencias de MCP. La configuración sessionIdGenerator: undefined ejecuta el servidor en modo sin estado.
    • Las herramientas usan esquemas Zod para definir parámetros de entrada con descripciones.
    • Se requiere un punto de conexión /health independiente para los sondeos de estado de las Container Apps.

Probar el servidor MCP localmente

Antes de implementar en Azure, compruebe que el servidor MCP funciona ejecutandolo localmente y conectándose desde GitHub Copilot.

  1. Inicie el servidor de desarrollo:

    npx tsx src/index.ts
    
  2. Abra VS Code, abra Chat de Copilot y seleccione Modo de agente .

  3. Seleccione el botón Herramientas y, a continuación, seleccione Agregar más herramientas...>Agregue el servidor MCP.

  4. Seleccione HTTP (HTTP o eventos Server-Sent).

  5. Escriba la dirección URL del servidor: http://localhost:3000/mcp

    Nota:

    El servidor de desarrollo local tiene como valor predeterminado el puerto 3000. Cuando se incluye en contenedores, Dockerfile establece la PORT variable de entorno en 8080 para que coincida con el puerto de destino de Container Apps.

  6. Escriba un identificador de servidor: tasks-mcp

  7. Seleccione Configuración del área de trabajo.

  8. Prueba con un indicador: "Muéstrame todas las tareas"

  9. Seleccione Continuar cuando copilot solicite confirmación de invocación de la herramienta.

Debería ver cómo Copilot devuelve la lista de tareas de 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.

  1. Crear un Dockerfile:

    FROM node:20-slim AS build
    WORKDIR /app
    COPY package*.json .
    RUN npm ci
    COPY tsconfig.json .
    COPY src/ src/
    RUN npm run build
    
    FROM node:20-slim
    WORKDIR /app
    COPY package*.json .
    RUN npm ci --omit=dev
    COPY --from=build /app/dist ./dist
    ENV PORT=8080
    EXPOSE 8080
    CMD ["node", "dist/index.js"]
    

    La compilación en varias fases compila TypeScript en la primera fase y, a continuación, crea una imagen de producción con solo las dependencias de tiempo de ejecución y el resultado JavaScript compilado. La PORT variable de entorno se establece en 8080 para que coincida con el puerto de destino de Container Apps.

  2. Compruebe localmente:

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

    Confirmar: 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.

  1. Establecer variables de entorno:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-node"
    
  2. Cree un grupo de recursos:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. Cree un entorno de Container Apps:

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. Implemente la aplicación contenedora:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  5. Configure CORS para permitir solicitudes de GitHub Copilot:

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

    Nota:

    Para la producción, sustituya los orígenes comodín * por orígenes específicos de confianza. Consulte Protección de servidores MCP en Container Apps para obtener instrucciones.

  6. 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.

  1. En el proyecto, cree o actualice .vscode/mcp.json:

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

    Reemplace <your-app-fqdn> por el FQDN de la salida de la implementación.

  2. En VS Code, abra El chat de Copilot en modo agente.

  3. Si el servidor no aparece automáticamente, seleccione el botón Herramientas y compruebe tasks-mcp-server que aparece. Seleccione Iniciar si es necesario.

  4. Pruebe con un mensaje como "Enumerar todas mis tareas" para confirmar que el servidor MCP implementado responde.

Configuración del escalado para uso interactivo

De forma predeterminada, Azure Container Apps puede escalar a cero réplicas. En el caso de los servidores MCP que sirven a clientes interactivos como Copilot, los inicios en frío provocan retrasos notables. Establezca un número mínimo de réplicas para mantener al menos una instancia en ejecución:

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

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 un agente con tecnología de modelos de lenguaje grandes (LLM) llama al servidor MCP, tenga en cuenta los ataques de inyección de solicitudes.

  • Autenticación y autorización: proteja el servidor MCP mediante el identificador de Microsoft Entra. Consulte Protección de servidores MCP en Container Apps.
  • Validación de entrada: los esquemas Zod proporcionan seguridad de tipos, pero agregan validación de reglas de negocio para los parámetros de la herramienta. Considere bibliotecas como zod-express-middleware para la validación a nivel de solicitud.
  • 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: Registra las invocaciones de la herramienta MCP para auditoría. Utilice Azure Monitor y Log Analytics.

Limpieza de recursos

Si no tiene previsto seguir usando esta aplicación, elimine el grupo de recursos para quitar todos los recursos que creó en este tutorial:

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

Paso siguiente