在本教學中,你將建立一個模型情境協定(MCP)伺服器,透過 Express 與 MCP TypeScript SDK 來暴露任務管理工具。 你要把伺服器部署到 Azure 容器應用程式,然後用 VS Code 從 GitHub Copilot Chat 連接。
在本教學課程中,您會:
- 建立一個 Express 應用程式來暴露 MCP 工具
- 用 GitHub Copilot 在本地測試 MCP 伺服器
- 將應用程式容器化並部署至 Azure 容器應用程式
- 將 GitHub Copilot 連接到已部署的 MCP 伺服器
先決條件
- 具有有效訂閱的 Azure 帳戶。 免費創建一個。
- Azure CLI 版本 2.62.0 或更新版本。
- Node.js 20 LTS或更高版本。
- 具有 GitHub Copilot 延伸模組的 Visual Studio Code
- Docker 桌面 (可選,僅需在本地測試容器)。
建立應用程式架構
在這個區段,你要用 Express 和 MCP TypeScript SDK 建立一個新的 Node.js 專案。
建立專案目錄並初始化:
mkdir tasks-mcp-server && cd tasks-mcp-server npm init -y安裝相依性:
npm install @modelcontextprotocol/sdk express zod npm install -D typescript @types/node @types/express tsx創建
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/,且啟用嚴格的型別檢查。更新
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 模組。為記憶體內資料儲存建立
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 客戶端。
創建
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 連接來確認它正常運作。
啟動開發伺服器:
npx tsx src/index.ts打開 VS Code,開啟 Copilot 聊天,並選擇 Agent 模式。
選擇 工具 按鈕,然後選擇 新增更多工具...>新增 MCP 伺服器。
選取HTTP(HTTP 或 Server-Sent 事件)。
輸入伺服器網址:
http://localhost:3000/mcp備註
本地開發伺服器預設使用 3000 埠。 容器化後,Dockerfile 會將環境變數設
PORT為 8080,以匹配容器應用程式的目標埠。輸入伺服器 ID:
tasks-mcp選取 [工作區設定]。
用一個提示來測試:「 顯示所有任務」
當 Copilot 請求工具呼叫確認時,選擇 繼續 。
你應該會看到 Copilot 從你的暫存中回傳任務列表。
小提示
試試其他提示,例如「建立任務以檢視 PR」、「標記任務 1 為完成」或「刪除任務 2」。
將應用程式容器化
把應用程式打包成 Docker 容器,這樣你可以在本地測試再部署到 Azure。
建立
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,以匹配容器應用程式的目標埠。請在本地確認:
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。
設定環境變數:
RESOURCE_GROUP="mcp-tutorial-rg" LOCATION="eastus" ENVIRONMENT_NAME="mcp-env" APP_NAME="tasks-mcp-server-node"建立資源群組:
az group create --name $RESOURCE_GROUP --location $LOCATION建立容器應用程式環境:
az containerapp env create \ --name $ENVIRONMENT_NAME \ --resource-group $RESOURCE_GROUP \ --location $LOCATION部署容器應用程式:
az containerapp up \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --environment $ENVIRONMENT_NAME \ --source . \ --ingress external \ --target-port 8080設定 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 伺服器 以獲得指引。驗證部署:
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 連接到已部署的端點。
在您的專案中,建立或更新
.vscode/mcp.json:{ "servers": { "tasks-mcp-server": { "type": "http", "url": "https://<your-app-fqdn>/mcp" } } }用部署輸出的 FQDN 替換
<your-app-fqdn>。在 VS Code 中,以客服模式開啟 Copilot 聊天。
如果伺服器沒有自動出現,請點選 工具 按鈕並確認
tasks-mcp-server是否被列出。 如果需要,選擇 開始 。用 「列出我所有任務」 的提示來測試,確認已部署的 MCP 伺服器有回應。
為互動使用設定縮放
預設情況下,Azure 容器應用程式可以擴展到零複本。 對於服務互動客戶端如 Copilot 的 MCP 伺服器,冷啟動會造成明顯延遲。 設定最小副本數量,以維持至少一個實例在運行:
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--min-replicas 1
安全性考慮
本教學使用未認證的 MCP 伺服器以簡化操作。 在正式環境中運行 MCP 伺服器前,請先檢視以下建議。 當由大型語言模型 (LLM) 技術支援的 Agent 呼叫您的 MCP 伺服器時,請注意提示注入攻擊。
- 認證與授權:使用 Microsoft Entra ID 保護你的 MCP 伺服器。 請參閱 容器應用程式上的 Secure MCP 伺服器。
- 輸入驗證:Zod 架構提供型別安全,但會加入工具參數的商業規則驗證。 可以考慮像 zod-express-middleware 這類函式庫來進行請求層級驗證。
- HTTPS:Azure 容器應用程式預設會強制執行 HTTPS,並自動使用 TLS 憑證。
- 最低權限:只公開你使用情境所需的工具。 避免使用未經確認就進行破壞性操作的工具。
- CORS:在執行環境中只允許受信任的網域作為來源。
- 日誌與監控:記錄 MCP 工具的調用以進行稽核。 使用 Azure 監視器 和日誌分析。
清理資源
如果你不打算繼續使用這個應用程式,請刪除資源群組,移除你在教學中建立的所有資源:
az group delete --resource-group $RESOURCE_GROUP --yes --no-wait