ドラフト エージェント設計パターン - Model Context Protocol (MCP)

mcpaiagentstypescriptnextjsvercellangchain
By sko X opus 4.19/20/202515 min read

Model Context Protocol (MCP) を実装して、外部ツール、サービス、データソースを動的に発見し相互作用する標準化された相互運用可能なAIエージェントを、Vercelのサーバーレスプラットフォーム上のNext.js 15で作成する方法を学びます。

メンタルモデル: AIエージェントのためのユニバーサルアダプター

MCPはAIエージェントのためのUSB-C標準として考えてください。USB-Cが互換性のある任意のデバイス(スマートフォン、ラップトップ、ディスプレイ)で動作するユニバーサルコネクタを提供するように、MCPはLLMが任意の外部システムに接続するための標準化されたプロトコルを作成します。TypeScript/Next.jsの世界では、任意の外部サービスに対して自動的にTypeScriptインターフェースを生成するユニバーサルミドルウェアのようなもので、AIエージェントが統合をハードコーディングすることなく実行時に機能を発見して使用できるようにします。@vercel/mcp-handlerパッケージはあなたのアダプターとして機能し、MCPサーバーをVercelのインフラストラクチャーとシームレスに動作するサーバーレス対応エンドポイントに変換します。

基本例: VercelでMCPサーバーを作成する

基本的なクライアント-サーバーアーキテクチャを実演する、タスク管理のためのツールを公開するシンプルなMCPサーバーを構築しましょう。

1. MCP依存関係をインストールする

npm install @vercel/mcp-handler @modelcontextprotocol/sdk zod es-toolkit

サーバーレス互換性のためのVercelのMCPハンドラー、MCP SDK、検証ユーティリティをインストールします。

2. MCPサーバーツールを定義する

// lib/mcp/task-tools.ts
import { z } from 'zod';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { groupBy, sortBy } from 'es-toolkit';

// 型安全性のためのスキーマを定義
const TaskSchema = z.object({
  id: z.string(),
  title: z.string(),
  status: z.enum(['pending', 'in-progress', 'completed']),
  priority: z.number().min(1).max(5),
  createdAt: z.date(),
  assignee: z.string().optional(),
});

type Task = z.infer<typeof TaskSchema>;

// インメモリタスクストア(本番環境ではデータベースに置き換える)
const tasks = new Map<string, Task>();

// 明確なインターフェースでMCPツールを定義
export const taskTools: Tool[] = [
  {
    name: 'create_task',
    description: 'Create a new task with specified details',
    inputSchema: {
      type: 'object',
      properties: {
        title: { type: 'string', description: 'Task title' },
        priority: {
          type: 'number',
          minimum: 1,
          maximum: 5,
          description: 'Priority level (1-5)'
        },
        assignee: {
          type: 'string',
          description: 'Optional assignee name'
        },
      },
      required: ['title', 'priority'],
    },
  },
  {
    name: 'list_tasks',
    description: 'List all tasks with optional filtering',
    inputSchema: {
      type: 'object',
      properties: {
        status: {
          type: 'string',
          enum: ['pending', 'in-progress', 'completed'],
          description: 'Filter by status'
        },
        assignee: {
          type: 'string',
          description: 'Filter by assignee'
        },
      },
    },
  },
  {
    name: 'update_task_status',
    description: 'Update the status of an existing task',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: 'Task ID' },
        status: {
          type: 'string',
          enum: ['pending', 'in-progress', 'completed'],
          description: 'New status'
        },
      },
      required: ['id', 'status'],
    },
  },
];

