VercelでModel Context Protocolサーバーを構築する

mcpvercelaiserverlessnextjstypescript
By sko X opus 4.19/19/202512 min read

Model Context Protocol(MCP)は、AIシステムを外部ツールやデータソースに接続するための基盤標準として登場し、vercel/mcp-handlerはVercelプラットフォーム向けのプロダクションレディな実装を提供しています。この包括的なガイドでは、2025年の最新アップデートとベストプラクティスを取り入れながら、概念から展開までのMCPサーバー開発を探求します。

開発者視点でMCPを理解する

MCPサーバーをAI アプリケーション用の特殊化された API サーバーとして考えてみてください。REST APIがWebクライアント用のエンドポイントを公開するように、MCPサーバーはAIモデルが使用するツール、リソース、プロンプトを公開します。主な違い:レンダリング用のJSONデータを返すのではなく、AIモデルが理解して行動できる構造化された情報を返します。

実践的な内訳は次のとおりです:

MCPサーバー = あなたのAPIサーバー AIが呼び出せる関数を書きます。これらはAPIエンドポイントと同じですが、/api/usersのようなHTTPルートの代わりに、get_user_dataprocess_paymentのようなツールを定義します。

ツール = APIエンドポイント 各ツールは基本的に、入力検証(Zodスキーマを使用)と戻り値を持つ関数です。ClaudeやGeminiがアクションを実行する必要があるとき、フロントエンドがAPIを呼び出すように、あなたのツールを呼び出します。

リソース = 静的データエンドポイント これらをコンテキスト情報を返すGETエンドポイントとして考えてください。リソースは静的(設定など)または動的(ユーザープロファイルなど)である可能性があります。それらはAIが参照できる読み取り専用のデータソースです。

プロンプト = リクエストテンプレート これらは一般的なAI操作用の事前設定されたテンプレートです - GraphQLフラグメントや保存されたPostmanリクエストに似ています。AIモデルがサーバーとやり取りする方法を標準化するのに役立ちます。

vercel/mcp-handlerパッケージは、WebSocket接続、メッセージルーティング、エラー処理など、すべてのプロトコルの複雑さを処理します - そのため、ビジネスロジックの記述に集中できます。MCPのためのExpress.jsのようなものです:ハンドラーを定義し、フレームワークがインフラストラクチャを管理します。

// これだけ理解すればよい:
// 1. AIがあなたのツールを呼び出す
// 2. リクエストを処理する
// 3. レスポンスを返す
// それ以外はすべてフレームワークが処理

包括的な例でvercel/mcp-handlerをセットアップ

インストールは、MCPサーバーの基盤を形成するコア依存関係から始まります:

npm install mcp-handler @modelcontextprotocol/sdk zod@^3

基本的なサーバー構造は、ハンドラーパターンの優雅さを示しています。このNext.js実装は、必須コンポーネントを示すシンプルなサイコロを振るツールを作成します:

// app/api/[transport]/route.ts
import { createMcpHandler } from "mcp-handler";
import { z } from "zod";

const handler = createMcpHandler(
  (server) => {
    server.tool(
      "roll_dice",
      "N面のサイコロを振る",
      { sides: z.number().int().min(2).max(100) },
      async ({ sides }) => {
        const value = 1 + Math.floor(Math.random() * sides);
        return {
          content: [{ type: "text", text: `🎲 ${value}が出ました!` }],
        };
      }
    );
  },
  {
    capabilities: {
      tools: { roll_dice: { description: "指定した面数のサイコロを振る" } },
    },
  },
  {
    redisUrl: process.env.REDIS_URL,
    basePath: "/api",
    maxDuration: 60,
    verboseLogs: process.env.NODE_ENV === "development",
  }
);

export { handler as GET, handler as POST, handler as DELETE };

設定オブジェクトはいくつかの重要なパラメータを受け入れます。redisUrlはSSEトランスポート状態管理を有効にし、リクエスト間での接続の永続性を維持します。basePathはルート構造と一致する必要があり、MCPエンドポイントがアクセス可能な場所を決定します。maxDurationは接続タイムアウトを設定し、verboseLogsは開発デバッグを支援します。

本番環境では、vercel.json設定が不可欠になります:

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "functions": {
    "api/[transport]/route.ts": {
      "maxDuration": 800,
      "memory": 1024
    }
  },
  "env": {
    "REDIS_URL": "@redis-url",
    "MCP_API_KEY": "@mcp-api-key"
  }
}

ツール、リソース、プロンプトの実装

ツールはMCPの主要な相互作用メカニズムを表し、AIモデルがアクションを実行して情報を取得できるようにします。包括的なツール実装は、検証、エラー処理、レスポンスフォーマットを示しています:

