在本教學課程中,您將瞭解如何透過模型內容通訊協定 (MCP) 公開 Express.js 應用程式的功能、將其新增為 GitHub Copilot 的工具,以及在 Copilot 聊天代理程式模式中使用自然語言與您的應用程式互動。
如果您的 Web 應用程式已經有有用的功能,例如購物、旅館預訂或資料管理,很容易就能讓這些功能可供使用:
- 支援 MCP 整合的任何應用程式,例如 Visual Studio Code 或 GitHub Codespaces 中的 GitHub Copilot 聊天代理程式模式。
- 使用 MCP 用戶端存取遠端工具的自訂代理程式。
藉由將 MCP 伺服器新增至 Web 應用程式,您即可讓代理程式瞭解及使用應用程式在回應使用者提示時的功能。 這表示您的應用程式可以執行的任何動作,代理程式也可以執行。
- 將 MCP 伺服器新增至 Web 應用程式。
- 在 GitHub Copilot 聊天代理程式模式中本機測試 MCP 伺服器。
- 將 MCP 伺服器部署至 Azure App Service,並在 GitHub Copilot Chat 中連線到它。
Prerequisites
本教學課程假設您使用教學 課程:將 Node.js + MongoDB Web 應用程式部署至 Azure 中使用的範例。
至少,在 GitHub Codespaces 中開啟 範例應用程式 ,並執行 azd up來部署應用程式。
將 MCP 伺服器新增至 Web 應用程式
在 codespace 終端機中,將必要的 npm 套件新增至您的專案:
npm install @modelcontextprotocol/sdk@latest zod-
- 開啟 routes/index.js。 為了簡單起見,您將在這裡新增所有 MCP 伺服器程式代碼。
在 routes/index.js頂端,新增下列需求:
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'); const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js'); const { z } = require('zod');在檔案底部,上方
module.exports = router;新增 MCP 伺服器的下列路由。router.post('/api/mcp', async function(req, res, next) { try { // Stateless server instance for each request const server = new McpServer({ name: "task-crud-server", version: "1.0.0" }); // Register tools server.registerTool( "create_task", { description: 'Create a new task', inputSchema: { taskName: z.string().describe('Name of the task to create') }, }, async ({ taskName }) => { const task = new Task({ taskName: taskName, createDate: new Date(), }); await task.save(); return { content: [ { type: 'text', text: `Task created: ${JSON.stringify(task)}` } ] }; } ); server.registerTool( "get_tasks", { description: 'Get all tasks' }, async () => { const tasks = await Task.find(); return { content: [ { type: 'text', text: `All tasks: ${JSON.stringify(tasks, null, 2)}` } ] }; } ); server.registerTool( "get_task", { description: 'Get a task by ID', inputSchema: { id: z.string().describe('Task ID') }, }, async ({ id }) => { try { const task = await Task.findById(id); if (!task) { throw new Error(); } return { content: [ { type: 'text', text: `Task: ${JSON.stringify(task)}` } ] }; } catch (error) { return { content: [ { type: 'text', text: `Task not found with ID: ${id}` } ], isError: true }; } } ); server.registerTool( "update_task", { description: 'Update a task', inputSchema: { id: z.string().describe('Task ID'), taskName: z.string().optional().describe('New task name'), completed: z.boolean().optional().describe('Task completion status') }, }, async ({ id, taskName, completed }) => { try { const updateData = {}; if (taskName !== undefined) updateData.taskName = taskName; if (completed !== undefined) { updateData.completed = completed; if (completed === true) { updateData.completedDate = new Date(); } } const task = await Task.findByIdAndUpdate(id, updateData); if (!task) { throw new Error(); } return { content: [ { type: 'text', text: `Task updated: ${JSON.stringify(task)}` } ] }; } catch (error) { return { content: [ { type: 'text', text: `Task not found with ID: ${id}` } ], isError: true }; } } ); server.registerTool( "delete_task", { description: 'Delete a task', inputSchema: { id: z.string().describe('Task ID to delete') }, }, async ({ id }) => { try { const task = await Task.findByIdAndDelete(id); if (!task) { throw new Error(); } return { content: [ { type: 'text', text: `Task deleted successfully: ${JSON.stringify(task)}` } ] }; } catch (error) { return { content: [ { type: 'text', text: `Task not found with ID: ${id}` } ], isError: true }; } } ); // Create fresh transport for this request const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); // Clean up when request closes res.on('close', () => { transport.close(); server.close(); }); await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error handling MCP request:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } });此路由會將 MCP 伺服器端點設定為
<url>/api/mcp,並在 MCP TypeScript SDK 中使用無狀態模式模式。-
server.registerTool()會在 MCP 伺服器新增工具及其實作。 - SDK 會使用 zod 進行輸入驗證。
-
設定物件中的
description和describe()中的inputSchema會提供適用於工具和輸入的一般人看得懂的描述。 其可協助呼叫代理程序瞭解如何使用工具及其參數。
此路由會複製現有路由的 create-read-update-delete (CRUD) 功能,這是不必要的,但為了簡單起見,您將保留它。 最佳做法是將應用程式邏輯移至模組,然後從所有路由呼叫模組。
-
在本機測試 MCP 伺服器
在codespace終端機中,使用
npm start執行應用程式。選取 [在瀏覽器中開啟],然後新增工作。
保持
npm start執行狀態。 您的 MCP 伺服器目前正在http://localhost:3000/api/mcp運行。回到程式代碼空間,開啟 [Copilot Chat],然後在提示方塊中選取 [代理程式 模式]。
選取 [ 工具] 按鈕,然後在下拉式清單中選取 [ 新增更多工具... ]。
選取 [新增 MCP 伺服器]。
選取HTTP(HTTP 或 Server-Sent 事件)。
在 [輸入伺服器 URL] 中,輸入 http://localhost:3000/api/mcp。
在 [輸入伺服器標識符] 中,輸入 todos-mcp 或任何您想要的名稱。
選取 [工作區設定]。
在新的 [Copilot Chat] 視窗中,輸入類似 「顯示待辦事項」 的指令。
根據預設,當您叫用 MCP 伺服器時,GitHub Copilot 會顯示安全性確認。 選取繼續。
您現在應該會看到指出 MCP 工具呼叫成功的回應。
將您的 MCP 伺服器部署至 App Service
回到 Codespace 終端機,藉由提交變更(GitHub Actions 方法)或執行
azd up(Azure Developer CLI 方法)來部署變更。在 AZD 輸出中,尋找應用程式的 URL。 AZD 輸出中 URL 看起來像這樣:
Deploying services (azd deploy) (✓) Done: Deploying service web - Endpoint: <app-url>
完成後,
azd up開啟.vscode/mcp.json。 將 URL 變更為<app-url>/api/mcp。在修改過的 MCP 伺服器組態上方,選取 [ 啟動]。
啟動新的 GitHub Copilot 聊天視窗。 您應該能夠在 Copilot 代理程式中檢視、建立、更新和刪除工作。
安全性最佳做法
當 MCP 伺服器由由大型語言模型 (LLM) 支援的代理程式呼叫時,請注意 提示插入 式攻擊。 請考慮下列安全性最佳做法:
- 認證與授權:以 Microsoft Entra 認證保護你的 MCP 伺服器,確保只有授權使用者或代理能存取你的工具。 請參閱透過 Microsoft Entra 驗證,從 Visual Studio Code 向 Azure App Service 發出安全的模型內容通訊協定呼叫 (機器翻譯)
-
輸入驗證和清理:本教學課程中的範例程式代碼會使用 zod 進行輸入驗證,確保傳入的數據符合預期的架構。 如需其他安全性,請考慮:
- 在處理之前驗證並清理所有使用者輸入,特別是針對資料庫查詢或輸出中使用的欄位。
- 如果您的 API 由瀏覽器使用,則應在回應中逸出輸出,以防止跨網站指令碼 (XSS)。
- 在您的模型中套用嚴格的架構和預設值,以避免非預期的數據。
- HTTPS: 此範例依賴 Azure App Service,其預設會強制執行 HTTPS,並提供免費的 TLS/SSL 憑證來加密傳輸中的數據。
- 最低許可權原則:只公開使用案例所需的工具和數據。 除非必要,否則請避免公開敏感性作業。
- 速率限制和節流:使用 API 管理 或自定義中間件來防止濫用和阻斷服務攻擊。
- 記錄和監視:記錄存取和使用MCP端點以進行稽核和異常偵測。 監視可疑活動。
- CORS 設定:如果您的 MCP 伺服器是從瀏覽器存取,請將跨原始來源要求限制為受信任的網域。 如需詳細資訊,請參閱 啟用CORS。
- 一般更新:讓您的相依性保持在最新狀態,以減輕已知的弱點。