// ツール実装
export async function executeTool(name: string, args: any) {
  switch (name) {
    case 'create_task': {
      const task: Task = {
        id: `task_${Date.now()}`,
        title: args.title,
        status: 'pending',
        priority: args.priority,
        createdAt: new Date(),
        assignee: args.assignee,
      };
      tasks.set(task.id, task);
      return { success: true, task };
    }

    case 'list_tasks': {
      let taskList = Array.from(tasks.values());

      // es-toolkitを使用してフィルターを適用
      if (args.status) {
        taskList = taskList.filter(t => t.status === args.status);
      }
      if (args.assignee) {
        taskList = taskList.filter(t => t.assignee === args.assignee);
      }

      // 優先度でソート
      taskList = sortBy(taskList, [(t) => -t.priority]);

      return { tasks: taskList, count: taskList.length };
    }

    case 'update_task_status': {
      const task = tasks.get(args.id);
      if (!task) {
        throw new Error(`Task ${args.id} not found`);
      }
      task.status = args.status;
      tasks.set(args.id, task);
      return { success: true, task };
    }

    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

JSON Schema検証でMCPツールを定義し、es-toolkitユーティリティでタスク管理操作を実装します。

3. MCPサーバーハンドラーを作成する

// lib/mcp/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { taskTools, executeTool } from './task-tools';

export async function createMCPServer() {
  const server = new Server(
    {
      name: 'task-manager',
      version: '1.0.0',
    },
    {
      capabilities: {
        tools: {},
        resources: {},
      },
    }
  );

  // ツール発見を登録
  server.setRequestHandler('tools/list', async () => {
    return { tools: taskTools };
  });

  // ツール実行を登録
  server.setRequestHandler('tools/call', async (request) => {
    const { name, arguments: args } = CallToolRequestSchema.parse(request);

    try {
      const result = await executeTool(name, args);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
          },
        ],
        isError: true,
      };
    }
  });

  return server;
}

// サーバーレス使用のためにエクスポート
export async function startMCPServer() {
  const server = await createMCPServer();
  const transport = new StdioServerTransport();
  await server.connect(transport);
  return server;
}

サーバーレス向けの適切なエラーハンドリングでツール発見と実行を処理するMCPサーバーを作成します。

4. Vercelサーバーレス MCPハンドラー

// app/api/mcp/route.ts
import { createMCPHandler } from '@vercel/mcp-handler';
import { createMCPServer } from '@/lib/mcp/server';

export const runtime = 'nodejs';
export const maxDuration = 60; // 複雑な操作のためにより長い実行を許可

const handler = createMCPHandler({
  createServer: createMCPServer,
  // Vercelのサーバーレス環境用に設定
  options: {
    keepAlive: false, // サーバーレス用に無効化
    timeout: 55000, // maxDurationよりわずかに短く
  },
});

export const POST = handler;

サーバーレスエンドポイントを通じてMCPサーバーを公開するVercel互換のAPIルートを作成します。

5. LangchainとのMCPクライアント統合

// lib/agents/mcp-client-agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';

export class MCPClientAgent {
  private model: ChatGoogleGenerativeAI;
  private mcpClient: Client;
  private tools: DynamicStructuredTool[] = [];

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-flash',
      temperature: 0,
    });

    this.mcpClient = new Client(
      {
        name: 'langchain-agent',
        version: '1.0.0',
      },
      {
        capabilities: {},
      }
    );
  }

  async connect(serverUrl: string) {
    // MCPサーバーに接続
    const transport = new WebSocketClientTransport(
      new URL(serverUrl)
    );
    await this.mcpClient.connect(transport);

    // 利用可能なツールを発見
    await this.discoverTools();
  }

  private async discoverTools() {
    const response = await this.mcpClient.request(
      { method: 'tools/list' },
      {}
    );

    // MCPツールをLangchainツールに変換
    this.tools = response.tools.map(tool =>
      new DynamicStructuredTool({
        name: tool.name,
        description: tool.description,
        schema: this.convertToZodSchema(tool.inputSchema),
        func: async (input) => {
          const result = await this.mcpClient.request(
            { method: 'tools/call' },
            { name: tool.name, arguments: input }
          );
          return result.content[0].text;
        },
      })
    );
  }

  private convertToZodSchema(jsonSchema: any): z.ZodSchema {
    // 簡略化した変換 - 本番環境では拡張
    const shape: Record<string, z.ZodSchema> = {};

    for (const [key, prop] of Object.entries(jsonSchema.properties || {})) {
      const propSchema = prop as any;

      if (propSchema.type === 'string') {
        shape[key] = propSchema.enum
          ? z.enum(propSchema.enum)
          : z.string();
      } else if (propSchema.type === 'number') {
        shape[key] = z.number();
      }

      // オプショナル処理を追加
      if (!jsonSchema.required?.includes(key)) {
        shape[key] = shape[key].optional();
      }
    }

    return z.object(shape);
  }

  async execute(input: string) {
    // 発見されたツールをモデルで使用
    const response = await this.model.invoke(
      input,
      { tools: this.tools }
    );

    return response;
  }
}