server.tool(
  "process_data",
  "統計操作でデータセットを分析",
  {
    data: z.array(z.object({
      id: z.string().uuid(),
      value: z.number(),
      timestamp: z.string().datetime(),
      metadata: z.record(z.any()).optional(),
    })).min(1, "データ配列は空にできません"),
    operation: z.enum(["sum", "average", "max", "min", "stddev"]),
    groupBy: z.string().optional(),
  },
  async ({ data, operation, groupBy }) => {
    try {
      const values = data.map(item => item.value);
      let result: number;

      switch (operation) {
        case "sum":
          result = values.reduce((acc, val) => acc + val, 0);
          break;
        case "average":
          result = values.reduce((acc, val) => acc + val, 0) / values.length;
          break;
        case "stddev":
          const mean = values.reduce((a, b) => a + b, 0) / values.length;
          const variance = values.reduce((acc, val) =>
            acc + Math.pow(val - mean, 2), 0) / values.length;
          result = Math.sqrt(variance);
          break;
        case "max":
          result = Math.max(...values);
          break;
        case "min":
          result = Math.min(...values);
          break;
      }

      return {
        content: [{
          type: "text",
          text: `${data.length}項目の${operation.toUpperCase()}: ${result.toFixed(2)}`
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: "text",
          text: `処理エラー: ${error.message}`
        }],
        isError: true,
      };
    }
  }
);

リソースはAIモデルにコンテキストデータを提供し、静的コンテンツと動的コンテンツの両方をサポートします。動的リソーステンプレートは、パラメータ化されたデータアクセスを可能にします:

import {
  ListResourceTemplatesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({
  resourceTemplates: [
    {
      uriTemplate: "user://{userId}/profile",
      name: "ユーザープロファイル",
      description: "設定を含む完全なユーザープロファイル",
      mimeType: "application/json",
    },
    {
      uriTemplate: "analytics://{metric}/{timeRange}",
      name: "分析データ",
      description: "指定されたメトリクスの時系列分析",
      mimeType: "application/json",
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  const userMatch = uri.match(/^user:\/\/([^\/]+)\/profile$/);
  if (userMatch) {
    const userId = decodeURIComponent(userMatch[1]);
    const userData = await fetchUserFromDatabase(userId);

    return {
      contents: [{
        uri,
        text: JSON.stringify(userData, null, 2),
        mimeType: "application/json",
      }],
    };
  }

  const analyticsMatch = uri.match(/^analytics:\/\/([^\/]+)\/(.+)$/);
  if (analyticsMatch) {
    const [, metric, timeRange] = analyticsMatch;
    const data = await fetchAnalytics(metric, timeRange);

    return {
      contents: [{
        uri,
        text: JSON.stringify(data, null, 2),
        mimeType: "application/json",
      }],
    };
  }

  throw new Error(`リソースが見つかりません: ${uri}`);
});

プロンプトは、一般的なAIインタラクション用の再利用可能なテンプレートを提供し、特に複雑な多段階操作に価値があります:

import { GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(GetPromptRequestSchema, (request) => {
  const { name, arguments: args } = request.params;

  if (name === "code-review") {
    const { language, code, focus } = args as {
      language: string;
      code: string;
      focus?: string;
    };

    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `この${language}コードをレビューしてください${focus ? `(${focus}に焦点を当てて)` : ''}:

\`\`\`${language}
${code}
\`\`\`

分析内容:
- 潜在的なバグとエッジケース
- パフォーマンス最適化
- セキュリティの脆弱性
- コードスタイルとベストプラクティス
- 推奨されるリファクタリングの改善`,
          },
        },
      ],
    };
  }

  throw new Error(`プロンプトが見つかりません: ${name}`);
});

認証パターンとセキュリティ実装

セキュリティは本番MCPサーバーにとって重要な考慮事項です。OAuth 2.1実装は、トークンの誤用を防ぐためのResource Indicators(RFC 8707)を含む最新の標準に従います:

import { createMcpHandler, withMcpAuth } from "mcp-handler";
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";

const baseHandler = createMcpHandler((server) => {
  server.tool(
    "protected_operation",
    "機密データ操作を実行",
    {
      operation: z.enum(["read", "write", "delete"]),
      resource: z.string(),
    },
    async ({ operation, resource }, extra) => {
      const authInfo = extra.authInfo;

      // スコープベースの認可
      if (operation === "delete" && !authInfo?.scopes?.includes("admin:delete")) {
        return {
          content: [{ type: "text", text: "削除の権限が不足しています" }],
          isError: true,
        };
      }

      // ユーザーコンテキストで操作を実行
      const result = await performOperation(operation, resource, authInfo?.clientId);

      return {
        content: [{
          type: "text",
          text: `${authInfo?.clientId}の${operation}が完了しました: ${result}`
        }],
      };
    }
  );
});

