次の方法で共有


Azure Container Apps を使用して TypeScript MCP サーバーを構築する

この記事では、Node.js と TypeScript を使用してモデル コンテキスト プロトコル (MCP) サーバーを構築する方法について説明します。 サーバーは、サーバーレス環境でツールとサービスを実行します。 この構造は、カスタム MCP サーバーを作成するための開始点として使用します。

コードにアクセスする

TypeScript リモート モデル コンテキスト プロトコル (MCP) サーバー サンプルを確認します。 Node.js と TypeScript を使用してリモート MCP サーバーを構築し、Azure Container Apps にデプロイする方法を示します。

このサンプルのしくみについては、 コードチュートリアルのセクション を参照してください。

アーキテクチャの概要

次の図は、サンプル アプリの単純なアーキテクチャを 示しています。エージェントと MCP クライアントをホストする Visual Studio Code から MCP Server へのアーキテクチャを示す図。

MCP サーバーは、Azure Container Apps (ACA) でコンテナー化されたアプリとして実行されます。 Node.js/TypeScript バックエンドを使用して、モデル コンテキスト プロトコルを使用して MCP クライアントにツールを提供します。 すべてのツールはバックエンド SQLite データベースで動作します。

費用

コストを低く抑えるために、このサンプルでは、ほとんどのリソースに基本価格レベルまたは従量課金レベルを使用します。 必要に応じてレベルを調整し、料金が発生しないように完了したらリソースを削除します。

[前提条件]

  1. Visual Studio Code - MCP サーバー開発をサポートする最新バージョン。
  2. GitHub Copilot Visual Studio Code 拡張機能
  3. GitHub Copilot チャット Visual Studio Code 拡張機能
  4. Azure Developer CLI (azd)

開発コンテナーには、この記事に必要なすべての依存関係が含まれています。 GitHub Codespaces (ブラウザー) で実行することも、Visual Studio Code を使用してローカルで実行することもできます。

この記事に従うには、次の前提条件を満たしていることを確認します。

  • Azure サブスクリプション - 無料アカウントを作成します
  • Azure アカウントのアクセス許可 – Azure アカウントには、Microsoft.Authorization/roleAssignments/write、所有者などのアクセス許可が必要です。 サブスクリプション レベルのアクセス許可がない場合は、既存のリソース グループの RBAC を 付与し、そのグループにデプロイする必要があります。
    • Azure アカウントには、サブスクリプション レベルで Microsoft.Resources/deployments/write アクセス許可も必要です。
  • GitHub アカウント

オープン開発環境

必要なすべての依存関係を含む事前構成済みの開発環境を設定するには、次の手順に従います。

GitHub Codespaces は、 インターフェイスとして Visual Studio Code for the Web を使用して GitHub によって管理される開発コンテナーを実行します。 この記事にプレインストールされている必要なツールと依存関係が付属しているため、最も簡単なセットアップには GitHub Codespaces を使用します。

Von Bedeutung

すべての GitHub アカウントでは、2 つのコア インスタンスで毎月最大 60 時間無料で Codespaces を使用できます。 詳細については、GitHub Codespaces の毎月含まれるストレージとコア時間についての情報は を参照してください。

以下の手順に従って、main GitHub リポジトリの Azure-Samples/mcp-container-ts ブランチに新しい GitHub Codespace を作成します。

  1. 次のボタンを右クリックし、[ 新しいウィンドウでリンクを開く] を選択します。 このアクションにより、開発環境とドキュメントをサイド バイ サイドで開くことができます。

    GitHub codespaces で開く で開く

  2. [ コードスペースの作成 ] ページで、確認し、[ 新しいコードスペースの作成] を選択します。

  3. Codespace が起動するまで待ちます。 これには数分かかる可能性があります。

  4. 画面の下部にあるターミナルで、Azure Developer CLI を使用して Azure にサインインします。

    azd auth login
    
  5. ターミナルからコードをコピーし、ブラウザーに貼り付けます。 手順に従って、Azure アカウントで認証します。

この開発コンテナーでは、残りのタスクを実行します。

