Tutorial: Implantar um servidor MCP Node.js 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 Express e o SDK do TypeScript 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 Express 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 Node.js com o Express e o SDK do TypeScript do MCP.

  1. Crie o diretório do projeto e inicialize-o:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. Instale as dependências:

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

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

    Esta configuração tem como alvo o ES2022 com resolução de módulo do Node.js, gera arquivos compilados para dist/ e permite a verificação estrita de tipos.

  4. Atualize package.json para habilitar módulos ES e adicionar scripts de build e início. Adicione ou substitua os type campos e scripts :

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

    Importante

    Defina "type": "module". O código do servidor MCP usa o nível awaitsuperior, que só tem suporte em módulos ES.

  5. Crie src/taskStore.ts para o armazenamento de dados na memória:

    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();
    

    A TaskItem interface define a forma de dados da tarefa. A TaskStore classe gerencia uma matriz na memória pré-preenchida com dados de exemplo e fornece métodos para listar, localizar, criar, alternar e excluir tarefas. É exportado um singleton no nível do módulo para uso pelas ferramentas do MCP.

Definir as ferramentas do MCP

Em seguida, você define o servidor MCP com registros de ferramentas que expõem o repositório de tarefas a clientes de IA.

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

    Pontos principais:

    • McpServer do SDK do TypeScript define o servidor MCP com registros de ferramentas.
    • StreamableHTTPServerTransport gerencia o protocolo HTTP transmissível do MCP. A configuração sessionIdGenerator: undefined executa o servidor no modo stateless.
    • As ferramentas usam esquemas Zod para definir parâmetros de entrada com descrições.
    • Um ponto de extremidade /health separado é necessário para investigações de integridade de Aplicativos de Contêiner.

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. Inicie o servidor de desenvolvimento:

    npx tsx src/index.ts
    
  2. Abra o VS Code, abra o Copilot Chat e selecione o modo Agente .

  3. Selecione o botão Ferramentas e, em seguida, selecione Adicionar Mais Ferramentas...>Adicionar servidor MCP.

  4. Selecione HTTP (HTTP ou Eventos Server-Sent).

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

    Observação

    O servidor de desenvolvimento local usa como padrão a porta 3000. Quando containerizado, o Dockerfile define a PORT variável de ambiente para 8080 para corresponder à porta de destino do Container Apps.

  6. Insira uma ID do servidor: tasks-mcp

  7. Selecione Configurações do Workspace.

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

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

Você deverá ver o Copilot retornar a lista de tarefas 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. Criar um 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"]
    

    O build de vários estágios compila o TypeScript no primeiro estágio e, depois, cria uma imagem de produção apenas com dependências de runtime e a saída do JavaScript compilada. A PORT variável de ambiente é definida como 8080 para corresponder à porta de destino dos Aplicativos de Contêiner.

  2. Verifique localmente:

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

    Confirmar: 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-node"
    
  2. Crie um grupo de recursos:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. Criar um ambiente de Aplicativos de Contêiner:

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. 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
    
  5. Configure o CORS para permitir solicitações do GitHub Copilot:

    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 para obter orientações.

  6. 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. Em seu projeto, crie ou atualize .vscode/mcp.json:

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

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

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

  3. Se o servidor não aparecer automaticamente, selecione o botão Ferramentas e verifique tasks-mcp-server se está listado. Selecione Iniciar , se necessário.

  4. Teste com um prompt como "Listar todas as minhas tarefas" para confirmar se o servidor MCP implantado responde.

Configurar o dimensionamento para uso interativo

Por padrão, os Aplicativos de Contêiner do Azure podem ser dimensionados para zero réplicas. Para servidores MCP que atendem clientes interativos como o Copilot, inicializações a frio causam atrasos perceptíveis. Defina uma contagem mínima de réplicas para manter pelo menos uma instância em execução:

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

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 um agente alimentado por grandes modelos de linguagem (LLMs) chamar seu servidor MCP, fique atento aos ataques de injeção de prompt.

  • Autenticação e autorização: proteja o servidor MCP usando a ID do Microsoft Entra. Consulte servidores MCP seguros em Aplicativos de Contêiner.
  • Validação de entrada: os esquemas Zod fornecem segurança de tipo, mas adicionam validação de regra de negócios para parâmetros de ferramenta. Considere bibliotecas como zod-express-middleware para validação no nível da solicitação.
  • 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 e monitoramento de logs: registre as invocações da ferramenta MCP para auditoria. Use o Azure Monitor e o Log Analytics.

Limpar os recursos

Se você não planeja continuar usando 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