FastifyとMCP v2.0 — ステートレスなプロキシゲートウェイの構築

Maru

@maru

Fastify와 MCP v2.0 — 무상태 프록시 게이트웨이 구축하기

FastifyとMCP v2.0 — ステートレスなプロキシゲートウェイの構築

AIエージェントが利用するツールの安全な管理は、企業向けAIインフラにおける重要な課題です。Model Context Protocol (MCP) v2.0仕様が標準的なHTTPベースのステートレスアーキテクチャへと移行したことで、複数のエージェントツールを中央で仲介・保護するプロキシゲートウェイの重要性が増しています。完璧なプラグインのカプセル化と優れたルーティング性能を提供するFastify v5.10は、このステートレスなプロキシレイヤーを構築するのに最適なツールです。本稿では、Fastifyと最新のMCP v2.0スタックを組み合わせ、本番環境に即座に導入可能なセキュアなAIプロキシ設計パターンを紹介します。

ステートレスなMCP v2.0パラダイムとプロキシの必要性

Model Context Protocol (MCP) v2.0の最大の変更点は、セッションを完全に取り払い、完全にステートレスなアーキテクチャへと移行したことです。従来のv1.0仕様では、クライアントとサーバーが接続状態を維持する必要がありました。このため、大規模なトラフィック分散環境において特定のサーバーにリクエストが集中するスティッキーセッションの設定を余儀なくされ、結果としてインフラの柔軟なオートスケーリングを阻むボトルネックとなっていました。

一方、最新のMCP v2.0では、接続初期化のためのハンドシェイクプロセスや複雑なセッション識別子ヘッダーが完全に削除されました。現在、すべてのツール呼び出しは独立した単一のHTTPリクエストとして処理されます。ツールサーバーを一般的なWeb APIサーバーのように扱えるようになったため、標準的なロードバランサーの背後に配置して水平方向に自由に拡張する道が開かれました。

しかし、こうしたステートレスな移行によってすべての問題が解決するわけではありません。ツールサーバーの数が増えるにつれ、統合認証、レート制限、異常動作の制御といった共通のビジネスロジックを個々のサーバーごとに何度も重複して実装するのは非効率です。セキュリティ上、外部インターネット網にリモートツールのエンドポイントをそのまま露出させることも危険です。

この点で、Fastifyベースのステートレスなプロキシゲートウェイが強力な解決策となります。高性能な非巡回グラフルーティングをサポートするFastifyを前面に配置すれば、バックエンドの実際のツールサーバーを内部ネットワーク内に安全に隠蔽しつつ、単一の入り口からすべてのトラフィックとセキュリティポリシーを一元管理できます。

公式Fastifyアダプターを活用したルートおよびプラグイン構成

公式ヘルパーパッケージである @modelcontextprotocol/fastifyを導入すると、MCP v2.0のHTTPベースのステートレスルートを非常に簡潔にマウントし制御できます。特にFastifyが誇る有向非巡回グラフ(DAG)アーキテクチャは、数多くのエージェントツールをスコープ単位で完全に隔離・隠蔽する上で、卓越した構造的利点をもたらします。

公式ガイドを満たしつつ、ツールの名前空間の衝突を完全に防止するための推奨プロジェクトファイル構造は以下の通りです。

text
mcp-gateway/
├── src/
│   ├── plugins/
│   │   └── mcp.ts       # McpServer 인스턴스 전역 등록 플러그인
│   ├── routes/
│   │   └── tools.ts     # 도구 핸들러 마운트 및 라우팅 구성
│   └── app.ts           # createMcpFastifyApp 기반 게이트웨이 진입점
├── package.json
└── tsconfig.json

fastify-pluginを利用して上位スコープにグローバル共有リソースである McpServerを宣言し、最新のStandard Schema規格に従う zodで入力値を強制する実際の実装コードです。

typescript
import fp from 'fastify-plugin';
import { FastifyPluginAsync } from 'fastify';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';

declare module 'fastify' {
  interface FastifyInstance {
    mcp: McpServer;
  }
}