MCP サーバーをローカルで実行するには:

  1. サンプル リポジトリの「ローカル環境のセットアップ」セクションの説明に従って 、環境を設定 します。
  2. サンプル リポジトリの 「Visual Studio Code での MCP サーバーの構成 」セクションの手順に従って、ローカル環境を使用するように MCP サーバーを構成します。
  3. 続行するには、「 エージェント モードで TODO MCP サーバー ツールを使用 する」セクションに進みます。

デプロイと実行

サンプル リポジトリには、MCP サーバーの Azure デプロイのすべてのコードと構成ファイルが含まれています。 次の手順では、サンプルの MCP サーバー Azure デプロイ プロセスについて説明します。

Azure にデプロイする

Von Bedeutung

このセクションの Azure リソースでは、完了する前にコマンドを停止した場合でも、すぐにコストがかかります。

  1. Azure リソースのプロビジョニングとソース コードのデプロイに対して、次の Azure Developer CLI コマンドを実行します。

    azd up
    
  2. プロンプトに応答するには、次の表を使用します。

    Prompt 答え
    環境名 短く、小文字にしてください。 名前またはエイリアスを追加します。 たとえば、my-mcp-server のようにします。 リソース グループ名の一部として使用されます。
    サブスクリプション リソースを作成するサブスクリプションを選択します。
    場所 (ホスティング用) 一覧から近くの場所を選択します。
    Azure OpenAI モデルの場所 一覧から近くの場所を選択します。 最初の場所と同じ場所を使用できる場合は、その場所を選択します。
  3. アプリがデプロイされるまで待ちます。 通常、デプロイの完了には 5 分から 10 分かかります。

  4. デプロイが完了したら、出力に指定された URL を使用して MCP サーバーにアクセスできます。 URL は次のようになります。

https://<env-name>.<container-id>.<region>.azurecontainerapps.io
  1. URL をクリップボードにコピーします。 次のセクションで必要になります。

Visual Studio Code で MCP サーバーを構成する

フォルダー内の ファイルに URL を追加して、ローカル VS Code 環境で MCP サーバーを構成します。

  1. mcp.json フォルダー内の.vscode ファイルを開きます。

  2. ファイル内の mcp-server-sse-remote セクションを見つけます。 次のようになります。

        "mcp-server-sse-remote": {
        "type": "sse",
        "url": "https://<container-id>.<location>.azurecontainerapps.io/sse"
    }
    
  3. 既存の url 値を、前の手順でコピーした URL に置き換えます。

  4. mcp.json ファイルを .vscode フォルダーに保存します。

エージェント モードで TODO MCP サーバー ツールを使用する

MCP サーバーを変更した後は、エージェント モードで提供されるツールを使用できます。 エージェント モードで MCP ツールを使用するには:

  1. チャット ビュー (Ctrl+Alt+I) を開き、ドロップダウンから [エージェント モード] を選択します。

  2. [ ツール ] ボタンを選択して、使用可能なツールの一覧を表示します。 必要に応じて、使用するツールを選択または選択解除します。 検索ボックスに入力して、ツールを検索できます。

  3. 次のスクリーンショットのように、チャット入力ボックスに「水曜日にマネージャーにメールを送信する必要があります」などのプロンプトを入力し、必要に応じてツールがどのように自動的に呼び出されるかを確認します。

    MCP サーバー ツールの呼び出しを示すスクリーンショット。

既定では、ツールが呼び出されたときに、ツールを実行する前にアクションを確認する必要があります。 そうしないと、ツールがコンピューター上でローカルに実行され、ファイルまたはデータを変更するアクションが実行される可能性があります。

[続行] ボタンのドロップダウン オプションを使用して、現在のセッション、ワークスペース、または今後のすべての呼び出しの特定のツールを自動的に確認します。

サンプル コードの探索

このセクションでは、MCP サーバー サンプルのキー ファイルとコード構造の概要について説明します。 コードは、いくつかの主要なコンポーネントに編成されています。

  • index.ts: Express.js HTTP サーバーとルーティングを設定する MCP サーバーのメイン エントリ ポイント。
  • server.ts: Server-Sent イベント (SSE) 接続と MCP プロトコルの処理を管理するトランスポート層。
  • tools.ts: MCP サーバーのビジネス ロジックとユーティリティ関数が含まれています。
  • types.ts: MCP サーバー全体で使用される TypeScript の型とインターフェイスを定義します。

