Partager via


Tutoriel : Déployer un serveur MCP Node.js sur Azure Container Apps

Dans ce tutoriel, vous créez un serveur MCP (Model Context Protocol) qui expose des outils de gestion des tâches à l’aide d’Express et du KIT SDK TypeScript MCP. Vous déployez le serveur sur Azure Container Apps et vous y connectez à partir de GitHub Copilot Chat dans VS Code.

Dans ce tutoriel, vous allez :

  • Créer une application Express qui expose les outils MCP
  • Tester le serveur MCP localement avec GitHub Copilot
  • Conteneuriser et déployer l’application sur Azure Container Apps
  • Connecter GitHub Copilot au serveur MCP déployé

Prerequisites

Créer la structure de l’application

Dans cette section, vous allez créer un projet Node.js avec Express et le Kit de développement logiciel (SDK) TypeScript MCP.

  1. Créez le répertoire du projet et initialisez-le :

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. Installez des dépendances :

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

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

    Cette configuration cible ES2022 avec la résolution de module Node.js génère des fichiers compilés vers dist/ et active une vérification de type stricte.

  4. Mettez à jour package.json pour activer les modules ES et ajouter des scripts de génération et de démarrage. Ajoutez ou remplacez les champs type et scripts.

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

    Important

    Définissez "type": "module". Le code du serveur MCP utilise le niveau awaitsupérieur, qui est uniquement pris en charge dans les modules ES.

  5. Créez src/taskStore.ts pour le magasin de données en mémoire :

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

    L’interface TaskItem définit la forme de données de tâche. La TaskStore classe gère un tableau en mémoire prérempli avec des exemples de données et fournit des méthodes pour répertorier, rechercher, créer, basculer et supprimer des tâches. Un singleton au niveau du module est exporté pour une utilisation par les outils MCP.

Définir les outils MCP

