共用方式為


將 App Service 應用程式整合為 GitHub Copilot Chat 的 MCP 伺服器 (Node.js)

在本教學課程中,您將瞭解如何透過模型內容通訊協定 (MCP) 公開 Express.js 應用程式的功能、將其新增為 GitHub Copilot 的工具,以及在 Copilot 聊天代理程式模式中使用自然語言與您的應用程式互動。

此螢幕快照顯示 Azure App Service 中裝載的 GitHub Copilot 呼叫 Todos MCP 伺服器。

如果您的 Web 應用程式已經有有用的功能,例如購物、旅館預訂或資料管理,很容易就能讓這些功能可供使用:

藉由將 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 應用程式

  1. 在 codespace 終端機中,將必要的 npm 套件新增至您的專案:

    npm install @modelcontextprotocol/sdk@latest zod
    
    1. 開啟 routes/index.js。 為了簡單起見,您將在這裡新增所有 MCP 伺服器程式代碼。
  2. routes/index.js頂端,新增下列需求:

    const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
    const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
    const { z } = require('zod');
    
  3. 在檔案底部,上方 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 進行輸入驗證。
    • 設定物件中的 descriptiondescribe() 中的 inputSchema 會提供適用於工具和輸入的一般人看得懂的描述。 其可協助呼叫代理程序瞭解如何使用工具及其參數。

    此路由會複製現有路由的 create-read-update-delete (CRUD) 功能,這是不必要的,但為了簡單起見,您將保留它。 最佳做法是將應用程式邏輯移至模組,然後從所有路由呼叫模組。

在本機測試 MCP 伺服器

  1. 在codespace終端機中,使用 npm start執行應用程式。

  2. 選取 [在瀏覽器中開啟],然後新增工作。

    保持 npm start 執行狀態。 您的 MCP 伺服器目前正在 http://localhost:3000/api/mcp 運行。

  3. 回到程式代碼空間,開啟 [Copilot Chat],然後在提示方塊中選取 [代理程式 模式]。

  4. 選取 [ 工具] 按鈕,然後在下拉式清單中選取 [ 新增更多工具... ]。

    顯示如何在 GitHub Copilot 聊天代理程式模式中新增 MCP 伺服器的螢幕快照。

  5. 選取 [新增 MCP 伺服器]。

  6. 選取HTTP(HTTP 或 Server-Sent 事件)

  7. [輸入伺服器 URL] 中,輸入 http://localhost:3000/api/mcp

  8. [輸入伺服器標識符] 中,輸入 todos-mcp 或任何您想要的名稱。

  9. 選取 [工作區設定]。

  10. 在新的 [Copilot Chat] 視窗中,輸入類似 「顯示待辦事項」 的指令。

  11. 根據預設,當您叫用 MCP 伺服器時,GitHub Copilot 會顯示安全性確認。 選取繼續

    此螢幕快照顯示 GitHub Copilot Chat 中 MCP 調用的預設安全性訊息。

    您現在應該會看到指出 MCP 工具呼叫成功的回應。

    顯示 GitHub Copilot Chat 視窗中 MCP 工具呼叫回應的螢幕快照。

將您的 MCP 伺服器部署至 App Service

  1. 回到 Codespace 終端機,藉由提交變更(GitHub Actions 方法)或執行 azd up (Azure Developer CLI 方法)來部署變更。

  2. 在 AZD 輸出中,尋找應用程式的 URL。 AZD 輸出中 URL 看起來像這樣:

     Deploying services (azd deploy)
    
       (✓) Done: Deploying service web
       - Endpoint: <app-url>
     
  3. 完成後,azd up開啟.vscode/mcp.json。 將 URL 變更為 <app-url>/api/mcp

  4. 在修改過的 MCP 伺服器組態上方,選取 [ 啟動]。

    顯示如何從本機 mcp.json 檔案手動啟動 MCP 伺服器的螢幕快照。

  5. 啟動新的 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
  • 一般更新:讓您的相依性保持在最新狀態,以減輕已知的弱點。

更多資源

將 AI 整合到 Azure App Service 應用程式中