index.ts - サーバーが HTTP 接続を開始して受け入れる方法

index.ts ファイルは、MCP サーバーのメイン エントリ ポイントです。 サーバーを初期化し、Express.js HTTP サーバーを設定し、Server-Sent イベント (SSE) エンドポイントのルーティングを定義します。

MCP サーバー インスタンスを作成する

次のコード スニペットは、コア MCP StreamableHTTPServer クラスのラッパーである Server クラスを使用して MCP サーバーを初期化します。 このクラスは、Server-Sent イベント (SSE) のトランスポート層を処理し、クライアント接続を管理します。

const server = new StreamableHTTPServer(
  new Server(
    {
      name: 'todo-http-server',
      version: '1.0.0',
    },
    {
      capabilities: {
        tools: {},
      },
    }
  )
);

概念:

  • コンポジション パターン: SSEPServer 低レベルのServer クラスをラップします
  • 機能宣言: サーバーがツールをサポートすると通知します (リソース/プロンプトはサポートされません)
  • 名前付け規則: サーバー名が MCP 識別の一部になる

Express ルートを設定する

次のコード スニペットは、SSE 接続とメッセージ処理の受信 HTTP 要求を処理するように Express.js サーバーを設定します。

router.post('/messages', async (req: Request, res: Response) => {
  await server.handlePostRequest(req, res);
});

router.get('/sse', async (req: Request, res: Response) => {
  await server.handleGetRequest(req, res);
});

概念:

  • 2 つのエンドポイント パターン: SSE 接続を確立するための GET、メッセージを送信するための POST
  • 委任パターン: Express ルートは直ちに SSEPServer に委任する

プロセス ライフサイクル管理

次のコード スニペットは、サーバーの起動や終了シグナル時の正常なシャットダウンなど、サーバーのライフサイクルを処理します。

process.on('SIGINT', async () => {
  log.error('Shutting down server...');
  await server.close();
  process.exit(0);
});

概念:

  • グレースフル シャットダウン: Ctrl + C での適切なクリーンアップ
  • 非同期クリーンアップ: サーバーの閉じる操作は非同期です
  • リソース管理: SSE 接続にとって重要

トランスポート層: server.ts

server.ts ファイルは、MCP サーバーのトランスポート層を実装します。具体的には、Server-Sent イベント (SSE) 接続を処理し、MCP プロトコル メッセージをルーティングします。

SSE クライアント接続を設定し、トランスポートを作成する

SSEPServer クラスは、MCP サーバーの Server-Sent イベント (SSE) を処理するための主要なトランスポート層です。 SSEServerTransport クラスを使用して、個々のクライアント接続を管理します。 複数のトランスポートとそのライフサイクルを管理します。

export class SSEPServer {
  server: Server;
  transport: SSEServerTransport | null = null;
  transports: Record<string, SSEServerTransport> = {};

  constructor(server: Server) {
    this.server = server;
    this.setupServerRequestHandlers();
  }
}

概念:

  • 状態管理: 現在のトランスポートとすべてのトランスポートの両方を追跡します
  • セッション マッピング: transports オブジェクトは、セッション ID をトランスポート インスタンスにマップします
  • コンストラクターの委任: 要求ハンドラーをすぐに設定する

SSE 接続の確立 (handleGetRequest)

handleGetRequestメソッドは、クライアントが /sse エンドポイントに対して GET 要求を行うときに、新しい SSE 接続を確立する役割を担います。

async handleGetRequest(req: Request, res: Response) {
  log.info(`GET ${req.originalUrl} (${req.ip})`);
  try {
    log.info("Connecting transport to server...");
    this.transport = new SSEServerTransport("/messages", res);
    TransportsCache.set(this.transport.sessionId, this.transport);

    res.on("close", () => {
      if (this.transport) {
        TransportsCache.delete(this.transport.sessionId);
      }
    });

    await this.server.connect(this.transport);
    log.success("Transport connected. Handling request...");
  } catch (error) {
    // Error handling...
  }
}