export const mcpPlugin: FastifyPluginAsync = fp(async (fastify) => {
  // MCP v2.0 명세를 준수하는 서버 인스턴스 초기화
  const mcpServer = new McpServer({ 
    name: 'secure-agent-gateway', 
    version: '2.0.0' 
  });

  // registerTool API 및 Standard Schema 스펙 기반 Zod 유효성 검증
  mcpServer.registerTool(
    'fetch_db_record',
    {
      description: '데이터베이스에서 지정된 사용자 ID의 레코드를 안전하게 조회합니다.',
      inputSchema: z.object({
        userId: z.string().uuid().describe('조회하려는 사용자의 UUID'),
      }),
    },
    async ({ userId }) => {
      // 비즈니스 로직 및 데이터 조회 영역
      return {
        content: [
          {
            type: 'text',
            text: `ID가 ${userId}인 사용자 레코드를 성공적으로 조회했습니다.`,
          },
        ],
      };
    }
  );

  // Fastify DAG 구조 내에서 하위 라우터들이 활용할 수 있게 데코레이터 등록
  fastify.decorate('mcp', mcpServer);
});

このように構成すれば、@modelcontextprotocol/fastify パッケージに内蔵されているDNSリバインディング保護とホストヘッダー検証レイヤーが動作し、ローカルサーバーインフラを外部からの侵入から保護します。エージェントツールは、グローバルプラグインが提供する認証情報やデータベースコネクションに常時安全にアクセスしつつ、個別のルーターコンテキスト内部へと完全にカプセル化されるため、密結合の問題やセキュリティ上の競合の可能性を完全に排除できます。

AES-256-GCMによる多者間往復リクエスト状態の保護

MCP v2.0仕様において、クライアントに追加の入力を求める多者間往復リクエストが発生する場合、サーバーは中間トランザクションデータを外部クライアントへ一時的に返却する必要があります。この際、ステートレスアーキテクチャを維持するために中間状態データを暗号化して伝達する requestState パラメーターを活用します。

単純な署名方式であるHMAC-SHA256はデータの改ざんを検知できますが、平文をそのまま露出させるという致命的な欠点があります。内部データベース識別子やユーザー権限情報が平文で露出すると、悪意あるクライアントがこれらを悪用し、他のツールの実行を誘導する「代理人混乱(Confused Deputy)」攻撃に対して脆弱になります。したがって、機密性と完全性を同時に保証する認証付き暗号化方式であるAES-256-GCMアルゴリズムを使用して、内部状態情報を完全に秘匿する必要があります。

Node.jsの標準モジュールである crypto モジュールを活用すれば、外部ライブラリへの依存なしに安全な状態暗号化を実装できます。ステートレスゲートウェイ環境で動作するように構成したTypeScriptのヘルパーコードは以下の通りです。

typescript
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.STATE_ENCRYPTION_KEY || '', 'hex'); // 32바이트 대칭키

export function encryptState(plainText: string): string {
  const iv = randomBytes(12);
  const cipher = createCipheriv(ALGORITHM, KEY, iv);
  const encrypted = Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  
  return Buffer.concat([iv, tag, encrypted]).toString('base64url');
}

export function decryptState(token: string): string {
  const buffer = Buffer.from(token, 'base64url');
  const iv = buffer.subarray(0, 12);
  const tag = buffer.subarray(12, 28);
  const encrypted = buffer.subarray(28);
  
  const decipher = createDecipheriv(ALGORITHM, KEY, iv);
  decipher.setAuthTag(tag);
  return decipher.update(encrypted) + decipher.final('utf8');
}

このヘルパーは初期化ベクトルと認証タグを暗号文と結合して単一の文字列として返却するため、複雑なセッションデータベースなしでもゲートウェイ間での状態転送が可能です。この方式により、パフォーマンスのボトルネックを最小化しつつ、代理人混乱攻撃や中間データの改ざんを完全に遮断できます。

OAuth 2.1規格およびRFC 9728リソースメタデータ探索の実装

エンタープライズ環境でステートレスAIプロキシを安定運用するには、厳格なセキュリティ統制と動的探索機能を組み合わせた認可モデルが不可欠です。MCP v2.0はセキュリティが大幅に強化された最新のOAuth 2.1規格を標準採用しており、セッションを維持することなく、安全かつ強力なトークン検証をサポートします。

クライアントが有効なアクセストークンなしでプロキシゲートウェイのMCPエンドポイントにアクセスした場合、Fastifyサーバーは即座にリクエストを遮断し、認証フローへ誘導する必要があります。このため、サーバーはHTTP 401ステータスコードとともに、WWW-AuthenticateヘッダーにRFC 9728仕様を遵守した保護対象リソースのメタデータのURLを載せて返却します。

クライアントは、このヘッダーに含まれるパスからメタデータを動的にパースし、信頼できる認証サーバーと有効なスコープ情報を識別してトークンを発行することになります。以下は、Fastify v5.10において無効なリクエストに対してRFC 9728規格に準拠した401認証チャレンジ応答を処理するpreHandlerフックの実装例です。

