Tutorial: Menyebarkan server MCP Node.js ke Azure Container Apps

Dalam tutorial ini, Anda membangun server Protokol Konteks Model (MCP) yang mengekspos alat manajemen tugas dengan menggunakan Express dan MCP TypeScript SDK. Anda menyebarkan server ke Azure Container Apps dan menghubungkannya dari GitHub Copilot Chat di VS Code.

Di tutorial ini, Anda akan:

  • Membuat aplikasi Ekspres yang mengekspos alat MCP
  • Menguji server MCP secara lokal dengan GitHub Copilot
  • Kontainerisasi dan sebarkan aplikasi ke Azure Container Apps
  • Menyambungkan GitHub Copilot ke server MCP yang disebarkan

Prasyarat

Membuat perancah aplikasi

Di bagian ini, Anda membuat proyek Node.js baru dengan Express dan MCP TypeScript SDK.

  1. Buat direktori proyek dan inisialisasi:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. Instal dependensi:

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

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

    Konfigurasi ini menargetkan ES2022 dengan resolusi modul Node.js, menghasilkan file yang dikompilasi ke dist/, dan memungkinkan pemeriksaan jenis yang ketat.

  4. Perbarui package.json untuk mengaktifkan modul ES dan menambahkan build dan memulai skrip. Tambahkan atau ganti kolom type dan scripts

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

    Penting

    Atur "type": "module". Kode server MCP menggunakan tingkat awaitatas , yang hanya didukung dalam modul ES.

  5. Buat src/taskStore.ts untuk penyimpanan data dalam memori:

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

    Antarmuka TaskItem menentukan bentuk data tugas. Kelas TaskStore mengelola array dalam memori yang telah diisi sebelumnya dengan data sampel dan menyediakan metode untuk mencantumkan, menemukan, membuat, mengalihkan, dan menghapus tugas. Singleton pada tingkat modul diekspor untuk digunakan oleh alat MCP.

Tentukan alat MCP

Selanjutnya, Anda mendefinisikan server MCP dengan registrasi alat yang memperlihatkan penyimpanan tugas yang dapat diakses oleh klien AI.

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

    Poin utama:

    • McpServer dari TypeScript SDK mendefinisikan server MCP dengan registrasi alat.
    • StreamableHTTPServerTransport menangani protokol HTTP yang dapat dialirkan MCP. Pengaturan sessionIdGenerator: undefined menjalankan server dalam mode stateless.
    • Alat menggunakan skema Zod untuk menentukan parameter input dengan deskripsi.
    • Titik akhir terpisah /health diperlukan untuk melakukan pemeriksaan kesehatan Container Apps.

Menguji server MCP secara lokal

Sebelum menyebarkan ke Azure, verifikasi server MCP berfungsi dengan menjalankannya secara lokal dan menyambungkan dari GitHub Copilot.

  1. Mulai server pengembangan:

    npx tsx src/index.ts
    
  2. Buka Visual Studio Code, buka Obrolan Copilot, dan pilih Mode agen .

  3. Pilih tombol Alat , lalu pilih Tambahkan Alat Lainnya...>Tambahkan Server MCP.

  4. Pilih HTTP (HTTP atau Event Server-Sent).

  5. Masukkan URL server: http://localhost:3000/mcp

    Nota

    Server pengembangan lokal secara default menggunakan port 3000. Ketika dikontainerisasi, Dockerfile mengatur PORT variabel lingkungan ke 8080 untuk mencocokkan dengan port target Container Apps.

  6. Masukkan ID server: tasks-mcp

  7. Pilih Pengaturan Ruang Kerja.

  8. Uji dengan perintah: "Tampilkan saya semua tugas"

  9. Pilih Lanjutkan saat Copilot meminta konfirmasi pemanggilan alat.

Anda seharusnya melihat Copilot menampilkan kembali daftar tugas yang berasal dari penyimpanan dalam memori Anda.

Petunjuk / Saran

Coba perintah lain seperti "Buat tugas untuk meninjau PR", "Tandai tugas 1 sebagai selesai", atau "Hapus tugas 2".

Menerapkan kontainer pada aplikasi