const verifyToken = async (
  req: Request,
  bearerToken?: string
): Promise<AuthInfo | undefined> => {
  if (!bearerToken) return undefined;

  try {
    // 認可サーバーでトークンを検証
    const response = await fetch(process.env.AUTH_SERVER_URL + "/introspect", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": `Basic ${Buffer.from(
          `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
        ).toString("base64")}`,
      },
      body: new URLSearchParams({
        token: bearerToken,
        token_type_hint: "access_token",
      }),
    });

    if (!response.ok) return undefined;

    const introspection = await response.json();

    if (!introspection.active) return undefined;

    return {
      token: bearerToken,
      scopes: introspection.scope?.split(" ") || [],
      clientId: introspection.sub,
      extra: {
        userId: introspection.sub,
        organizationId: introspection.org_id,
        permissions: introspection.permissions,
      },
    };
  } catch (error) {
    console.error("トークン検証に失敗しました:", error);
    return undefined;
  }
};

const authHandler = withMcpAuth(baseHandler, verifyToken, {
  required: true,
  requiredScopes: ["mcp:read", "mcp:execute"],
  resourceMetadataPath: "/.well-known/oauth-protected-resource",
});

export { authHandler as GET, authHandler as POST };

OAuth保護リソースメタデータエンドポイントは、ディスカバリ情報を提供します:

// app/.well-known/oauth-protected-resource/route.ts
import { protectedResourceHandler } from "mcp-handler";

const handler = protectedResourceHandler({
  authServerUrls: [process.env.AUTH_SERVER_URL],
});

export { handler as GET };

Vercel固有のデプロイメントの考慮事項

Vercelのプラットフォームは、MCPサーバー用に異なるトレードオフを持つ2つの関数タイプを提供します。サーバーレス関数は、完全なNode.jsランタイム互換性、エンタープライズプランで最大900秒までの長い実行制限、包括的なnpmパッケージサポートにより、推奨されるアプローチを提供します。エッジ関数は40%高速なコールドスタートと15倍低いコストを提供しますが、V8専用ランタイムと4MBのサイズ制限により、複雑なMCP実装には適していません。

デプロイメントアーキテクチャは、特に不規則な使用パターンを持つAIワークロードに有益な、従来のサーバーレスと比較して90%のコスト削減のために、VercelのFluid Computeを活用します。応答性を維持するために、コールドスタート軽減戦略が不可欠になります:

// api/keepalive.js - コールドスタートを防ぐ
export default async function handler(req, res) {
  // 関数をウォームに保つための簡単なヘルスチェック
  const timestamp = new Date().toISOString();
  res.status(200).json({ status: "warm", timestamp });
}

// vercel.jsonでcronジョブを設定
{
  "crons": [{
    "path": "/api/keepalive",
    "schedule": "*/5 * * * *"
  }]
}

CORS設定は、異なるドメイン間での適切なクライアント接続を確保します:

export const config = {
  api: {
    cors: {
      origin: ["https://claude.ai", "https://gemini.google.com", "https://cursor.sh"],
      methods: ["GET", "POST", "OPTIONS"],
      allowedHeaders: ["Content-Type", "Authorization", "X-API-Key"],
      credentials: true,
    }
  }
}

デバッグ、テスト、モニタリング戦略

開発は、サーバーの機能に関する即座のフィードバックを提供するMCP Inspectorを使用したローカルテストから始まります:

# 開発サーバーを起動
vercel dev

# MCP Inspectorでテスト
npx @modelcontextprotocol/inspector http://localhost:3000/api/mcp

包括的なロギングは、本番環境でのデバッグを容易にします:

import { createLogger } from "./utils/logger";

const logger = createLogger({
  service: "mcp-server",
  environment: process.env.VERCEL_ENV,
});

server.tool("complex_operation", "複雑な処理を実行", schema,
  async (args) => {
    const requestId = crypto.randomUUID();

    logger.info("ツール実行開始", {
      requestId,
      tool: "complex_operation",
      args: JSON.stringify(args),
    });

    try {
      const result = await performComplexOperation(args);

      logger.info("ツール実行完了", {
        requestId,
        duration: Date.now() - startTime,
        resultSize: JSON.stringify(result).length,
      });

      return result;
    } catch (error) {
      logger.error("ツール実行失敗", {
        requestId,
        error: error.message,
        stack: error.stack,
      });

      throw error;
    }
  }
);