typescript
import type { FastifyPluginAsync } from 'fastify';

export const oauthSecurityPlugin: FastifyPluginAsync = async (fastify) => {
  fastify.addHook('preHandler', async (request, reply) => {
    const authHeader = request.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      // RFC 9728에 맞춰 보호된 자원 메타데이터 제공용 동적 URL 생성
      const metadataUrl = `${request.protocol}://${request.host}/.well-known/oauth-protected-resource`;

      reply
        .code(401)
        .header(
          'WWW-Authenticate',
          `Bearer error="invalid_token", error_description="Authentication required", resource_metadata="${metadataUrl}"`
        );

      return reply.send({
        error: 'unauthorized',
        error_description: 'Valid access token is required.'
      });
    }
  });
};

この方式を適用すれば、ゲートウェイ内部の認可メカニズムやID認証基盤が変更されたとしても、クライアントの静的コードを一切修正することなく、柔軟に認証レイヤーをリアルタイムに調整できるため、保守性が大幅に向上します。

Fastify v5.10 LogControllerを活用した分散トレーシングロギング

ステートレスAIインフラでは、エージェントからの1つのリクエストがバックエンド内で数多くのツール呼び出しにつながるため、マイクロサービス全体を貫く一貫した分散トレーシング体系が不可欠です。Fastify v5.10.0からは、従来の静的ロギングオプションであった disableRequestLoggingrequestIdLogLabel が正式にサポート終了となりました(それぞれFSTDEP023、FSTDEP024)。代わりに新しく導入されたオブジェクト指向ベースの LogController アーキテクチャを使用すれば、エンジンレベルでログのフィルタリングとコンテキスト注入のライフサイクルを高性能に制御できます。

このアーキテクチャを実装するために LogController クラスを継承したカスタムコントローラーを設計すると便利です。クライアントが送信したHTTPヘッダーのW3C分散トレース識別子である traceparentbaggage だけでなく、MCP v2.0仕様がJSON-RPCの _meta フィールドを通じて伝達するエージェントメタデータを動的に抽出し、Fastifyの標準リクエストロガーへ注入できます。以下は、Fastify v5.10+環境に完全に適合するスレッドセーフな動的トレースコントローラーのコードです。

typescript
import { LogController, FastifyRequest, FastifyReply } from 'fastify';

export class AgentTraceController extends LogController {
  override incomingRequest(
    request: FastifyRequest,
    reply: FastifyReply,
    metadata?: Record<string, unknown>
  ): void {
    // W3C 분산 추적 헤더 추출
    const traceparent = request.headers['traceparent'] as string | undefined;
    const baggage = request.headers['baggage'] as string | undefined;

    // MCP v2.0 JSON-RPC 메타데이터 추출
    const body = request.body as Record<string, any> | undefined;
    const mcpMeta = body?._meta;

    // Fastify 자식 로거에 실시간 분산 추적 컨텍스트 동적 바인딩
    request.log.info(
      {
        traceparent: traceparent || mcpMeta?.traceparent,
        baggage: baggage || mcpMeta?.baggage,
        ...metadata,
      },
      'MCP 요청 수신됨'
    );
  }

  override requestCompleted(
    error: Error | null,
    request: FastifyRequest,
    reply: FastifyReply,
    metadata?: Record<string, unknown>
  ): void {
    const traceparent = request.headers['traceparent'] as string | undefined;
    const logData = {
      traceparent,
      durationMs: reply.elapsedTime, // Fastify v5에서 정밀 측정을 지원하는 경과 시간 프로퍼티
      ...metadata,
    };

    if (error) {
      request.log.error({ ...logData, err: error }, 'MCP 요청 처리 실패');
    } else {
      request.log.info(logData, 'MCP 요청 완료');
    }
  }
}

このようにコントローラー層で注入した一貫性のある分散トレースデータは、OpenTelemetryやOpenInference規格と自然に統合されます。個別のエージェント実行フローが単一の滝(ウォーターフォール)のようなトレース形式で収集されるため、ステートレスゲートウェイを経由する多段階呼び出しプロセスにおいて、特定のAIツールのレスポンス遅延やツールの異常停止(サイレント失敗)問題をリアルタイムAPMツールで迅速かつ確実に捕捉可能です。

Fastifyベースの高性能MCP v2プロキシの実践的なコード実装

