次の方法で共有


チュートリアル: Node.js MCP サーバーを Azure Container Apps にデプロイする

このチュートリアルでは、Express と MCP TypeScript SDK を使用してタスク管理ツールを公開するモデル コンテキスト プロトコル (MCP) サーバーを構築します。 サーバーを Azure Container Apps にデプロイし、VS Code の GitHub Copilot Chat からサーバーに接続します。

このチュートリアルでは、次の操作を行います。

  • MCP ツールを公開する Express アプリを作成する
  • GitHub Copilot を使用して MCP サーバーをローカルでテストする
  • アプリをコンテナー化して Azure Container Apps にデプロイする
  • デプロイされた MCP サーバーに GitHub Copilot を接続する

[前提条件]

アプリの雛形を作成する

このセクションでは、Express と MCP TypeScript SDK を使用して新しい Node.js プロジェクトを作成します。

  1. プロジェクト ディレクトリを作成し、初期化します。

    mkdir tasks-mcp-server && cd tasks-mcp-server
    npm init -y
    
  2. 依存関係をインストールします。

    npm install @modelcontextprotocol/sdk express zod
    npm install -D typescript @types/node @types/express tsx
    
  3. tsconfig.jsonを作成します。

    {
        "compilerOptions": {
            "target": "ES2022",
            "module": "Node16",
            "moduleResolution": "Node16",
            "outDir": "./dist",
            "rootDir": "./src",
            "strict": true,
            "esModuleInterop": true,
            "declaration": true
        },
        "include": ["src/**/*"]
    }
    

    この構成は、Node.js モジュール解決を使用して ES2022 をターゲットにし、コンパイル済みファイルを dist/に出力し、厳密な型チェックを有効にします。

  4. package.jsonを更新して ES モジュールを有効にし、ビルド スクリプトと開始スクリプトを追加します。 typeフィールドと scripts フィールドを追加または置換します。

    {
        "type": "module",
        "scripts": {
            "build": "tsc",
            "start": "node dist/index.js",
            "dev": "tsx watch src/index.ts"
        }
    }
    

    Important

    "type": "module" を設定します。 MCP サーバー コードでは、最上位レベルの awaitが使用されます。これは ES モジュールでのみサポートされています。

  5. インメモリ データ ストアの 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 ツールを定義する

次に、タスク ストアを AI クライアントに公開するツール登録を使用して MCP サーバーを定義します。

  1. 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 スキーマを使用して、説明付きの入力パラメーターを定義します。
    • Container Apps 正常性プローブには、別の /health エンドポイントが必要です。

MCP サーバーをローカルでテストする

Azure にデプロイする前に、MCP サーバーがローカルで実行され、GitHub Copilot から接続されて動作することを確認します。

  1. 開発サーバーを起動します。

    npx tsx src/index.ts
    
  2. VS Code を開き、 Copilot チャットを開き、 エージェント モードを選択します。

  3. [ ツール ] ボタンを選択し、[ その他のツールの追加] を選択します。..>MCP サーバーを追加します

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

  5. サーバーの URL を入力します。 http://localhost:3000/mcp

    ローカル開発サーバーの既定値はポート 3000 です。 コンテナー化されると、Dockerfile によって、Container Apps ターゲット ポートに一致するように PORT 環境変数が 8080 に設定されます。

  6. サーバー ID を入力します。 tasks-mcp

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

  8. "すべてのタスクを表示する" というプロンプトでテストする

  9. Copilot がツールの呼び出しの確認を要求した場合は、[ 続行] を 選択します。

Copilot がメモリ内ストアからタスクの一覧を返すことがわかります。

ヒント

「PR を確認するタスクを作成する」、「タスク 1 を完了としてマークする」、「タスク 2 を削除する」などの他のプロンプトを試してください。

アプリケーションのコンテナー格納

