草案 智能体设计模式 - 模型上下文协议 (MCP)

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

学习如何实现模型上下文协议 (MCP),在 Vercel 无服务器平台上的 Next.js 15 中创建标准化、可互操作的 AI 智能体,能够动态发现并与外部工具、服务和数据源交互。

思维模型: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: '创建一个具有指定详细信息的新任务',
    inputSchema: {
      type: 'object',
      properties: {
        title: { type: 'string', description: '任务标题' },
        priority: {
          type: 'number',
          minimum: 1,
          maximum: 5,
          description: '优先级 (1-5)'
        },
        assignee: {
          type: 'string',
          description: '可选的负责人姓名'
        },
      },
      required: ['title', 'priority'],
    },
  },
  {
    name: 'list_tasks',
    description: '列出所有任务并支持可选过滤',
    inputSchema: {
      type: 'object',
      properties: {
        status: {
          type: 'string',
          enum: ['pending', 'in-progress', 'completed'],
          description: '按状态过滤'
        },
        assignee: {
          type: 'string',
          description: '按负责人过滤'
        },
      },
    },
  },
  {
    name: 'update_task_status',
    description: '更新现有任务的状态',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string', description: '任务 ID' },
        status: {
          type: 'string',
          enum: ['pending', 'in-progress', 'completed'],
          description: '新状态'
        },
      },
      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(`任务 ${args.id} 未找到`);
      }
      task.status = args.status;
      tasks.set(args.id, task);
      return { success: true, task };
    }

    default:
      throw new Error(`未知工具: ${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 instanceof Error ? error.message : '未知错误'}`,
          },
        ],
        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;

创建一个 Vercel 兼容的 API 路由,通过无服务器端点公开 MCP 服务器。

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: '文档库',
    description: '访问系统中的所有文档',
    mimeType: 'application/json',
  },
  {
    uri: 'analytics://events',
    name: '分析事件',
    description: '分析事件流',
    mimeType: 'application/x-ndjson',
  },
  {
    uri: 'config://settings',
    name: '系统配置',
    description: '当前系统设置和配置',
    mimeType: 'application/json',
  },
];

// 增强工具定义
export const advancedTools: Tool[] = [
  {
    name: 'analyze_documents',
    description: '对文档执行语义分析',
    inputSchema: {
      type: 'object',
      properties: {
        documentIds: {
          type: 'array',
          items: { type: 'string' },
          description: '要分析的文档 ID',
        },
        analysisType: {
          type: 'string',
          enum: ['sentiment', 'summary', 'keywords', 'entities'],
          description: '要执行的分析类型',
        },
        options: {
          type: 'object',
          properties: {
            maxLength: { type: 'number' },
            threshold: { type: 'number' },
          },
        },
      },
      required: ['documentIds', 'analysisType'],
    },
  },
  {
    name: 'process_batch',
    description: '并行批量处理多个项目',
    inputSchema: {
      type: 'object',
      properties: {
        items: {
          type: 'array',
          description: '要处理的项目',
        },
        batchSize: {
          type: 'number',
          default: 10,
          description: '每批次的大小',
        },
        operation: {
          type: 'string',
          enum: ['transform', 'validate', 'enrich'],
        },
      },
      required: ['items', 'operation'],
    },
  },
];

// 常用操作的提示模板
export const prompts: Prompt[] = [
  {
    name: 'document_qa',
    description: '基于文档上下文的问答',
    arguments: [
      {
        name: 'question',
        description: '要回答的问题',
        required: true,
      },
      {
        name: 'context',
        description: '用于回答的文档上下文',
        required: true,
      },
    ],
  },
  {
    name: 'data_synthesis',
    description: '从多个数据源综合见解',
    arguments: [
      {
        name: 'sources',
        description: '要综合的数据源',
        required: true,
      },
      {
        name: 'format',
        description: '输出格式(报告、摘要、要点)',
        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('处理事件批次:', 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(`未知资源: ${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(`未知工具: ${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(`未知提示: ${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: '分析完成' };
        }
      })
    );

    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 || '摘要';
        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('未授权: 未提供令牌');
    }

    const valid = await verifyToken(token);
    if (!valid) {
      throw new Error('未授权: 令牌无效');
    }

    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 客户端未初始化');

      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 客户端未初始化');

      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('无效的 JSON 输入:', 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>
  );
}

创建一个全面的 React 界面,用于与 MCP 服务器交互、发现功能和执行工具。

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 // 每个资源最多每秒一次
  );

  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(`连接到 ${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(`连接到 ${toolCount} 个跨 MCP 服务器的工具`);

  const result = await agent.execute(message);

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

实现一个动态 MCP 智能体,可以同时发现和使用来自多个 MCP 服务器的工具。

结论

模型上下文协议 (MCP) 通过为工具、资源和提示提供标准化、可发现的接口,改变了 AI 智能体与外部系统的交互方式。通过在 Vercel 上的 Next.js 应用程序中实现 MCP,您可以创建能够动态适应新功能而无需代码更改的智能体。@vercel/mcp-handler 与 Langchain 和 es-toolkit 的结合为构建生产就绪的无服务器 AI 系统提供了强大的基础,这些系统可以与任何符合 MCP 的服务无缝集成。这种协议优先的方法确保您的智能体在 AI 工具生态系统持续发展的过程中保持互操作性和面向未来。