概念:

  • トランスポートの作成: 各 GET リクエストに対する新しいSSEServerTransport
  • セッション管理: キャッシュに格納されている自動生成されたセッション ID
  • イベント ハンドラー: 接続が閉じた場合のクリーンアップ
  • MCP 接続: server.connect() プロトコル接続を確立します
  • 非同期フロー: 接続のセットアップはエラー処理境界を用いて非同期で行われます

メッセージ処理 (handlePostRequest)

handlePostRequest メソッドは、クライアントから送信された MCP メッセージを処理するために受信 POST 要求を処理します。 クエリ パラメーターのセッション ID を使用して、適切なトランスポート インスタンスを検索します。

async handlePostRequest(req: Request, res: Response) {
  log.info(`POST ${req.originalUrl} (${req.ip}) - payload:`, req.body);

  const sessionId = req.query.sessionId as string;
  const transport = TransportsCache.get(sessionId);
  if (transport) {
    await transport.handlePostMessage(req, res, req.body);
  } else {
    log.error("Transport not initialized. Cannot handle POST request.");
    res.status(400).json(/* error response */);
  }
}

概念:

  • セッション参照: sessionId クエリ パラメーターを使用してトランスポートを検索します
  • セッション検証: 最初に SSE 接続を検証します。
  • メッセージ委任: トランスポートが実際のメッセージ処理を処理する
  • エラー応答: セッションが見つからない場合の適切な HTTP エラー コード

MCP プロトコル ハンドラーのセットアップ (setupServerRequestHandlers)

setupServerRequestHandlers メソッドは、MCP プロトコル要求に対して次のハンドラーを登録します。

  • 使用可能な TODO ツールの一覧を返す ListToolsRequestSchema のハンドラー。
  • 指定された引数を使用して要求されたツールを検索して実行する CallToolRequestSchema のハンドラー。

このメソッドでは 、Zod スキーマを 使用して、予想される要求と応答の形式を定義します。

private setupServerRequestHandlers() {
  this.server.setRequestHandler(ListToolsRequestSchema, async (_request) => {
    return {
      tools: TodoTools,
    };
  });
  
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    
    const tool = TodoTools.find((tool) => tool.name === name);
    if (!tool) {
      return this.createJSONErrorResponse(`Tool "${name}" not found.`);
    }
    
    const response = await tool.execute(args as any);
    return { content: [{ type: "text", text: response }] };
  });
}

概念:

  • Schema-Based ルーティング: タイプ セーフな要求処理に Zod スキーマを使用します
  • ツール検出: ListToolsRequestSchema は静的な TodoTools 配列を返します
  • ツールの実行: ツールを検索して実行CallToolRequestSchema
  • エラー処理: 不明なツールの正常な処理
  • 応答形式: MCP 準拠の応答構造
  • 型の安全性: TypeScript 型は正しい引数の受け渡しを保証します

ビジネス ロジック: tools.ts

tools.ts ファイルは、MCP クライアントで使用できる実際の機能を定義します。

  • ツール メタデータ (名前、説明、スキーマ)
  • 入力検証スキーマ
  • ツールの実行ロジック
  • データベース レイヤーとの統合

この MCP サーバーでは、次の 4 つの TODO 管理ツールが定義されています。

  • add_todo: 新しい TODO 項目を作成します。
  • complete_todo: TODO 項目を完了としてマークします。
  • delete_todo: TODO アイテムを削除します。
  • list_todos: すべての TODO 項目を一覧表示します。
  • update_todo_text: 既存の TODO 項目のテキストを更新します。

ツール定義パターン

ツールはオブジェクトの配列として定義され、それぞれが特定の TODO 操作を表します。 次のコード スニペットでは、 addTodo ツールが定義されています。

{
  name: "addTodo",
  description: "Add a new TODO item to the list...",
  inputSchema: {
    type: "object",
    properties: {
      text: { type: "string" },
    },
    required: ["text"],
  },
  outputSchema: { type: "string" },
  async execute({ text }: { text: string }) {
    const info = await addTodo(text);
    return `Added TODO: ${text} (id: ${info.lastInsertRowid})`;
  },
}

各ツール定義には次の機能があります。

  • name: ツール専用の識別子
  • description: ツールの目的の簡単な説明
  • inputSchema: 予期される入力形式を定義する Zod スキーマ
  • outputSchema: 予期される出力形式を定義する Zod スキーマ
  • execute: ツールのロジックを実装する関数

