Zelfstudie: Een Node.js MCP-server implementeren in Azure Container Apps

In deze zelfstudie bouwt u een MCP-server (Model Context Protocol) die hulpprogramma's voor taakbeheer beschikbaar maakt met behulp van Express en de MCP TypeScript SDK. U implementeert de server in Azure Container Apps en maakt er verbinding mee vanuit GitHub Copilot Chat in VS Code.

In deze handleiding leert u:

  • Een Express-app maken die MCP-hulpprogramma's beschikbaar maakt
  • De MCP-server lokaal testen met GitHub Copilot
  • De app containeriseren en implementeren in Azure Container Apps
  • GitHub Copilot verbinden met de geïmplementeerde MCP-server

Vereiste voorwaarden

De scaffold van de app maken

In deze sectie maakt u een nieuw Node.js project met Express en de MCP TypeScript SDK.

  1. Maak de projectmap en initialiseer deze:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. Afhankelijkheden installeren:

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

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

    Deze configuratie is gericht op ES2022 met Node.js moduleresolutie, uitvoer gecompileerde bestanden naar dist/en maakt strikte typecontrole mogelijk.

  4. Update package.json voor het inschakelen van ES-modules en het toevoegen van build- en startscripts. Voeg de velden toe type of vervang dezescripts:

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

    Belangrijk

    Stel "type": "module"in. De MCP-servercode maakt gebruik van het hoogste niveau await, dat alleen wordt ondersteund in ES-modules.

  5. Maak src/taskStore.ts voor de gegevensopslag in het geheugen aan.

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

    De TaskItem-interface definieert de vorm van de taakgegevens. De TaskStore klasse beheert een in-memory matrix die vooraf is ingevuld met voorbeeldgegevens en biedt methoden voor het weergeven, zoeken, maken, in- en uitschakelen van taken. Een singleton op moduleniveau wordt geëxporteerd voor gebruik door de MCP-hulpprogramma's.

Definieer de MCP-hulpprogramma's

Vervolgens definieert u de MCP-server met hulpprogrammaregistraties waarmee het taakarchief beschikbaar wordt gesteld aan AI-clients.

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

    Belangrijke punten:

    • McpServer definieert de MCP-server met registraties van tools vanuit de TypeScript SDK.
    • StreamableHTTPServerTransport verwerkt het HTTP-protocol dat kan worden gestreamd met MCP. Door de instelling sessionIdGenerator: undefined draait de server in stateless-modus.
    • Hulpprogramma's gebruiken Zod-schema's om invoerparameters met beschrijvingen te definiëren.
    • Er is een afzonderlijk /health eindpunt vereist voor gezondheidscontroles van Container Apps.

De MCP-server lokaal testen

Voordat u implementeert in Azure, controleert u of de MCP-server werkt door deze lokaal uit te voeren en verbinding te maken vanuit GitHub Copilot.

  1. Start de ontwikkelserver:

    npx tsx src/index.ts
    
  2. Open VS Code, open Copilot Chat en selecteer agentmodus .

  3. Selecteer de knop Extra en selecteer vervolgens Meer hulpprogramma's toevoegen...>MCP-server toevoegen.

  4. Selecteer HTTP (HTTP of Server-Sent-gebeurtenissen).

  5. Voer de server-URL in: http://localhost:3000/mcp

    Opmerking

    De lokale ontwikkelserver wordt standaard ingesteld op poort 3000. Wanneer de container is geplaatst, stelt het Dockerfile de PORT omgevingsvariabele in op 8080 zodat deze overeenkomt met de doelpoort van Container Apps.

  6. Voer een server-id in: tasks-mcp

  7. Selecteer Werkruimte-instellingen.

  8. Test met een prompt: 'Toon alle taken'

  9. Selecteer Doorgaan wanneer Copilot om bevestiging van de aanroep van het hulpprogramma vraagt.

U zou moeten zien dat Copilot de lijst met taken uit uw in-memory opslag retourneert.

Aanbeveling

Probeer andere prompts zoals 'Creëer een taak om de PR te controleren', 'Taak 1 markeren als voltooid' of 'Taak 2 verwijderen'.

