このチュートリアルでは、Express と MCP TypeScript SDK を使用してタスク管理ツールを公開するモデル コンテキスト プロトコル (MCP) サーバーを構築します。 サーバーを Azure Container Apps にデプロイし、VS Code の GitHub Copilot Chat からサーバーに接続します。
このチュートリアルでは、次の操作を行います。
- MCP ツールを公開する Express アプリを作成する
- GitHub Copilot を使用して MCP サーバーをローカルでテストする
- アプリをコンテナー化して Azure Container Apps にデプロイする
- デプロイされた MCP サーバーに GitHub Copilot を接続する
[前提条件]
- アクティブなサブスクリプションを持つ Azure アカウント。 無料で作成できます。
- Azure CLI バージョン 2.62.0 以降。
- Node.js 20 LTS 以降。
- GitHub Copilot 拡張機能を含む Visual Studio Code。
- Docker Desktop (省略可能 - コンテナーをローカルでテストするためにのみ必要)。
アプリの雛形を作成する
このセクションでは、Express と MCP TypeScript SDK を使用して新しい Node.js プロジェクトを作成します。
プロジェクト ディレクトリを作成し、初期化します。
mkdir tasks-mcp-server && cd tasks-mcp-server npm init -y依存関係をインストールします。
npm install @modelcontextprotocol/sdk express zod npm install -D typescript @types/node @types/express tsxtsconfig.jsonを作成します。{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "declaration": true }, "include": ["src/**/*"] }この構成は、Node.js モジュール解決を使用して ES2022 をターゲットにし、コンパイル済みファイルを
dist/に出力し、厳密な型チェックを有効にします。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 モジュールでのみサポートされています。インメモリ データ ストアの
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 サーバーを定義します。
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`); });重要なポイント:
-
McpServerTypeScript SDK から、ツールの登録を使用して MCP サーバーを定義します。 -
StreamableHTTPServerTransportは、MCP ストリーミング可能な HTTP プロトコルを処理します。sessionIdGenerator: undefined設定すると、サーバーがステートレス モードで実行されます。 - ツールでは Zod スキーマを使用して、説明付きの入力パラメーターを定義します。
- Container Apps 正常性プローブには、別の
/healthエンドポイントが必要です。
-
MCP サーバーをローカルでテストする
Azure にデプロイする前に、MCP サーバーがローカルで実行され、GitHub Copilot から接続されて動作することを確認します。
開発サーバーを起動します。
npx tsx src/index.tsVS Code を開き、 Copilot チャットを開き、 エージェント モードを選択します。
[ ツール ] ボタンを選択し、[ その他のツールの追加] を選択します。..>MCP サーバーを追加します。
HTTP (HTTP または Server-Sent イベント) を選択します。
サーバーの URL を入力します。
http://localhost:3000/mcp注
ローカル開発サーバーの既定値はポート 3000 です。 コンテナー化されると、Dockerfile によって、Container Apps ターゲット ポートに一致するように
PORT環境変数が 8080 に設定されます。サーバー ID を入力します。
tasks-mcp[ ワークスペースの設定] を選択します。
"すべてのタスクを表示する" というプロンプトでテストする
Copilot がツールの呼び出しの確認を要求した場合は、[ 続行] を 選択します。
Copilot がメモリ内ストアからタスクの一覧を返すことがわかります。
ヒント
「PR を確認するタスクを作成する」、「タスク 1 を完了としてマークする」、「タスク 2 を削除する」などの他のプロンプトを試してください。
アプリケーションのコンテナー格納
Azure にデプロイする前にローカルでテストできるように、アプリケーションを Docker コンテナーとしてパッケージ化します。
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 に設定されます。ローカルで確認する:
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 は必要ありません。
環境変数を設定します。
RESOURCE_GROUP="mcp-tutorial-rg" LOCATION="eastus" ENVIRONMENT_NAME="mcp-env" APP_NAME="tasks-mcp-server-node"リソース グループを作成します。
az group create --name $RESOURCE_GROUP --location $LOCATIONContainer Apps 環境を作成します。
az containerapp env create \ --name $ENVIRONMENT_NAME \ --resource-group $RESOURCE_GROUP \ --location $LOCATIONコンテナー アプリをデプロイします。
az containerapp up \ --name $APP_NAME \ --resource-group $RESOURCE_GROUP \ --environment $ENVIRONMENT_NAME \ --source . \ --ingress external \ --target-port 8080GitHub 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 サーバーのセキュリティ保護に関 するトピックを参照してください。デプロイを検証します。
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 を構成します。
プロジェクトで、
.vscode/mcp.jsonを作成または更新します。{ "servers": { "tasks-mcp-server": { "type": "http", "url": "https://<your-app-fqdn>/mcp" } } }<your-app-fqdn>をデプロイ出力の FQDN に置き換えます。VS Code で、エージェント モードで Copilot チャットを開きます。
サーバーが自動的に表示されない場合は、[ ツール ] ボタンを選択し、
tasks-mcp-serverが一覧表示されていることを確認します。 必要に応じて [開始] を選択します 。"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