統合テストは、エンドツーエンドの機能を検証します:

import { describe, it, expect, beforeAll } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse";

describe("MCP Server統合", () => {
  let client: Client;

  beforeAll(async () => {
    client = new Client({
      transport: new SSEClientTransport({
        url: "http://localhost:3000/api/mcp/sse",
      }),
    });

    await client.connect();
  });

  it("適切な検証でツールを実行すべき", async () => {
    const result = await client.callTool({
      name: "process_data",
      args: {
        data: [
          { id: "123e4567-e89b-12d3-a456-426614174000", value: 42, timestamp: new Date().toISOString() },
          { id: "987f6543-e21b-12d3-a456-426614174111", value: 84, timestamp: new Date().toISOString() },
        ],
        operation: "average",
      },
    });

    expect(result.content[0].text).toContain("2項目のAVERAGE: 63.00");
  });
});

本番モニタリングは、Vercelの組み込み分析とカスタムメトリクスを活用します:

const metrics = {
  toolExecutions: new Map(),
  errors: [],
  responseTimings: [],
};

const withMetrics = (toolName: string, toolFunction: Function) => {
  return async (...args: any[]) => {
    const startTime = Date.now();
    const executionId = crypto.randomUUID();

    metrics.toolExecutions.set(executionId, {
      tool: toolName,
      startTime,
      status: "running",
    });

    try {
      const result = await toolFunction(...args);
      const duration = Date.now() - startTime;

      metrics.toolExecutions.set(executionId, {
        ...metrics.toolExecutions.get(executionId),
        status: "completed",
        duration,
      });

      metrics.responseTimings.push(duration);

      return result;
    } catch (error) {
      metrics.errors.push({
        tool: toolName,
        error: error.message,
        timestamp: new Date().toISOString(),
      });

      throw error;
    }
  };
};

実世界のユースケースとアーキテクチャパターン

本番デプロイメントは、業界全体で多様なMCPアプリケーションを示しています。**Block(Square)**は、内部金融ツール用に60以上のMCPサーバーを運用し、支払い、在庫、分析用に別々のサーバーを持つドメイン駆動設計を実装しています。Netflixは、コンテンツ管理ワークフローにMCPを活用し、メタデータ、トランスコーディング、推奨システムを独立して処理するマルチサーバーパターンを使用しています。

マルチサーバーアーキテクチャパターンは、懸念事項を効果的に分離します:

// コアビジネスロジックサーバー
const coreServer = createMcpHandler((server) => {
  server.tool("create_order", "新しい注文を作成", orderSchema,
    async (orderData) => {
      const order = await createOrder(orderData);
      await publishEvent("order.created", order);
      return { content: [{ type: "text", text: `注文 ${order.id} が作成されました` }] };
    }
  );
});

// 読み取り専用操作を持つ分析サーバー
const analyticsServer = createMcpHandler((server) => {
  server.tool("generate_report", "分析レポートを生成", reportSchema,
    async (params) => {
      const report = await generateReport(params);
      return { content: [{ type: "text", text: JSON.stringify(report) }] };
    }
  );
});

// 昇格された権限を持つ管理サーバー
const adminServer = createMcpHandler((server) => {
  server.tool("manage_users", "ユーザー管理操作", userManagementSchema,
    async (operation, extra) => {
      if (!extra.authInfo?.scopes?.includes("admin:users")) {
        throw new Error("管理者権限が必要です");
      }
      const result = await performUserManagement(operation);
      return { content: [{ type: "text", text: result }] };
    }
  );
});

サーキットブレーカーパターンは、外部サービスを統合する際のレジリエンスを確保します:

class CircuitBreaker {
  private failureCount = 0;
  private lastFailureTime?: number;
  private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED";

  constructor(
    private threshold = 5,
    private timeout = 60000,
    private resetTimeout = 120000
  ) {}

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === "OPEN") {
      if (Date.now() - this.lastFailureTime! > this.resetTimeout) {
        this.state = "HALF_OPEN";
        this.failureCount = 0;
      } else {
        throw new Error("サーキットブレーカーがOPEN - サービス利用不可");
      }
    }

    try {
      const result = await Promise.race([
        operation(),
        new Promise<never>((_, reject) =>
          setTimeout(() => reject(new Error("操作タイムアウト")), this.timeout)
        ),
      ]);

      if (this.state === "HALF_OPEN") {
        this.state = "CLOSED";
        this.failureCount = 0;
      }

      return result;
    } catch (error) {
      this.failureCount++;
      this.lastFailureTime = Date.now();

      if (this.failureCount >= this.threshold) {
        this.state = "OPEN";
      }

      throw error;
    }
  }
}