前述したステートレスな設計パターン、AES-256-GCMセキュリティ検証、そして新しい LogController を組み合わせることで、実稼働可能なFastifyベースのMCP v2.0プロキシゲートウェイを構築できます。この実装方式は、分散エージェント環境においてもツール実行の信頼性と一貫したモニタリングを完璧に保証します。

以下は、Fastify v5.10.0と公式の @modelcontextprotocol/server パッケージを使用して作成した、実用的なプロキシゲートウェイの核心的なバックエンドコードです。

typescript
import Fastify, { LogController, FastifyRequest, FastifyReply } from 'fastify';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
import crypto from 'node:crypto';

// 1. W3C 분산 추적 헤더 주입을 위한 Fastify v5.10 LogController 구현
class TraceLogController extends LogController {
  override incomingRequest(request: FastifyRequest, reply: FastifyReply, metadata?: Record<string, unknown>) {
    super.incomingRequest(request, reply, {
      ...metadata,
      traceparent: request.headers['traceparent'],
      baggage: request.headers['baggage'],
    });
  }
}

const app = Fastify({
  logger: true,
  logController: new TraceLogController(),
});

// 2. MCP v2.0 서버 초기화
const mcpServer = new McpServer({
  name: 'enterprise-secure-proxy',
  version: '2.0.0',
});

// 3. Standard Schema v1.0 규격을 준수하는 Zod 기반 도구 등록
mcpServer.registerTool(
  'process_payment',
  {
    description: '안전한 결제 트랜잭션을 처리합니다.',
    inputSchema: z.object({
      amount: z.number().positive(),
      currency: z.string().length(3),
      encryptedState: z.string(), // AES-256-GCM 암호화된 상태 값
    }),
  },
  async ({ amount, currency, encryptedState }) => {
    // AES-256-GCM 검증 및 복호화 메커니즘 실행
    try {
      const decrypted = decryptState(encryptedState);
      const state = JSON.parse(decrypted);
      
      return {
        content: [{
          type: 'text',
          text: `결제 승인 완료: ${amount} ${currency} (주문 ID: ${state.orderId})`,
        }],
      };
    } catch (err) {
      throw new Error('유효하지 않거나 변조된 요청 상태 값입니다.');
    }
  }
);

// 4. AES-256-GCM 복호화 헬퍼 함수
function decryptState(token: string): string {
  const secretKey = Buffer.from(process.env.STATE_SECRET_KEY || '', 'hex'); // 32바이트 키
  const [ivHex, tagHex, encrypted] = token.split(':');
  
  const decipher = crypto.createDecipheriv('aes-256-gcm', secretKey, Buffer.from(ivHex, 'hex'));
  decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
  
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

// 5. 프록시 라우트 등록 및 OAuth 2.1 인증 검증
app.post('/mcp', async (request, reply) => {
  const authHeader = request.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return reply
      .status(401)
      .header('WWW-Authenticate', 'Bearer error="invalid_token", resource_metadata="https://api.example.com/prm"')
      .send({ error: '인증 헤더가 누락되었거나 유효하지 않습니다.' });
  }

  // 이후 흐름은 mcpServer 핸들러에 바인딩하여 무상태 JSON-RPC 요청을 수행합니다.
});

この実践的な実装は、エンタープライズ環境で求められる3つの安定性基準を解決します。第一に、TraceLogController を活用して着信リクエストのW3Cトレース識別子を傍受し、非同期エージェントフロー全体を透過的にロギングします。第二に、最新の registerTool APIとZod v4を組み合わせ、入力スキーマのバリデーションをランタイム段階で強力に制御します。第三に、クライアントが伝達したステートレス往復パラメーターである暗号化状態値をAES-256-GCM方式で精密に検証・復号し、代理人混乱攻撃やデータ改ざんの試みを未然に防ぎます。

安全で拡張性の高いAIツールバックエンドの準備

Fastify v5.10とMCP v2.0のステートレスHTTPアーキテクチャは、大規模な分散環境でAIエージェントインフラを拡張するための強力な指標となります。セッション管理の負担がなくなった代わりに、プロキシレイヤーでの徹底的なセキュリティ統制と分散トレーシングがサービスの成否を分けます。安定した本番運用のためには、AES-256-GCM秘密鍵の定期的なローテーションポリシーを確立し、RFC 9728規格に準拠した動的リソースメタデータの認証フローがクライアント仕様と正しく連携していることを検証する必要があります。最後に、新しく導入された LogController ベースのトレースロギングが、多者間往復リクエスト全体の異常ログを漏れなく集計しているかを確認し、安全かつ堅牢なAIツールバックエンドを完成させてください。


参考リンク