你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:将 Node.js MCP 服务器部署到 Azure 容器应用

在本教程中,你将生成一个模型上下文协议(MCP)服务器,该服务器使用 Express 和 MCP TypeScript SDK 公开任务管理工具。 将服务器部署到 Azure 容器应用,并从 VS Code 中的 GitHub Copilot 聊天连接到它。

在本教程中,你将了解:

  • 创建公开 MCP 工具的 Express 应用
  • 使用 GitHub Copilot 在本地测试 MCP 服务器
  • 容器化应用并将其部署到 Azure 容器应用
  • 将 GitHub Copilot 连接到部署的 MCP 服务器

先决条件

创建应用基架

在本部分中,你将使用 Express 和 MCP TypeScript SDK 创建新的 Node.js 项目。

  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/**/*"]
    }
    

    此配置针对采用 Node.js 模块解析的 ES2022,将编译后的文件输出到 dist/,并启用严格的类型检查。

  4. 更新 package.json 以启用 ES 模块并添加生成和启动脚本。 添加或替换 typescripts 字段:

    {
        "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 服务器,并通过工具注册将任务存储公开给 AI 客户端。

  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 处理 MCP 可流式传输 HTTP 协议。 设置 sessionIdGenerator: undefined 以无状态模式运行服务器。
    • 工具使用 Zod 架构定义具有说明的输入参数。
    • 容器应用的状态探测需要单独的 /health 终结点。

在本地测试 MCP 服务器

在部署到 Azure 之前,请验证 MCP 服务器是否正常工作,方法是在本地运行它并从 GitHub Copilot 进行连接。

  1. 启动开发服务器:

    npx tsx src/index.ts
    
  2. 打开 VS Code,打开 Copilot 对话助手,然后选择 代理 模式。

  3. 选择 “工具 ”按钮,然后选择“ 添加更多工具...”>添加 MCP 服务器

  4. 选择“HTTP(HTTP 或 Server-Sent 事件)”。

  5. 输入服务器 URL: http://localhost:3000/mcp

    注释

    本地开发服务器默认为端口 3000。 容器化后,Dockerfile 会将 PORT 环境变量设置为 8080 以匹配容器应用目标端口。

  6. 输入服务器 ID: 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 容器应用

容器化应用程序后,使用 Azure CLI 将其部署到 Azure 容器应用。 该 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 是否已列出。 根据需要选择 “开始 ”。

  4. 使用 “列出所有任务” 等提示进行测试,以确认部署的 MCP 服务器响应。

配置缩放以供交互式使用

默认情况下,Azure 容器应用可以扩展到零个副本。 对于为 Copilot 等交互式客户端提供服务的 MCP 服务器,冷启动会导致明显的延迟。 设置最小副本计数以保持至少一个实例运行:

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

安全注意事项

本教程为简单起见,使用未经身份验证的 MCP 服务器。 在生产环境中运行 MCP 服务器之前,请查看以下建议。 当由大型语言模型(LLM)提供支持的代理调用 MCP 服务器时,请注意 提示注入 攻击。

  • 身份验证和授权:使用 Microsoft Entra ID 保护 MCP 服务器。 请参阅 容器应用中的安全 MCP 服务器
  • 输入验证:Zod 架构提供类型安全性,但为工具参数添加业务规则验证。 考虑使用诸如zod-express-middleware这样的库进行请求级验证。
  • HTTPS:Azure 容器应用默认使用自动 TLS 证书强制实施 HTTPS。
  • 最低特权:仅公开用例所需的工具。 避免使用在未经确认的情况下执行破坏性操作的工具。
  • CORS:将允许的源限制为生产中的受信任域。
  • 日志记录和监视:记录 MCP 工具调用进行审核。 使用 Azure Monitor 和 Log Analytics。

清理资源

如果不打算继续使用此应用程序,请删除资源组以删除在本教程中创建的所有资源:

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

后续步骤