مشاركة عبر


التعليم: نشر خادم Node.js MCP إلى Azure Container Apps

في هذا الدرس، تبني خادم بروتوكول السياق النموذجي (MCP) الذي يعرض أدوات إدارة المهام باستخدام Express وحزمة تطوير البرمجيات MCP TypeScript. تقوم بنشر الخادم إلى Azure Container Apps وتتصل به من GitHub Copilot Chat في VS Code.

في هذا البرنامج التعليمي، سوف تتعلّم:

  • أنشئ تطبيق إكسبريس يعرض أدوات MCP
  • اختبار خادم MCP محليا باستخدام GitHub Copilot
  • Containerize ونشر التطبيق في Azure Container Apps
  • توصيل GitHub Copilot بخادم MCP المنشور

المتطلبات المسبقه

أنشئ منصة التطبيق

في هذا القسم، تنشئ مشروع Node.js جديد باستخدام Express وحزمة تطوير MCP TypeScript.

  1. أنشئ مجلد المشروع وقم بتهيئته:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. تثبيت التبعيات:

    npm install @modelcontextprotocol/sdk express zod
    npm install -D typescript @types/node @types/express tsx
    
  3. إنشاء tsconfig.json:

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

    يستهدف هذا التكوين ES2022 بدقة Node.js للوحدات، ويخرج الملفات المترجمة إلى dist/، ويتيح التحقق الصارم من الأنواع.

  4. قم بالتحديث package.json لتفعيل وحدات ES وإضافة سكريبتات البناء والتشغيل. أضف أو استبدل الحقول type و scripts :

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

    مهم

    ضبط "type": "module". يستخدم كود خادم MCP كود المستوى awaitالأعلى ، وهو مدعوم فقط في وحدات ES.

  5. إنشاء src/taskStore.ts لمخزن البيانات في الذاكرة:

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

    تحدد الواجهة TaskItem شكل بيانات المهمة. تدير الفئة TaskStore مصفوفة في الذاكرة مجهزة مسبقا ببيانات عينة وتوفر طرقا لسرد المهام، والعثور عليها، وإنشائها، وتبديلها، وحذفها. يتم تصدير ملف واحد على مستوى الوحدة لاستخدامه من قبل أدوات MCP.

تعريف أدوات MCP