サーバーからツールを発見し、Langchain互換のツールに変換するMCPクライアントを作成します。

高度な例: リソースとプロンプトを含む本番MCPシステム

リソース(データアクセス)、ツール(アクション)、プロンプト(テンプレート)を含む包括的なMCP実装を構築しましょう。

1. リソース付き拡張MCPサーバー

// lib/mcp/advanced-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Resource, Tool, Prompt } from '@modelcontextprotocol/sdk/types.js';
import { groupBy, chunk, debounce } from 'es-toolkit';
import { z } from 'zod';

// 包括的なデータスキーマを定義
const DocumentSchema = z.object({
  id: z.string(),
  title: z.string(),
  content: z.string(),
  metadata: z.object({
    author: z.string(),
    createdAt: z.date(),
    tags: z.array(z.string()),
    version: z.number(),
  }),
});

const AnalyticsEventSchema = z.object({
  eventType: z.string(),
  userId: z.string(),
  timestamp: z.date(),
  properties: z.record(z.unknown()),
});

type Document = z.infer<typeof DocumentSchema>;
type AnalyticsEvent = z.infer<typeof AnalyticsEventSchema>;

// リソース定義
export const resources: Resource[] = [
  {
    uri: 'documents://list',
    name: 'Document Library',
    description: 'Access to all documents in the system',
    mimeType: 'application/json',
  },
  {
    uri: 'analytics://events',
    name: 'Analytics Events',
    description: 'Stream of analytics events',
    mimeType: 'application/x-ndjson',
  },
  {
    uri: 'config://settings',
    name: 'System Configuration',
    description: 'Current system settings and configuration',
    mimeType: 'application/json',
  },
];

// 拡張ツール定義
export const advancedTools: Tool[] = [
  {
    name: 'analyze_documents',
    description: 'Perform semantic analysis on documents',
    inputSchema: {
      type: 'object',
      properties: {
        documentIds: {
          type: 'array',
          items: { type: 'string' },
          description: 'Document IDs to analyze',
        },
        analysisType: {
          type: 'string',
          enum: ['sentiment', 'summary', 'keywords', 'entities'],
          description: 'Type of analysis to perform',
        },
        options: {
          type: 'object',
          properties: {
            maxLength: { type: 'number' },
            threshold: { type: 'number' },
          },
        },
      },
      required: ['documentIds', 'analysisType'],
    },
  },
  {
    name: 'process_batch',
    description: 'Process multiple items in parallel batches',
    inputSchema: {
      type: 'object',
      properties: {
        items: {
          type: 'array',
          description: 'Items to process',
        },
        batchSize: {
          type: 'number',
          default: 10,
          description: 'Size of each batch',
        },
        operation: {
          type: 'string',
          enum: ['transform', 'validate', 'enrich'],
        },
      },
      required: ['items', 'operation'],
    },
  },
];

// 一般的な操作のためのプロンプトテンプレート
export const prompts: Prompt[] = [
  {
    name: 'document_qa',
    description: 'Question answering based on document context',
    arguments: [
      {
        name: 'question',
        description: 'The question to answer',
        required: true,
      },
      {
        name: 'context',
        description: 'Document context for answering',
        required: true,
      },
    ],
  },
  {
    name: 'data_synthesis',
    description: 'Synthesize insights from multiple data sources',
    arguments: [
      {
        name: 'sources',
        description: 'Data sources to synthesize',
        required: true,
      },
      {
        name: 'format',
        description: 'Output format (report, summary, bullets)',
        required: false,
      },
    ],
  },
];

