次の方法で共有


App Service アプリを GitHub Copilot Chat 用 MCP サーバーとして統合する (Node.js)

このチュートリアルでは、モデル コンテキスト プロトコル (MCP) を使用して Express.js アプリの機能を公開し、GitHub Copilot にツールとして追加し、Copilot Chat エージェント モードで自然言語を使用してアプリと対話する方法について説明します。

Azure App Service でホストされている Todos MCP サーバーを呼び出す GitHub Copilot を示すスクリーンショット。

Web アプリケーションにショッピング、ホテル予約、データ管理などの便利な機能が既にある場合は、これらの機能を次の目的で簡単に利用できます。

MCP サーバーを Web アプリに追加することで、エージェントがユーザー プロンプトに応答したときにアプリの機能を理解して使用できるようになります。 つまり、アプリでできることは何でも、エージェントでも実行できます。

  • MCP サーバーを Web アプリに追加します。
  • GitHub Copilot Chat エージェント モードで 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. ルート/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 チャット エージェント モードで MCP サーバーを追加する方法を示すスクリーンショット。

  5. [ MCP サーバーの追加] を選択します

  6. HTTP (HTTP または Server-Sent イベント) を選択します。

  7. [ サーバー URL の入力] に「 http://localhost:3000/api/mcp」と入力します。

  8. [ サーバー ID の入力] に、 todos-mcp または任意の名前を入力します。

  9. [ ワークスペースの設定] を選択します

  10. 新しい Copilot チャット ウィンドウで、「やることリストを表示する」のように入力します。

  11. 既定では、MCP サーバーを呼び出すと、GitHub Copilot にセキュリティ確認が表示されます。 続行を選択します。

    GitHub Copilot Chat での MCP 呼び出しからの既定のセキュリティ メッセージを示すスクリーンショット。

    MCP ツールの呼び出しが成功したことを示す応答が表示されます。

    GitHub Copilot チャット ウィンドウの MCP ツール呼び出しからの応答を示すスクリーンショット。

MCP サーバーを App Service にデプロイする

  1. codespace ターミナルに戻り、変更をコミットして変更をデプロイするか (GitHub Actions メソッド)、または azd up (Azure Developer 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 App Service へのセキュリティで保護されたモデル コンテキスト プロトコルの呼び出し に関するページを参照してください。
  • 入力の検証とサニタイズ: このチュートリアルのコード例では、入力の検証に zod を使用し、受信データが予想されるスキーマと一致することを確認します。 追加のセキュリティについては、次の点を考慮してください。
    • 特にデータベース クエリまたは出力で使用されるフィールドについては、処理前にすべてのユーザー入力を検証してサニタイズします。
    • API がブラウザーによって使用されている場合にクロスサイト スクリプティング (XSS) を防ぐために、応答で出力をエスケープする。
    • モデルに厳密なスキーマと既定値を適用して、予期しないデータを回避する。
  • HTTPS: このサンプルは、既定で HTTPS を適用し、転送中のデータを暗号化するための無料の TLS/SSL 証明書を提供する Azure App Service に依存しています。
  • 最小限の特権の原則: ユース ケースに必要なツールとデータのみを公開します。 必要な場合を除き、機密性の高い操作を公開しないでください。
  • レート制限と調整: API Management またはカスタム ミドルウェアを使用して、不正使用やサービス拒否攻撃を防ぎます。
  • ログ記録と監視: 監査と異常検出のための MCP エンドポイントのログ アクセスと使用状況。 疑わしいアクティビティを監視します。
  • CORS 構成: MCP サーバーがブラウザーからアクセスされる場合は、クロスオリジン要求を信頼されたドメインに制限します。 詳細については、「 CORS を有効にする」を参照してください。
  • 定期的な更新: 既知の脆弱性を軽減するために、依存関係を最新の状態に保ちます。

その他のリソース

AI を Azure App Service アプリケーションに統合する