De toepassing in een container plaatsen

Verpak de toepassing als een Docker-container, zodat u deze lokaal kunt testen voordat u implementeert in Azure.

  1. Maak een 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"]
    

    De build met meerdere fasen compileert TypeScript in de eerste fase en maakt vervolgens een productie-installatiekopie met alleen runtime-afhankelijkheden en de gecompileerde JavaScript-uitvoer. De PORT omgevingsvariabele is ingesteld op 8080 zodat deze overeenkomt met de doelpoort van Container Apps.

  2. Lokaal verifiëren:

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

    Bevestigen: curl http://localhost:8080/health

Implementeren in Azure Container Apps

Nadat u de toepassing in een container hebt geplaatst, implementeert u deze in Azure Container Apps met behulp van Azure CLI. De opdracht az containerapp up bouwt de containerafbeelding in de cloud, zodat u Docker niet nodig hebt op uw machine voor deze stap.

  1. Omgevingsvariabelen instellen:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-node"
    
  2. Een resourcegroep maken:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. Een Container Apps-omgeving maken:

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. De container-app implementeren:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  5. CONFIGUREER CORS om GitHub Copilot-aanvragen toe te staan:

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

    Opmerking

    Vervang tijdens de productiefase de jokertekens * door specifieke vertrouwde bronnen. Zie Beveiligde MCP-servers in Container Apps voor hulp.

  6. Controleer de implementatie:

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

GitHub Copilot verbinden met de geïmplementeerde server

Nu de MCP-server wordt uitgevoerd in Azure, configureert u VS Code om GitHub Copilot te verbinden met het geïmplementeerde eindpunt.

  1. Maak of werk .vscode/mcp.jsonhet volgende bij in uw project:

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

    Vervang <your-app-fqdn> door de FQDN uit de implementatie-uitvoer.

  2. Open Copilot Chat in Agent modus in VS Code.

  3. Als de server niet automatisch wordt weergegeven, selecteert u de knop Extra en verifieert u of tasks-mcp-server wordt weergegeven. Selecteer indien nodig Start.

  4. Test met een prompt zoals 'Al mijn taken vermelden' om te bevestigen dat de geïmplementeerde MCP-server reageert.

Configureer schaling voor interactief gebruik

Azure Container Apps kan standaard worden geschaald naar nul replica's. Voor MCP-servers die interactieve clients zoals Copilot bedienen, leidt koud starten tot merkbare vertragingen. Stel een minimumaantal replica's in om ten minste één exemplaar actief te houden:

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

Beveiligingsoverwegingen

In deze zelfstudie wordt gebruikgemaakt van een niet-geverifieerde MCP-server voor het gemak. Voordat u een MCP-server in productie draait, bekijk de volgende aanbevelingen. Wanneer een agent aangedreven door grote taalmodellen (LLM's) uw MCP-server aanroept, houd rekening met promptinjectie-aanvallen.

  • Verificatie en autorisatie: beveilig uw MCP-server met behulp van Microsoft Entra-id. Zie Beveiligde MCP-servers in Container Apps.
  • Invoervalidatie: Zod-schema's bieden typeveiligheid, maar voeg bedrijfsregelvalidatie toe voor hulpprogrammaparameters. Overweeg bibliotheken zoals zod-express-middleware voor validatie op aanvraagniveau.
  • HTTPS: Azure Container Apps dwingt HTTPS standaard af met automatische TLS-certificaten.
  • Minimale bevoegdheid: alleen de hulpprogramma's beschikbaar maken die uw use-case vereist. Vermijd hulpprogramma's die destructieve bewerkingen uitvoeren zonder bevestiging.
  • CORS: Toegestane oorsprongen beperken tot vertrouwde domeinen in productie.
  • Logboekregistratie en bewaking: MCP-hulpprogramma-aanroepen loggen voor auditing. Gebruik Azure Monitor en Log Analytics.

De hulpbronnen opschonen

Als u niet van plan bent om deze toepassing te blijven gebruiken, verwijdert u de resourcegroep om alle resources te verwijderen die u in deze zelfstudie hebt gemaakt:

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

Volgende stap