// 高度なMCPサーバーを作成
export class AdvancedMCPServer {
  private server: Server;
  private documents = new Map<string, Document>();
  private events: AnalyticsEvent[] = [];

  // デバウンスされたイベントプロセッサー
  private processEvents = debounce(() => {
    const grouped = groupBy(this.events, e => e.eventType);
    console.log('Processing event batches:', Object.keys(grouped));
    // グループ化されたイベントを処理
  }, 1000);

  constructor() {
    this.server = new Server(
      {
        name: 'advanced-mcp-server',
        version: '2.0.0',
      },
      {
        capabilities: {
          tools: {},
          resources: { subscribe: true },
          prompts: {},
        },
      }
    );

    this.setupHandlers();
  }

  private setupHandlers() {
    // リソースハンドラー
    this.server.setRequestHandler('resources/list', async () => {
      return { resources };
    });

    this.server.setRequestHandler('resources/read', async (request) => {
      const { uri } = request;

      switch (uri) {
        case 'documents://list':
          return {
            contents: [
              {
                uri,
                mimeType: 'application/json',
                text: JSON.stringify(
                  Array.from(this.documents.values()),
                  null,
                  2
                ),
              },
            ],
          };

        case 'analytics://events':
          // チャンクを使用してイベントをストリーミング
          const eventChunks = chunk(this.events, 100);
          return {
            contents: eventChunks.map((chunk, idx) => ({
              uri: `${uri}#chunk-${idx}`,
              mimeType: 'application/x-ndjson',
              text: chunk.map(e => JSON.stringify(e)).join('\n'),
            })),
          };

        case 'config://settings':
          return {
            contents: [
              {
                uri,
                mimeType: 'application/json',
                text: JSON.stringify({
                  maxBatchSize: 100,
                  timeout: 30000,
                  features: {
                    streaming: true,
                    batching: true,
                    caching: true,
                  },
                }),
              },
            ],
          };

        default:
          throw new Error(`Unknown resource: ${uri}`);
      }
    });

    // 高度な処理を含むツールハンドラー
    this.server.setRequestHandler('tools/list', async () => {
      return { tools: advancedTools };
    });

    this.server.setRequestHandler('tools/call', async (request) => {
      const { name, arguments: args } = request;

      switch (name) {
        case 'analyze_documents':
          return await this.analyzeDocuments(args);

        case 'process_batch':
          return await this.processBatch(args);

        default:
          throw new Error(`Unknown tool: ${name}`);
      }
    });

    // プロンプトハンドラー
    this.server.setRequestHandler('prompts/list', async () => {
      return { prompts };
    });

    this.server.setRequestHandler('prompts/get', async (request) => {
      const { name, arguments: args } = request;
      const prompt = prompts.find(p => p.name === name);

      if (!prompt) {
        throw new Error(`Unknown prompt: ${name}`);
      }

      // テンプレートと引数に基づいてプロンプトを生成
      return {
        messages: [
          {
            role: 'system',
            content: this.generatePromptContent(name, args),
          },
        ],
      };
    });
  }