これらのツール定義は server.ts にインポートされ、 ListToolsRequestSchema ハンドラーを介して公開されます。

概念:

  • モジュール式ツール設計:各ツールは自己完結型オブジェクトです
  • JSON スキーマの検証: inputSchema は想定されるパラメーターを定義します
  • Type Safety: TypeScript 型がスキーマ定義と一致する
  • 非同期実行: すべてのツール実行が非同期です
  • データベース統合: インポートされたデータベース関数を呼び出す
  • Human-Readable 応答: 生データではなく、書式設定された文字列を返します

ツール配列のエクスポート

ツールは静的配列としてエクスポートされるため、サーバーでのインポートと使用が簡単になります。 各ツールは、メタデータと実行ロジックを持つオブジェクトです。 この構造により、MCP サーバーはクライアント要求に基づいてツールを動的に検出して実行できます。

export const TodoTools = [
  { /* addTodo */ },
  { /* listTodos */ },
  { /* completeTodo */ },
  { /* deleteTodo */ },
  { /* updateTodoText */ },
];

概念:

  • 静的登録: モジュールの読み込み時に定義されたツール
  • 配列構造: 単純な配列を使用すると、ツールを簡単に反復処理できます
  • インポート/エクスポート: サーバー ロジックからのクリーンな分離

ツールの実行エラー処理

各ツールの execute 関数は、エラーをスムーズに処理し、例外をスローする代わりにクリア メッセージを返します。 この方法により、MCP サーバーはシームレスなユーザー エクスペリエンスを提供します。

ツールは、さまざまなエラー シナリオを処理します。

async execute({ id }: { id: number }) {
  const info = await completeTodo(id);
  if (info.changes === 0) {
    return `TODO with id ${id} not found.`;
  }
  return `Marked TODO ${id} as completed.`;
}

概念:

  • データベース応答チェック: info.changes を使用してエラーを検出する
  • グレースフル デグラデーション: 説明的なエラーメッセージを返すか、例外をスローする
  • User-Friendly エラー: AI 解釈に適したメッセージ

データ レイヤー: db.ts

db.ts ファイルは、SQLite データベース接続を管理し、TODO アプリの CRUD 操作を処理します。 同期データベース アクセスには、 better-sqlite3 ライブラリを使用します。

データベースの初期化

データベースは SQLite に接続し、テーブルが存在しない場合は作成することによって初期化されます。 次のコード スニペットは、初期化プロセスを示しています。

const db = new Database(":memory:", {
  verbose: log.info,
});

try {
  db.pragma("journal_mode = WAL");
  db.prepare(
    `CREATE TABLE IF NOT EXISTS ${DB_NAME} (
     id INTEGER PRIMARY KEY AUTOINCREMENT,
     text TEXT NOT NULL,
     completed INTEGER NOT NULL DEFAULT 0
   )`
  ).run();
  log.success(`Database "${DB_NAME}" initialized.`);
} catch (error) {
  log.error(`Error initializing database "${DB_NAME}":`, { error });
}

概念:

  • In-Memory データベース: :memory: は、再起動時にデータが失われることを意味します (デモ/テストのみ)
  • WAL モード: パフォーマンスを向上させるためにログ記録を Write-Ahead する
  • スキーマ定義: 自動インクリメント ID を持つ単純な TODO テーブル
  • エラー処理: 初期化エラーの円滑な処理
  • ログ統合: デバッグのためにデータベース操作がログに記録される

CRUD 操作パターン

db.ts ファイルには、TODO 項目を管理するための 4 つの主要な CRUD 操作が用意されています。

作成操作:

export async function addTodo(text: string) {
  log.info(`Adding TODO: ${text}`);
  const stmt = db.prepare(`INSERT INTO todos (text, completed) VALUES (?, 0)`);
  return stmt.run(text);
}

読み取り操作:

export async function listTodos() {
  log.info("Listing all TODOs...");
  const todos = db.prepare(`SELECT id, text, completed FROM todos`).all() as Array<{
    id: number;
    text: string;
    completed: number;
  }>;
  return todos.map(todo => ({
    ...todo,
    completed: Boolean(todo.completed),
  }));
}