Azure にデプロイする前にローカルでテストできるように、アプリケーションを Docker コンテナーとしてパッケージ化します。

  1. 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環境変数は、Container Apps ターゲット ポートに一致するように 8080 に設定されます。

  2. ローカルで確認する:

    docker build -t tasks-mcp-server .
    docker run -p 8080:8080 tasks-mcp-server
    

    確認: curl http://localhost:8080/health

Azure Container Apps へのデプロイ

アプリケーションをコンテナー化したら、Azure CLI を使用して Azure Container Apps にデプロイします。 az containerapp up コマンドはクラウドにコンテナー イメージをビルドするため、この手順ではマシン上に Docker は必要ありません。

  1. 環境変数を設定します。

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-node"
    
  2. リソース グループを作成します。

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
  3. Container Apps 環境を作成します。

    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  4. コンテナー アプリをデプロイします。

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  5. GitHub Copilot 要求を許可するように CORS を構成します。

    az containerapp ingress cors enable \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --allowed-origins "*" \
        --allowed-methods "GET,POST,DELETE,OPTIONS" \
        --allowed-headers "*"
    

    運用環境では、ワイルドカード * オリジンを特定の信頼されたオリジンに置き換えます。 ガイダンスについては、 Container Apps 上の MCP サーバーのセキュリティ保護に関 するトピックを参照してください。

  6. デプロイを検証します。

    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 で実行されたので、デプロイされたエンドポイントに GitHub Copilot を接続するように VS Code を構成します。

  1. プロジェクトで、 .vscode/mcp.jsonを作成または更新します。

    {
        "servers": {
            "tasks-mcp-server": {
                "type": "http",
                "url": "https://<your-app-fqdn>/mcp"
            }
        }
    }
    

    <your-app-fqdn>をデプロイ出力の FQDN に置き換えます。

  2. VS Code で、エージェント モードで Copilot チャットを開きます。

  3. サーバーが自動的に表示されない場合は、[ ツール ] ボタンを選択し、 tasks-mcp-server が一覧表示されていることを確認します。 必要に応じて [開始] を選択します

  4. "List all my tasks" のようなプロンプトでテストし、デプロイされた MCP サーバーが応答することを確認します。

対話型使用のスケーリングを構成する

既定では、Azure Container Apps は 0 個のレプリカにスケーリングできます。 Copilot などの対話型クライアントにサービスを提供する MCP サーバーの場合、コールド スタートは顕著な遅延を引き起こします。 少なくとも 1 つのインスタンスを実行し続けるために、レプリカの最小数を設定します。

az containerapp update \
    --name $APP_NAME \
    --resource-group $RESOURCE_GROUP \
    --min-replicas 1

セキュリティに関する考慮事項

このチュートリアルでは、簡単にするために認証されていない MCP サーバーを使用します。 運用環境で MCP サーバーを実行する前に、次の推奨事項を確認してください。 大規模言語モデル (LLM) を利用するエージェントが MCP サーバーを呼び出す場合は、 迅速なインジェクション 攻撃に注意してください。

  • 認証と承認: Microsoft Entra ID を使用して MCP サーバーをセキュリティで保護します。 Container Apps での MCP サーバーのセキュリティ保護に関する情報を参照してください。
  • 入力検証: Zod スキーマは型の安全性を提供しますが、ツール パラメーターのビジネス ルール検証を追加します。 要求レベルの検証には 、zod-express-middleware などのライブラリを検討してください。
  • HTTPS: Azure Container Apps では、自動 TLS 証明書を使用して既定で HTTPS が適用されます。
  • 最小権限: ユース ケースで必要なツールのみを公開します。 確認なしで破壊的な操作を実行するツールは避けてください。
  • CORS: 許可された配信元を運用環境の信頼されたドメインに制限します。
  • ログ記録と監視: MCP ツールの呼び出しを監査用にログに記録します。 Azure Monitor と Log Analytics を使用します。

リソースをクリーンアップする

このアプリケーションを引き続き使用する予定がない場合は、リソース グループを削除して、このチュートリアルで作成したすべてのリソースを削除します。

az group delete --resource-group $RESOURCE_GROUP --yes --no-wait

次のステップ