Kemas aplikasi sebagai kontainer Docker sehingga Anda dapat mengujinya secara lokal sebelum menyebarkan ke Azure.

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

    Build multi-tahap mengompilasi TypeScript pada tahap pertama, kemudian membuat citra produksi hanya dengan dependensi runtime dan output JavaScript yang terkompilasi. Variabel PORT lingkungan diatur ke 8080 agar sesuai dengan port target Container Apps.

  2. Verifikasi secara lokal:

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

    Konfirmasi: curl http://localhost:8080/health

Menyebarkan ke Azure Container Apps

Setelah Anda membuat kontainer aplikasi, sebarkan ke Azure Container Apps dengan menggunakan Azure CLI. Perintah ini az containerapp up membangun gambar kontainer di cloud, sehingga Anda tidak memerlukan Docker di komputer Anda untuk langkah ini.

  1. Atur variabel lingkungan:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-node"
    
  2. Buat grup sumber daya:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. Membuat lingkungan Aplikasi Kontainer:

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. Sebarkan aplikasi kontainer:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  5. Konfigurasikan CORS untuk mengizinkan permintaan 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

    Untuk produksi, gantilah asal wildcard * dengan asal tepercaya yang spesifik. Lihat Mengamankan server MCP di Container Apps untuk panduan.

  6. Verifikasi penyebaran:

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

Menyambungkan GitHub Copilot ke server yang disebarkan

Sekarang setelah server MCP berjalan di Azure, konfigurasikan VS Code untuk menyambungkan GitHub Copilot ke endpoint yang telah diterapkan.

  1. Dalam proyek Anda, buat atau perbarui .vscode/mcp.json:

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

    Ganti <your-app-fqdn> dengan FQDN dari output penyebaran.

  2. Di Visual Studio Code, buka Copilot Chat dalam mode Agent.

  3. Jika server tidak muncul secara otomatis, pilih tombol Alat dan pastikan tasks-mcp-server tercantum dalam daftar. Pilih Mulai jika diperlukan.

  4. Uji dengan perintah seperti "Daftar semua tugas saya" untuk mengonfirmasi server MCP yang disebarkan merespons.

Mengonfigurasi penskalaan untuk penggunaan interaktif

Secara default, Azure Container Apps dapat menskalakan ke nol replika. Untuk server MCP yang melayani klien interaktif seperti Copilot, cold start menyebabkan penundaan yang nyata. Atur jumlah replika minimum untuk menjaga setidaknya satu instans tetap berjalan:

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

Pertimbangan keamanan

Tutorial ini menggunakan server MCP yang tidak diaauthenticated untuk kesederhanaan. Sebelum menjalankan server MCP dalam produksi, tinjau rekomendasi berikut. Ketika agen yang didukung oleh model bahasa besar (LLM) memanggil server MCP Anda, waspadai serangan injeksi yang cepat .

  • Autentikasi dan otorisasi: Amankan server MCP Anda dengan menggunakan ID Microsoft Entra. Lihat Mengamankan server MCP di Container Apps.
  • Validasi input: Skema Zod memberikan keamanan tipe, tetapi menambahkan validasi berdasarkan aturan bisnis untuk parameter alat. Pertimbangkan perpustakaan seperti zod-express-middleware untuk validasi pada tingkat permintaan.
  • HTTPS: Azure Container Apps memberlakukan HTTPS secara default dengan sertifikat TLS otomatis.
  • Hak istimewa terkecil: Hanya mengekspos alat yang diperlukan kasus penggunaan Anda. Hindari alat yang melakukan operasi destruktif tanpa konfirmasi.
  • CORS: Membatasi asal yang diizinkan ke domain tepercaya di lingkungan produksi.
  • Pencatatan dan Pemantauan: Pemanggilan alat MCP dicatat untuk audit. Gunakan Azure Monitor dan Analitik Log.

Membersihkan sumber daya

Jika Anda tidak berencana untuk terus menggunakan aplikasi ini, hapus grup sumber daya untuk menghapus semua sumber daya yang Anda buat dalam tutorial ini:

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

Langkah selanjutnya