  private async analyzeDocuments(args: any) {
    const { documentIds, analysisType, options = {} } = args;

    // ドキュメントを取得
    const docs = documentIds
      .map((id: string) => this.documents.get(id))
      .filter(Boolean);

    // es-toolkitユーティリティを使用して分析を実行
    const results = await Promise.all(
      docs.map(async (doc) => {
        switch (analysisType) {
          case 'summary':
            return {
              docId: doc!.id,
              summary: doc!.content.substring(0, options.maxLength || 200),
            };

          case 'keywords':
            // キーワードを抽出(簡略化)
            const words = doc!.content.toLowerCase().split(/\s+/);
            const wordGroups = groupBy(words, w => w);
            const keywords = Object.entries(wordGroups)
              .sort((a, b) => b[1].length - a[1].length)
              .slice(0, 10)
              .map(([word]) => word);

            return {
              docId: doc!.id,
              keywords,
            };

          default:
            return { docId: doc!.id, result: 'Analysis complete' };
        }
      })
    );

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify({ analysisType, results }, null, 2),
        },
      ],
    };
  }

  private async processBatch(args: any) {
    const { items, batchSize = 10, operation } = args;

    // アイテムをバッチで処理
    const batches = chunk(items, batchSize);
    const results = [];

    for (const batch of batches) {
      const batchResults = await Promise.all(
        batch.map(async (item: any) => {
          switch (operation) {
            case 'transform':
              return { ...item, transformed: true };

            case 'validate':
              return { item, valid: true };

            case 'enrich':
              return { ...item, enriched: { timestamp: new Date() } };

            default:
              return item;
          }
        })
      );

      results.push(...batchResults);
    }

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify({
            operation,
            processedCount: results.length,
            batchCount: batches.length,
            results,
          }, null, 2),
        },
      ],
    };
  }

  private generatePromptContent(name: string, args: any): string {
    switch (name) {
      case 'document_qa':
        return `あなたは質問に答えるためにドキュメントを分析しています。

コンテキスト: ${args.context}

提供されたコンテキストに基づいて、以下の質問に答えてください:
${args.question}

コンテキストに裏付けられた明確で簡潔な回答を提供してください。`;

      case 'data_synthesis':
        const format = args.format || 'summary';
        return `以下のデータソースから洞察を合成してください:

${JSON.stringify(args.sources, null, 2)}

出力形式: ${format}

すべてのソース間でパターン、相関関係、主要な洞察を分析してください。`;

      default:
        return '提供された引数に基づいてリクエストを処理してください。';
    }
  }

  async connect(transport: any) {
    await this.server.connect(transport);
  }
}

データアクセス用のリソース、高度なツール、プロンプトテンプレートを含む包括的なMCPサーバーを実装します。

2. 認証付きVercelハンドラー

// app/api/mcp/advanced/route.ts
import { NextRequest } from 'next/server';
import { createMCPHandler } from '@vercel/mcp-handler';
import { AdvancedMCPServer } from '@/lib/mcp/advanced-server';
import { verifyToken } from '@/lib/auth';

export const runtime = 'nodejs';
export const maxDuration = 300;

const handler = createMCPHandler({
  createServer: async () => new AdvancedMCPServer(),

  // 認証ミドルウェアを追加
  middleware: async (req: NextRequest) => {
    const token = req.headers.get('authorization')?.replace('Bearer ', '');

    if (!token) {
      throw new Error('Unauthorized: No token provided');
    }

    const valid = await verifyToken(token);
    if (!valid) {
      throw new Error('Unauthorized: Invalid token');
    }

    return req;
  },

  options: {
    keepAlive: false,
    timeout: 295000, // maxDurationよりわずかに短く
    maxPayloadSize: 10 * 1024 * 1024, // 10MB
  },
});

export const POST = handler;

MCPサーバーエンドポイントに認証とセキュリティを追加します。

3. MCP統合を含むReactフロントエンド

// components/MCPInterface.tsx
'use client';

import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

interface Tool {
  name: string;
  description: string;
  inputSchema: any;
}

interface Resource {
  uri: string;
  name: string;
  description: string;
}

