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

将应用服务应用集成为用于 GitHub Copilot Chat 的 MCP 服务器 (Node.js)

本教程介绍如何通过模型上下文协议(MCP)公开 Express.js 应用的功能,将其作为工具添加到 GitHub Copilot,并在 Copilot Chat 代理模式下使用自然语言与应用交互。

显示 GitHub Copilot 调用 Azure 应用服务中托管的 Todos MCP 服务器的屏幕截图。

如果 Web 应用程序已具有有用的功能(如购物、酒店预订或数据管理),则可以轻松地使这些功能可供以下功能使用:

通过将 MCP 服务器添加到 Web 应用,可以在代理响应用户提示时了解和使用应用的功能。 这意味着你的应用可以执行的任何操作,智能助手也可以执行。

  • 将 MCP 服务器添加到 Web 应用。
  • 在 GitHub Copilot Chat 代理模式下本地测试 MCP 服务器。
  • 将 MCP 服务器部署到 Azure 应用服务,并在 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. 路由/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 进行输入验证。
    • description在配置对象中,describe()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 Chat 代理模式下添加 MCP 服务器的屏幕截图。

  5. 选择 “添加 MCP 服务器”。

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

  7. Enter 服务器 URL 中,键入 http://localhost:3000/api/mcp

  8. Enter 服务器 ID 中,键入 todos-mcp 或任何喜欢的名称。

  9. 选择 工作区设置

  10. 在新的 Copilot Chat 窗口中,键入类似于“显示待办事项”的内容。

  11. 默认情况下,GitHub Copilot 在调用 MCP 服务器时会显示安全确认。 选择继续

    显示 GitHub Copilot Chat 中 MCP 调用的默认安全消息的屏幕截图。

    现在应会看到一个响应,指示 MCP 工具调用成功。

    屏幕截图显示了在 GitHub Copilot Chat 窗口中调用 MCP 工具时的响应。

将 MCP 服务器部署到应用服务

  1. 返回 codespace 终端,通过提交更改(GitHub Actions 方法)或运行 azd up (Azure 开发人员 CLI 方法)来部署更改。

  2. 在 AZD 输出中,找到应用的 URL。 该 URL 在 AZD 输出中如下所示:

     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 应用服务的安全模型上下文协议调用
  • 输入验证和清理:本教程中的示例代码使用 zod 进行输入验证,确保传入数据与预期的架构匹配。 若要提高安全性,请考虑:
    • 在处理之前验证和清理所有用户输入,尤其是对于数据库查询或输出中使用的字段。
    • 如果 API 被浏览器使用,则对响应中的输出进行转义以防止跨站脚本 (XSS)。
    • 在模型中应用严格的架构和默认值以避免意外的数据。
  • HTTPS: 此示例依赖于 Azure 应用服务,该服务默认强制实施 HTTPS,并提供免费的 TLS/SSL 证书来加密传输中的数据。
  • 最低特权原则:仅公开用例所需的工具和数据。 除非必要,否则避免公开敏感操作。
  • 速率限制和限制:使用 API 管理 或自定义中间件来防止滥用和拒绝服务攻击。
  • 日志记录和监视:用于审核和异常检测的 MCP 终结点的日志访问和使用情况。 监视可疑活动。
  • CORS 配置:如果从浏览器访问 MCP 服务器,请将跨域请求限制为受信任的域。 有关详细信息,请参阅 “启用 CORS”。
  • 常规更新:使依赖项保持最新,以缓解已知漏洞。

更多资源

将 AI 集成到 Azure 应用服务应用程序中