更新操作:

export async function completeTodo(id: number) {
  log.info(`Completing TODO with ID: ${id}`);
  const stmt = db.prepare(`UPDATE todos SET completed = 1 WHERE id = ?`);
  return stmt.run(id);
}

削除操作:

export async function deleteTodo(id: number) {
  log.info(`Deleting TODO with ID: ${id}`);
  const row = db.prepare(`SELECT text FROM todos WHERE id = ?`).get(id) as
    | { text: string }
    | undefined;
  if (!row) {
    log.error(`TODO with ID ${id} not found`);
    return null;
  }
  db.prepare(`DELETE FROM todos WHERE id = ?`).run(id);
  log.success(`TODO with ID ${id} deleted`);
  return row;
}

概念:

  • 準備されたステートメント: SQL インジェクションに対する保護
  • 型キャスト: TypeScript におけるクエリ結果の型を明示的に指定すること
  • データ変換: SQLite 整数をブール値に変換する
  • アトミック操作: 各関数は単一のデータベース トランザクションです
  • 戻り値の整合性: 関数は操作メタデータを返します
  • 防御プログラミング: 削除前のチェック パターン

スキーマの設計

データベース スキーマは、単純な SQL ステートメントを使用して db.ts ファイルで定義されます。 todos テーブルには、次の 3 つのフィールドがあります。

CREATE TABLE todos (
  id INTEGER PRIMARY KEY AUTOINCREMENT,  -- Unique identifier
  text TEXT NOT NULL,                    -- TODO description  
  completed INTEGER NOT NULL DEFAULT 0   -- Boolean as integer
);

ヘルパー ユーティリティ: helpers/ ディレクトリ

helpers/ ディレクトリには、サーバーのユーティリティ関数とクラスが用意されています。

デバッグと監視のための構造化ログ: helpers/logs.ts

helpers/logs.ts ファイルには、MCP サーバー用の構造化ログ ユーティリティが用意されています。 ログ記録にはdebugライブラリを使用し、コンソールでの色分けされた出力にはchalkを使用します。

export const logger = (namespace: string) => {
  const dbg = debug('mcp:' + namespace);
  const log = (colorize: ChalkInstance, ...args: any[]) => {
    const timestamp = new Date().toISOString();
    const formattedArgs = [timestamp, ...args].map((arg) => {
      if (typeof arg === 'object') {
        return JSON.stringify(arg, null, 2);
      }
      return arg;
    });
    dbg(colorize(formattedArgs.join(' ')));
  };

  return {
    info(...args: any[]) { log(chalk.cyan, ...args); },
    success(...args: any[]) { log(chalk.green, ...args); },
    warn(...args: any[]) { log(chalk.yellow, ...args); },
    error(...args: any[]) { log(chalk.red, ...args); },
  };
};

SSE トランスポートのセッション管理: helpers/cache.ts

helpers/cache.ts ファイルでは、Mapを使用してセッション ID で SSE トランスポートを格納します。 この方法により、サーバーはアクティブな接続をすばやく見つけて管理できます。

import type { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse";

export const TransportsCache = new Map<string, SSEServerTransport>();

TransportsCacheは単純なメモリ内キャッシュです。 運用環境では、Redis やセッション管理用のデータベースなどのより堅牢なソリューションを使用することを検討してください。

実行フローの概要

次の図は、ツールの実行とデータベース操作を含む、クライアントから MCP サーバーへの完全な要求の過程を示しています。

クライアントから MCP サーバーへの完全な要求体験と戻る手順を示す図。

GitHub Codespaces をクリーンアップする

GitHub Codespaces 環境を削除して、コアごとの無料時間を最大化します。

Von Bedeutung

GitHub アカウントの無料ストレージとコア時間の詳細については、 GitHub Codespaces の月単位の含まれるストレージとコア時間を参照してください。

  1. GitHub Codespaces ダッシュボードにサインインします。

  2. Azure-Samples//mcp-container-ts GitHub リポジトリから作成されたアクティブな Codespace を見つけます。

  3. codespace のコンテキスト メニューを開き、[削除] を選択します。

ヘルプを取得する

リポジトリの問題に問題を記録します。