export default function MCPInterface() {
  const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
  const [toolInput, setToolInput] = useState('{}');
  const [mcpClient, setMcpClient] = useState<Client | null>(null);

  // MCPクライアントを初期化
  useEffect(() => {
    const initClient = async () => {
      const client = new Client(
        {
          name: 'web-client',
          version: '1.0.0',
        },
        {
          capabilities: {},
        }
      );

      // Vercelエンドポイント経由でサーバーに接続
      await client.connectToServer('/api/mcp/advanced');
      setMcpClient(client);
    };

    initClient();
  }, []);

  // 利用可能なツールを発見
  const { data: tools, isLoading: toolsLoading } = useQuery({
    queryKey: ['mcp-tools'],
    queryFn: async () => {
      if (!mcpClient) return [];

      const response = await mcpClient.request(
        { method: 'tools/list' },
        {}
      );

      return response.tools as Tool[];
    },
    enabled: !!mcpClient,
  });

  // 利用可能なリソースを発見
  const { data: resources } = useQuery({
    queryKey: ['mcp-resources'],
    queryFn: async () => {
      if (!mcpClient) return [];

      const response = await mcpClient.request(
        { method: 'resources/list' },
        {}
      );

      return response.resources as Resource[];
    },
    enabled: !!mcpClient,
  });

  // ツール実行ミューテーション
  const executeTool = useMutation({
    mutationFn: async ({ tool, input }: { tool: Tool; input: any }) => {
      if (!mcpClient) throw new Error('MCP client not initialized');

      const response = await mcpClient.request(
        { method: 'tools/call' },
        {
          name: tool.name,
          arguments: input,
        }
      );

      return response;
    },
  });

  // リソース読み取りミューテーション
  const readResource = useMutation({
    mutationFn: async (uri: string) => {
      if (!mcpClient) throw new Error('MCP client not initialized');

      const response = await mcpClient.request(
        { method: 'resources/read' },
        { uri }
      );

      return response;
    },
  });

  const handleToolExecution = () => {
    if (!selectedTool) return;

    try {
      const input = JSON.parse(toolInput);
      executeTool.mutate({ tool: selectedTool, input });
    } catch (error) {
      console.error('Invalid JSON input:', error);
    }
  };

  return (
    <div className="card bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">MCPコントロールパネル</h2>

        {/* ツールセクション */}
        <div className="divider">利用可能なツール</div>

        {toolsLoading ? (
          <div className="loading loading-spinner"></div>
        ) : (
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
            {tools?.map((tool) => (
              <div
                key={tool.name}
                className={`card bg-base-200 cursor-pointer hover:bg-base-300 ${
                  selectedTool?.name === tool.name ? 'ring-2 ring-primary' : ''
                }`}
                onClick={() => setSelectedTool(tool)}
              >
                <div className="card-body compact">
                  <h3 className="font-bold">{tool.name}</h3>
                  <p className="text-sm opacity-70">{tool.description}</p>
                </div>
              </div>
            ))}
          </div>
        )}

        {/* ツール実行 */}
        {selectedTool && (
          <div className="mt-6">
            <h3 className="text-lg font-bold mb-2">
              実行: {selectedTool.name}
            </h3>

            <div className="form-control">
              <label className="label">
                <span className="label-text">入力 (JSON)</span>
              </label>
              <textarea
                className="textarea textarea-bordered h-32 font-mono text-sm"
                value={toolInput}
                onChange={(e) => setToolInput(e.target.value)}
                placeholder={JSON.stringify(
                  selectedTool.inputSchema.properties,
                  null,
                  2
                )}
              />
            </div>

            <button
              className="btn btn-primary mt-4"
              onClick={handleToolExecution}
              disabled={executeTool.isPending}
            >
              {executeTool.isPending ? (
                <>
                  <span className="loading loading-spinner"></span>
                  実行中...
                </>
              ) : (
                'ツールを実行'
              )}
            </button>

            {executeTool.data && (
              <div className="alert alert-success mt-4">
                <pre className="text-xs overflow-auto">
                  {JSON.stringify(executeTool.data, null, 2)}
                </pre>
              </div>
            )}

            {executeTool.isError && (
              <div className="alert alert-error mt-4">
                <span>実行に失敗しました。入力を確認してください。</span>
              </div>
            )}
          </div>
        )}

        {/* リソースセクション */}
        <div className="divider">利用可能なリソース</div>

        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          {resources?.map((resource) => (
            <div
              key={resource.uri}
              className="card bg-base-200 cursor-pointer hover:bg-base-300"
              onClick={() => readResource.mutate(resource.uri)}
            >
              <div className="card-body compact">
                <h3 className="font-bold">{resource.name}</h3>
                <p className="text-sm opacity-70">{resource.description}</p>
                <p className="text-xs font-mono">{resource.uri}</p>
              </div>
            </div>
          ))}
        </div>

        {readResource.data && (
          <div className="modal modal-open">
            <div className="modal-box max-w-3xl">
              <h3 className="font-bold text-lg">リソースコンテンツ</h3>
              <pre className="text-xs overflow-auto mt-4">
                {JSON.stringify(readResource.data, null, 2)}
              </pre>
              <div className="modal-action">
                <button
                  className="btn"
                  onClick={() => readResource.reset()}
                >
                  閉じる
                </button>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

MCPサーバーとの相互作用、機能の発見、ツールの実行のための包括的なReactインターフェースを作成します。

4. 動的MCP発見を含むLangchainエージェント

// lib/agents/dynamic-mcp-agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { z } from 'zod';
import { memoize, throttle } from 'es-toolkit';

export class DynamicMCPAgent {
  private model: ChatGoogleGenerativeAI;
  private mcpClients: Map<string, Client> = new Map();
  private agent: any;

  // メモ化されたツール発見
  private discoverTools = memoize(
    async (serverUrl: string) => {
      const client = await this.connectToServer(serverUrl);
      const response = await client.request(
        { method: 'tools/list' },
        {}
      );
      return response.tools;
    },
    {
      getCacheKey: (serverUrl) => serverUrl,
      ttl: 300000, // 5分間キャッシュ
    }
  );

  // スロットルされたリソース読み取り
  private readResource = throttle(
    async (client: Client, uri: string) => {
      const response = await client.request(
        { method: 'resources/read' },
        { uri }
      );
      return response;
    },
    1000 // 最大1秒に1回
  );

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-pro',
      temperature: 0,
      streaming: true,
    });
  }

  async connectToServer(serverUrl: string): Promise<Client> {
    if (this.mcpClients.has(serverUrl)) {
      return this.mcpClients.get(serverUrl)!;
    }

    const client = new Client(
      {
        name: 'dynamic-agent',
        version: '1.0.0',
      },
      {
        capabilities: {},
      }
    );

    // プロトコルに基づいて接続
    if (serverUrl.startsWith('ws://') || serverUrl.startsWith('wss://')) {
      // WebSocket接続
      const { WebSocketClientTransport } = await import(
        '@modelcontextprotocol/sdk/client/websocket.js'
      );
      const transport = new WebSocketClientTransport(new URL(serverUrl));
      await client.connect(transport);
    } else {
      // Vercel経由のHTTP接続
      await client.connectToServer(serverUrl);
    }

    this.mcpClients.set(serverUrl, client);
    return client;
  }

  async addMCPServers(serverUrls: string[]) {
    const allTools: DynamicStructuredTool[] = [];

    for (const serverUrl of serverUrls) {
      try {
        // 各サーバーからツールを発見
        const tools = await this.discoverTools(serverUrl);
        const client = this.mcpClients.get(serverUrl)!;

        // Langchainツールに変換
        for (const tool of tools) {
          const langchainTool = new DynamicStructuredTool({
            name: `${serverUrl}_${tool.name}`.replace(/[^a-zA-Z0-9_]/g, '_'),
            description: `[${serverUrl}] ${tool.description}`,
            schema: this.convertMCPSchemaToZod(tool.inputSchema),
            func: async (input) => {
              const result = await client.request(
                { method: 'tools/call' },
                {
                  name: tool.name,
                  arguments: input,
                }
              );
              return result.content[0].text;
            },
          });

          allTools.push(langchainTool);
        }

        // リソース読み取り用のツールも作成
        const resourcesResponse = await client.request(
          { method: 'resources/list' },
          {}
        );

        for (const resource of resourcesResponse.resources || []) {
          const resourceTool = new DynamicStructuredTool({
            name: `read_${resource.uri}`.replace(/[^a-zA-Z0-9_]/g, '_'),
            description: `リソースを読み取る: ${resource.description}`,
            schema: z.object({}),
            func: async () => {
              const result = await this.readResource(client, resource.uri);
              return JSON.stringify(result.contents[0], null, 2);
            },
          });

          allTools.push(resourceTool);
        }
      } catch (error) {
        console.error(`Failed to connect to ${serverUrl}:`, error);
      }
    }

    // 発見されたすべてのツールでエージェントを作成
    this.agent = createReactAgent({
      llm: this.model,
      tools: allTools,
    });

    return allTools.length;
  }

  private convertMCPSchemaToZod(schema: any): z.ZodSchema {
    if (!schema || !schema.properties) {
      return z.object({});
    }

    const shape: Record<string, z.ZodSchema> = {};

    for (const [key, prop] of Object.entries(schema.properties)) {
      const propSchema = prop as any;
      let zodSchema: z.ZodSchema;

      switch (propSchema.type) {
        case 'string':
          zodSchema = propSchema.enum
            ? z.enum(propSchema.enum as [string, ...string[]])
            : z.string();
          if (propSchema.description) {
            zodSchema = zodSchema.describe(propSchema.description);
          }
          break;

        case 'number':
          zodSchema = z.number();
          if (propSchema.minimum !== undefined) {
            zodSchema = (zodSchema as z.ZodNumber).min(propSchema.minimum);
          }
          if (propSchema.maximum !== undefined) {
            zodSchema = (zodSchema as z.ZodNumber).max(propSchema.maximum);
          }
          break;

        case 'boolean':
          zodSchema = z.boolean();
          break;

        case 'array':
          if (propSchema.items) {
            zodSchema = z.array(this.convertMCPSchemaToZod(propSchema.items));
          } else {
            zodSchema = z.array(z.unknown());
          }
          break;

        case 'object':
          zodSchema = propSchema.properties
            ? this.convertMCPSchemaToZod(propSchema)
            : z.record(z.unknown());
          break;

        default:
          zodSchema = z.unknown();
      }

      // 必須フィールドを処理
      if (!schema.required?.includes(key)) {
        zodSchema = zodSchema.optional();
      }

      shape[key] = zodSchema;
    }

    return z.object(shape);
  }

  async execute(input: string) {
    if (!this.agent) {
      throw new Error('MCPサーバーが接続されていません。最初にaddMCPServersを呼び出してください。');
    }

    const response = await this.agent.stream({
      messages: [{ role: 'user', content: input }],
    });

    const results = [];
    for await (const chunk of response) {
      results.push(chunk);
    }

    return results;
  }
}

// APIルートでの使用
export async function POST(req: Request) {
  const { message, mcpServers } = await req.json();

  const agent = new DynamicMCPAgent();

  // 複数のMCPサーバーに接続
  const toolCount = await agent.addMCPServers(
    mcpServers || [
      '/api/mcp/advanced',
      'ws://localhost:3001/mcp',
      'https://external-mcp-server.com/mcp',
    ]
  );

  console.log(`MCPサーバー間で${toolCount}個のツールに接続しました`);

  const result = await agent.execute(message);

  return new Response(
    JSON.stringify({ result }),
    { headers: { 'Content-Type': 'application/json' } }
  );
}

複数のMCPサーバーから同時にツールを発見して使用できる動的MCPエージェントを実装します。

結論

Model Context Protocol (MCP) は、ツール、リソース、プロンプトのための標準化された発見可能なインターフェースを提供することで、AIエージェントが外部システムと相互作用する方法を変革します。VercelでNext.jsアプリケーションにMCPを実装することで、コード変更なしに新しい機能に動的に適応できるエージェントを作成できます。@vercel/mcp-handlerとLangchain、es-toolkitの組み合わせは、任意のMCP準拠サービスとシームレスに統合できる本番レディのサーバーレスAIシステムを構築するための強力な基盤を提供します。このプロトコルファーストのアプローチにより、AIツールのエコシステムが進化し続ける中で、あなたのエージェントが相互運用可能で将来性を保つことができます。