Ensuite, vous définissez le serveur MCP avec des inscriptions d’outils qui exposent le magasin de tâches aux clients IA.

  1. Créer 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`);
    });
    

    Points clés :

    • McpServer du kit SDK TypeScript définit le serveur MCP avec des enregistrements d'outils.
    • StreamableHTTPServerTransport gère le protocole HTTP streamable MCP. Le paramètre sessionIdGenerator: undefined exécute le serveur en mode sans état.
    • Les outils utilisent des schémas Zod pour définir des paramètres d’entrée avec des descriptions.
    • Un point de terminaison /health distinct est requis pour les sondes d’intégrité des Container Apps.

Tester le serveur MCP localement

Avant de déployer sur Azure, vérifiez que le serveur MCP fonctionne en l’exécutant localement et en vous connectant à partir de GitHub Copilot.

  1. Démarrez le serveur de développement :

    npx tsx src/index.ts
    
  2. Ouvrez VS Code, ouvrez Copilot Chat, puis sélectionnez Le mode Agent .

  3. Sélectionnez le bouton Outils , puis sélectionnez Ajouter d’autres outils...>Ajoutez le serveur MCP.

  4. Sélectionnez HTTP (HTTP ou événements envoyés par le serveur).

  5. Entrez l’URL du serveur : http://localhost:3000/mcp

    Note

    Le serveur de développement local est défini par défaut sur le port 3000. Lorsqu’il est conteneurisé, le fichier Dockerfile définit la PORT variable d’environnement sur 8080 pour qu’elle corresponde au port cible Container Apps.

  6. Entrez un ID de serveur : tasks-mcp

  7. Sélectionnez Paramètres de l’espace de travail.

  8. Tester avec une invite : « Afficher toutes les tâches »

  9. Sélectionnez Continuer lorsque Copilot demande la confirmation de l’appel de l’outil.

Vous devriez voir Copilot retourner la liste des tâches depuis votre magasin en mémoire.

Conseil / Astuce

Essayez d'autres invites comme « Créer une tâche pour examiner la demande de tirage », « Marquer la tâche 1 comme terminée » ou « Supprimer la tâche 2 ».

Conteneuriser l’application

Empaqueter l’application en tant que conteneur Docker afin de pouvoir la tester localement avant de le déployer sur Azure.

  1. Créez 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 build à plusieurs étapes compile TypeScript dans la première étape, puis crée une image de production avec uniquement des dépendances d’exécution et la sortie JavaScript compilée. La PORT variable d’environnement est définie sur 8080 pour correspondre au port cible Container Apps.

  2. Vérifiez localement :

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

    Confirmer: curl http://localhost:8080/health

Déployer sur Azure Container Apps

Après avoir conteneurisé l’application, déployez-la sur Azure Container Apps à l’aide d’Azure CLI. La az containerapp up commande génère l’image conteneur dans le cloud. Vous n’avez donc pas besoin de Docker sur votre machine pour cette étape.

  1. Définissez des variables d’environnement :

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-node"
    
  2. Créez un groupe de ressources :

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. Créez un environnement Container Apps :

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. Déployez l’application conteneur :

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  5. Configurez CORS pour autoriser les requêtes GitHub Copilot :

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

    Note

    Pour la production, remplacez les origines génériques * par des origines spécifiques approuvées. Consultez les serveurs MCP sécurisés sur Container Apps pour obtenir des conseils.

  6. Vérifiez le déploiement :

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

Connecter GitHub Copilot au serveur déployé

Maintenant que le serveur MCP s’exécute dans Azure, configurez VS Code pour connecter GitHub Copilot au point de terminaison déployé.

  1. Dans votre projet, créez ou mettez à jour .vscode/mcp.json:

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

    Remplacez <your-app-fqdn> par le FQDN de la sortie du déploiement.

  2. Dans VS Code, ouvrez Copilot Chat en mode Agent.

  3. Si le serveur n’apparaît pas automatiquement, sélectionnez le bouton Outils et vérifiez tasks-mcp-server qu’il est répertorié. Sélectionnez Démarrer si nécessaire.

  4. Testez avec une invite telle que « Répertorier toutes mes tâches » pour confirmer que le serveur MCP déployé répond.

Configurer la mise à l’échelle pour une utilisation interactive

Par défaut, Azure Container Apps peut effectuer une mise à l’échelle vers zéro réplica. Pour les serveurs MCP qui servent des clients interactifs comme Copilot, les démarrages à froid provoquent des retards notables. Définissez un nombre minimal de réplicas pour conserver au moins une instance en cours d’exécution :

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

Considérations relatives à la sécurité

Ce tutoriel utilise un serveur MCP non authentifié pour plus de simplicité. Avant d’exécuter un serveur MCP en production, passez en revue les recommandations suivantes. Lorsqu’un agent alimenté par de grands modèles de langage (LLMs) appelle votre serveur MCP, tenez compte des attaques d’injection de commande.

  • Authentification et autorisation : Sécurisez votre serveur MCP à l’aide de l’ID Microsoft Entra. Consultez les serveurs MCP sécurisés sur Container Apps.
  • Validation d’entrée : les schémas Zod fournissent une sécurité de type, mais ajoutent une validation de règle métier pour les paramètres d’outil. Considérez les bibliothèques comme zod-express-middleware pour la validation au niveau de la demande.
  • HTTPS : Azure Container Apps applique HTTPS par défaut avec des certificats TLS automatiques.
  • Privilège minimum : exposez uniquement les outils dont votre cas d’usage a besoin. Évitez les outils qui effectuent des opérations destructrices sans confirmation.
  • CORS : Restreindre les origines autorisées aux domaines de confiance en production.
  • Journalisation et surveillance : journaliser les appels de l'outil MCP pour l'audit. Utilisez Azure Monitor et Log Analytics.

Nettoyer les ressources

Si vous ne prévoyez pas de continuer à utiliser cette application, supprimez le groupe de ressources pour supprimer toutes les ressources que vous avez créées dans ce tutoriel :

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

Étape suivante