بعد ذلك، تحدد خادم MCP بتسجيلات أدوات تعرض مخزن المهام لعملاء الذكاء الاصطناعي.

  1. إنشاء 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`);
    });
    

    النقاط الرئيسية:

    • McpServer من TypeScript SDK يحدد خادم MCP مع تسجيل الأدوات.
    • StreamableHTTPServerTransport تتعامل مع بروتوكول HTTP القابل للبث في MCP. تشغيل sessionIdGenerator: undefined الخادم في وضع بدون حالة.
    • تستخدم الأدوات مخططات زود لتعريف معلمات الإدخال مع الأوصاف.
    • يتطلب الأمر نقطة نهاية منفصلة /health لاختبارات صحة تطبيقات الحاويات.

اختبار خادم MCP محليا

قبل النشر على Azure، تحقق من عمل خادم MCP عن طريق تشغيله محليا والاتصال من GitHub Copilot.

  1. بدء تشغيل خادم التطوير:

    npx tsx src/index.ts
    
  2. افتح كود VS، افتح دردشة Copilot، واختر وضع الوكيل .

  3. اختر زر الأدوات ، ثم اختر إضافة المزيد من الأدوات...>أضف خادم MCP.

  4. حدد HTTP (HTTP أو أحداث Server-Sent).

  5. أدخل رابط الخادم: http://localhost:3000/mcp

    ‏‫ملاحظة‬

    خادم التطوير المحلي ينصب افتراضيا على المنفذ 3000. عند الحاوية، يضبط ملف دوكر متغير PORT البيئة على 8080 ليطابق منفذ الهدف في تطبيقات الحاويات.

  6. أدخل معرف الخادم: tasks-mcp

  7. حدد إعدادات مساحة العمل.

  8. اختبر مع مطلب: "أرني كل المهام"

  9. اختر متابعة عندما يطلب Copilot تأكيد استدعاء الأداة.

يجب أن ترى Copilot يعيد قائمة المهام من مخزن الذاكرة الخاص بك.

نصيحة

جرب تعليمات أخرى مثل "إنشاء مهمة لمراجعة الPR"، "وسم المهمة 1 كمكتملة"، أو "حذف المهمة 2".

ضع التطبيق في حاوية

قم بتغليف التطبيق كحاوية Docker حتى تتمكن من اختباره محليا قبل نشره على Azure.

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

    يقوم البناء متعدد المراحل بترجمة TypeScript في المرحلة الأولى، ثم ينشئ صورة إنتاجية تحتوي فقط على تبعيات وقت التشغيل ومخرجات JavaScript المترجمة. يتم تعيين PORT متغير البيئة على 8080 ليطابق منفذ الهدف الخاص بتطبيقات الحاويات.

  2. تحقق محليا:

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

    تأكيد: curl http://localhost:8080/health

التوزيع إلى Azure Container Apps

بعد أن تقوم بتحويل التطبيق إلى حاوية، قم بنشره في Azure Container Apps باستخدام Azure CLI. تقوم هذه الأوامر az containerapp up ببناء صورة الحاوية في السحابة، لذا لا تحتاج إلى Docker على جهازك لهذه الخطوة.

  1. حدد متغيرات البيئة:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-node"
    
  2. أنشئ مجموعة موارد:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. إنشاء بيئة تطبيقات الحاويات:

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. نشر تطبيق الحاوية:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  5. قم بتكوين CORS للسماح بطلبات GitHub Copilot:

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

    ‏‫ملاحظة‬

    للإنتاج، استبدل أصول البطاقات * البرية بأصول موثوقة محددة. راجع خوادم MCP الآمنة على تطبيقات الحاويات للإرشاد.

  6. تحقق من النشر:

    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 بالخادم المنشور

الآن بعد أن يعمل خادم MCP في Azure، قم بتكوين VS Code لربط GitHub Copilot بنقطة النهاية المنشورة.

  1. في مشروعك، أنشئ أو حدث .vscode/mcp.json:

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

    استبدلها <your-app-fqdn> ب FQDN من مخرجات النشر.

  2. في VS Code، افتح دردشة Copilot في وضع الوكيل.

  3. إذا لم يظهر الخادم تلقائيا، اختر زر الأدوات وتحقق tasks-mcp-server من القائمة. اختر Start إذا لزم الأمر.

  4. اختبر بطلب مثل "سرد جميع مهامي" للتأكد من استجابة خادم MCP المنتشرة.

تكوين التوسع للاستخدام التفاعلي

بشكل افتراضي، يمكن لتطبيقات حاويات Azure أن تتوسع إلى صفر نسخ. بالنسبة لخوادم MCP التي تخدم عملاء تفاعليين مثل Copilot، يسبب التشغيل البارد تأخيرات ملحوظة. حدد الحد الأدنى لعدد النسخ للحفاظ على تشغيل نسخة واحدة على الأقل:

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

اعتبارات الأمان

يستخدم هذا الدرس خادم MCP غير مصادق للبساطة. قبل تشغيل خادم MCP في الإنتاج، راجع التوصيات التالية. عندما يتصل وكيل مدعوم بنماذج لغوية كبيرة (LLMs) بخادم MCP الخاص بك، كن على علم بهجمات حقن الطلبات .

  • المصادقة والتفويض: أمن خادم MCP الخاص بك باستخدام Microsoft Entra ID. انظر خوادم MCP الآمنة على تطبيقات الحاويات.
  • التحقق من صحة المدخلات: توفر مخططات Zod أمان النوع، لكنها تضيف التحقق من قواعد الأعمال لمعلمات الأداة. فكر في مكتبات مثل zod-express-middleware للتحقق من صحة على مستوى الطلب.
  • HTTPS: Azure Container Apps يفرض HTTPS بشكل افتراضي مع شهادات TLS تلقائية.
  • أقل امتياز: اعرض فقط الأدوات التي تتطلبها حالتك. تجنب الأدوات التي تقوم بعمليات مدمرة دون تأكيد.
  • CORS: تقييد المصادر المسموح بها على النطاقات الموثوقة في الإنتاج.
  • التسجيل والمراقبة: سجل استدعاءات أدوات MCP للتدقيق. استخدم Azure Monitor وLog Analytics.

تنظيف الموارد

إذا لم تكن تخطط للاستمرار في استخدام هذا التطبيق، احذف مجموعة الموارد لإزالة جميع الموارد التي أنشأتها في هذا الدرس:

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

الخطوة التالية