const breaker = new CircuitBreaker();

server.tool("external_api_call", "サーキットブレーカーで外部APIを呼び出す", schema,
  async (params) => {
    try {
      const result = await breaker.execute(async () => {
        const response = await fetch("https://api.external.com/data", {
          method: "POST",
          body: JSON.stringify(params),
        });
        return response.json();
      });

      return { content: [{ type: "text", text: JSON.stringify(result) }] };
    } catch (error) {
      return {
        content: [{
          type: "text",
          text: "外部サービスが一時的に利用できません - 後でもう一度お試しください"
        }],
        isError: true,
      };
    }
  }
);

最近のアップデートとエコシステムの進化

MCPエコシステムは、2024年11月のリリース以来、爆発的な成長を遂げています。2025年の主要なプラットフォーム採用には、3月のGeminiデスクトップおよびAgents SDK全体でのGoogleの包括的な統合、5月のMicrosoftのワンクリックサーバー接続を備えたネイティブCopilot Studioサポート、7月のAWSのLambda、ECS、およびその他のサービス用の公式MCPサーバーが含まれます。

セキュリティの改善により、Resource Indicators(RFC 8707)を使用したOAuth 2.1実装、動作宣言のための強化されたツール注釈、包括的な監査ログ機能を通じて、初期の脆弱性に対処しました。プロトコルは現在、テキストと画像と一緒にオーディオコンテンツ、改善された効率のためのJSON-RPCバッチング、廃止されたSSEメカニズムを置き換えるStreamable HTTPトランスポートをサポートしています。

コミュニティは2025年2月までに1000以上のMCPサーバーを作成し、金融サービス(Stripe、Block)から開発者ツール(GitHub、VS Code)、データプラットフォーム(Databricks、Supabase)まで、多様なユースケースに及んでいます。NetflixやBlockなどの企業が本番環境で数十のサーバーを運用しており、企業の展開はMCPの本番準備を示しています。

市場予測では、MCPエコシステムは2025年末までに45億ドルに達し、組織の90%がプロトコルを採用すると予想されています。技術的な進化は、マルチエージェント調整に対処するGoogleのAgent2Agentプロトコル、拡張されたクラウドプラットフォーム統合、継続的なセキュリティフレームワークの成熟を続けています。

ベストプラクティスとアーキテクチャの推奨事項

成功するMCPサーバーの実装には、セキュリティ、パフォーマンス、保守性に注意深い注意が必要です。セキュリティファーストの設計は、最初からOAuth 2.1を実装し、Zodスキーマを使用してすべての入力を検証し、レート制限とサーキットブレーカーを実装し、包括的な監査ログを維持します。DockerまたはKubernetesを使用したコンテナ分離は、本番展開用の追加のセキュリティ境界を提供します。

パフォーマンス最適化は、接続状態管理のためにRedisを活用し、インテリジェントなキャッシング戦略を実装し、データベースアクセス用の接続プーリングを使用し、メトリクスを継続的に監視します。マルチサーバーパターンは、読み取りと書き込み操作を分離し、重い処理ワークロードを分離し、異なる機能の独立したスケーリングを可能にします。

開発ワークフローのベストプラクティスには、新しいプロジェクト用のStreamable HTTPトランスポートから始め、一貫した実装のために公式SDKを使用し、フォールバックを使用した包括的なエラー処理を実装し、統合テストを含む高いテストカバレッジを維持することが含まれます。ドキュメントは、API仕様、認証要件、リクエストとレスポンスの例、トラブルシューティングガイドをカバーする必要があります。

デプロイメント戦略は、機能フラグを使用した段階的なロールアウト、初日からの包括的なモニタリング、定期的なセキュリティ監査と更新、災害復旧計画を優先します。組織は、低リスクのパイロットプログラムから始め、小規模な展開でパターンを検証し、証明された成功に基づいて徐々に拡大する必要があります。

Model Context Protocolは、AIシステムが外部ツールやデータソースとどのように相互作用するかにおける根本的な変化を表しています。MCPの思慮深いプロトコル設計とvercel/mcp-handlerのプロダクションレディな実装の組み合わせは、洗練されたAI統合を構築するための強力な基盤を作成します。エコシステムが強化されたセキュリティ機能、より広範なプラットフォームの採用、実証済みのエンタープライズ展開で成熟し続ける中、MCPは次世代のAI駆動アプリケーションの標準プロトコルとしての地位を確立しています。