// ============================================ // 1. MCP Client 实现 // ============================================
import { z } from 'zod'; import { tool } from 'ai'; import { streamText } from 'ai'; import { openai } from '@ai-sdk/openai';
/**
- MCP Tool 的定义(来自 MCP Server)
-
- 为什么需要这个类型?
- - MCP 协议定义了 tool 的标准格式
- - 我们需要明确知道从 MCP Server 返回的数据结构 */ interface MCPTool { name: string; description: string; inputSchema: { type: 'object'; properties: Record<string, any>; required?: string[]; }; }
/**
- MCP Resource 的定义(文档/知识库)
-
- 为什么需要 Resource?
- - 有些问答需要基于文档内容(类似 RAG 的 context)
- - MCP 允许 Server 提供可读取的资源(文档、配置等) */ interface MCPResource { uri: string; name: string; description?: string; mimeType?: string; }
/**
- MCP Client 类
-
- 为什么要封装成类?
- - 保持连接状态(连接一次,多次调用)
- - 统一管理 tools 和 resources
- - 方便错误处理和重连逻辑 */ class MCPClient { private baseUrl: string; private connected: boolean = false;
constructor(serverUrl: string) { this.baseUrl = serverUrl; }
/**
- 连接到 MCP Server
-
- 为什么需要显式连接?
- - 验证服务是否可用(fail-fast 原则)
- - 可以在连接时做初始化(如身份验证)
- - 对于 stdio 类型的 MCP,这里会启动子进程 */ async connect(): Promise { try { // 对于 HTTP 类型的 MCP Server,发送 ping 请求验证连接 const response = await fetch(`${this.baseUrl}/health`, { method: 'GET', });
if (!response.ok) { throw new Error(`MCP Server 不可用: ${response.statusText}`); }
this.connected = true; console.log('✅ MCP Server 连接成功'); } catch (error) { throw new Error(`连接 MCP Server 失败: ${error}`); } }
/**
- 获取 MCP Server 提供的所有 tools
-
- 为什么用 listTools 而不是自动发现?
- - MCP 协议规定:Client 必须先询问 Server 有哪些 tools
- - 这是显式契约,避免 Client 调用不存在的 tool
- - 类似 OpenAPI 的 schema discovery */ async listTools(): Promise<MCPTool[]> { if (!this.connected) { throw new Error('请先调用 connect() 连接到 MCP Server'); }
try {
const response = await fetch(`${this.baseUrl}/tools/list`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const data = await response.json();
// MCP 协议规定返回格式为 { tools: [...] }
return data.tools || [];
} catch (error) {
throw new Error(`获取 tools 列表失败: ${error}`);
}
}
/**
- 调用指定的 tool
-
- 为什么需要这个方法?
- - 这是实际执行 tool 的地方
- - 会被 Vercel AI SDK 的 tool.execute 调用
- - 封装了网络请求、错误处理、重试逻辑
-
- @param toolName - tool 的名称(必须在 listTools 返回的列表中)
- @param args - tool 的参数(必须符合 inputSchema) */ async callTool(toolName: string, args: Record<string, any>): Promise { if (!this.connected) { throw new Error('请先调用 connect() 连接到 MCP Server'); }
try {
const response = await fetch(`${this.baseUrl}/tools/call`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: toolName,
arguments: args,
}),
});
if (!response.ok) {
throw new Error(`Tool 调用失败: ${response.statusText}`);
}
const result = await response.json();
// MCP 协议规定返回格式为 { content: [...] }
// content 是一个数组,可能包含多个部分(文本、图片等)
// 这里我们简化处理,只取第一个文本内容
return result.content?.[0]?.text || result;
} catch (error) {
throw new Error(`调用 tool "${toolName}" 失败: ${error}`);
}
}
/**
- 获取 MCP Server 提供的所有 resources(文档/知识库)
-
- 为什么需要 resources?
- - 某些问答需要基于文档回答(如"使用手册是什么")
- - Resources 可以是静态文档、配置文件、API 文档等
- - 类似 RAG 中的 knowledge base,但由 MCP Server 管理 */ async listResources(): Promise<MCPResource[]> { if (!this.connected) { throw new Error('请先调用 connect() 连接到 MCP Server'); }
try {
const response = await fetch(`${this.baseUrl}/resources/list`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
const data = await response.json();
return data.resources || [];
} catch (error) {
throw new Error(`获取 resources 列表失败: ${error}`);
}
}
/**
- 读取指定 resource 的内容
-
- 为什么分成 list 和 read 两步?
- - list 只返回元数据(名称、描述),数据量小
- - read 返回完整内容,可能很大(几 MB 的文档)
- - 按需读取,节省带宽和内存
-
- @param uri - resource 的唯一标识符(来自 listResources) */ async readResource(uri: string): Promise { if (!this.connected) { throw new Error('请先调用 connect() 连接到 MCP Server'); }
try {
const response = await fetch(`${this.baseUrl}/resources/read`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uri }),
});
const data = await response.json();
// MCP 协议规定返回格式为 { contents: [...] }
return data.contents?.[0]?.text || '';
} catch (error) {
throw new Error(`读取 resource "${uri}" 失败: ${error}`);
}
}
/**
- 断开连接
-
- 为什么需要?
- - 释放资源(对于 stdio 类型,需要关闭子进程)
- - 良好的编程习惯(显式管理生命周期) */ async disconnect(): Promise { this.connected = false; console.log('✅ MCP Server 连接已断开'); } }
// ============================================ // 2. 格式转换:MCP Tool → Vercel AI SDK Tool // ============================================
/**
- 将 MCP 的 JSON Schema 转换为 Zod Schema
-
- 为什么需要转换?
- - MCP 使用 JSON Schema 描述参数(标准、通用)
- - Vercel AI SDK 使用 Zod 做类型校验(TypeScript 友好)
- - 需要在两种格式间转换
-
- 这是简化版实现,只处理常见类型
- 生产环境可以用 json-schema-to-zod 等库 */ function jsonSchemaToZod(schema: any): z.ZodObject { const shape: Record<string, z.ZodTypeAny> = {};
// 遍历 JSON Schema 的每个属性 for (const [key, value] of Object.entries(schema.properties || {})) { const prop = value as any;
// 根据 JSON Schema 的 type 转换为对应的 Zod 类型
let zodType: z.ZodTypeAny;
switch (prop.type) {
case 'string':
zodType = z.string();
break;
case 'number':
zodType = z.number();
break;
case 'boolean':
zodType = z.boolean();
break;
case 'array':
zodType = z.array(z.any());
break;
case 'object':
zodType = z.object({});
break;
default:
zodType = z.any();
}
// 如果有 description,添加到 Zod schema(会显示在 LLM 提示中)
if (prop.description) {
zodType = zodType.describe(prop.description);
}
// 如果不在 required 列表中,标记为可选
if (!schema.required?.includes(key)) {
zodType = zodType.optional();
}
shape[key] = zodType;
}
return z.object(shape); }
/**
- 将 MCP Tool 转换为 Vercel AI SDK 的 tool
-
- 为什么需要这个函数?
- - 这是整个适配层的核心
- - 把 MCP 的 tool 定义"翻译"成 Vercel AI SDK 能理解的格式
- - 封装了实际的 tool 调用逻辑
-
- @param mcpTool - MCP Server 返回的 tool 定义
- @param mcpClient - 用于实际调用 tool 的 client 实例 */ function convertMCPToolToVercelTool(mcpTool: MCPTool, mcpClient: MCPClient) { return tool({ // tool 的名称(LLM 会看到这个名字) name: mcpTool.name,
// tool 的描述(LLM 根据这个决定是否调用) // 为什么描述很重要? // - LLM 通过描述理解 tool 的功能 // - 描述越清晰,LLM 调用越准确 description: mcpTool.description,
// 参数的类型定义(用 Zod 校验) // 为什么用 Zod? // - 运行时类型校验(防止 LLM 传错参数) // - TypeScript 类型推导(开发时类型安全) // - 自动生成参数文档(传给 LLM) parameters: jsonSchemaToZod(mcpTool.inputSchema),
// 实际执行 tool 的函数 // 为什么是 async? // - tool 调用通常涉及 I/O(网络请求、数据库查询) // - 异步执行不阻塞主线程 execute: async (args) => { console.log(`🔧 执行 tool: ${mcpTool.name}`, args);
// 调用 MCP Server 执行 tool const result = await mcpClient.callTool(mcpTool.name, args);
console.log(`✅ Tool 执行结果:`, result);
return result; }, }); }
// ============================================ // 3. 集成到 Vercel AI SDK // ============================================
/**
- 创建一个带 MCP Tools 的 AI 会话
-
- 这是整个流程的入口函数
-
- 为什么要封装成函数?
- - 隐藏复杂的初始化逻辑
- - 自动处理 MCP 连接、tool 转换、资源加载
- - 提供简洁的 API 给业务代码使用 */ async function createAIWithMCPTools( mcpServerUrl: string, userMessage: string, options?: { includeResources?: boolean; // 是否加载 MCP resources systemPrompt?: string; // 自定义 system prompt } ) { // 1. 连接到 MCP Server console.log('📡 正在连接 MCP Server...'); const mcpClient = new MCPClient(mcpServerUrl); await mcpClient.connect();
// 2. 获取所有可用的 tools console.log('🔍 正在获取 MCP Tools...'); const mcpTools = await mcpClient.listTools(); console.log(`📦 发现 ${mcpTools.length} 个 tools:`, mcpTools.map(t => t.name));
// 3. 转换为 Vercel AI SDK 格式 // 为什么要 reduce? // - 转换成 { toolName: tool } 的对象格式 // - Vercel AI SDK 的 tools 参数要求是对象,不是数组 const vercelTools = mcpTools.reduce((acc, mcpTool) => { acc[mcpTool.name] = convertMCPToolToVercelTool(mcpTool, mcpClient); return acc; }, {} as Record<string, any>);
// 4. 构建 messages(包含 system prompt 和 user message) const messages: any[] = [];
// 4.1 如果需要加载 resources,添加到 system prompt if (options?.includeResources) { console.log('📚 正在加载 MCP Resources...'); const resources = await mcpClient.listResources();
if (resources.length > 0) {
// 读取第一个 resource 的内容(实际应用中可能读取多个)
const resourceContent = await mcpClient.readResource(resources[0].uri);
// 为什么放在 system prompt?
// - system prompt 是 AI 的"背景知识"
// - 让 AI 知道有哪些文档可以参考
// - 类似 RAG 中把 retrieved context 放在 system message
messages.push({
role: 'system',
content: `参考文档:\n\n${resourceContent}\n\n${options.systemPrompt || ''}`,
});
console.log(`📄 已加载 resource: ${resources[0].name}`);
}
} else if (options?.systemPrompt) { messages.push({ role: 'system', content: options.systemPrompt, }); }
// 4.2 添加用户消息 messages.push({ role: 'user', content: userMessage, });
// 5. 调用 Vercel AI SDK console.log('🤖 正在调用 AI...\n');
const result = await streamText({ model: openai('gpt-4o'), // 使用支持 function calling 的模型
// 会话消息(system + user)
messages,
// 可用的 tools
// 为什么传 tools?
// - Vercel AI SDK 会自动处理 tool calling 循环
// - LLM 根据用户问题决定是否调用 tool
// - 调用后结果会自动添加到对话历史,继续生成
tools: vercelTools,
// 允许最多 5 轮 tool calling
// 为什么需要限制?
// - 防止死循环(LLM 一直调用 tool)
// - 控制成本(每次调用都消耗 token)
maxToolRoundtrips: 5,
});
// 6. 流式输出结果 // 为什么用流式? // - 用户体验好(逐字显示,不用等待) // - 降低首字节时间(TTFB) for await (const textPart of result.textStream) { process.stdout.write(textPart); }
console.log('\n');
// 7. 清理资源 await mcpClient.disconnect(); }
// ============================================ // 4. 使用示例 // ============================================
/**
- 示例 1:调用 tool 查询用户信息
-
- 这个示例演示:
- - LLM 自动识别需要调用 tool
- - 自动提取参数(userId: "002")
- - 调用 MCP Server 的 query_user tool
- - 将结果整合到回答中 */ async function example1() { console.log('=== 示例 1:Tool Calling ===\n');
await createAIWithMCPTools( '[http://localhost:3000](http://localhost:3000/)', // MCP Server 地址 '查一下用户002的信息', // 用户问题 { systemPrompt: '你是一个友好的助手,可以查询用户信息。', } ); }
/**
- 示例 2:基于文档回答问题
-
- 这个示例演示:
- - 从 MCP Server 加载 resource(文档)
- - 将文档内容注入到 system prompt
- - LLM 基于文档内容回答问题
- - 类似 RAG,但文档由 MCP 管理 */ async function example2() { console.log('=== 示例 2:Resource + 文档问答 ===\n');
await createAIWithMCPTools( '[http://localhost:3000](http://localhost:3000/)', 'MCP Server 的使用指南是什么?', { includeResources: true, // 加载 resources systemPrompt: '你是一个技术文档助手,基于提供的文档回答问题。', } ); }
/**
- 示例 3:复杂对话(多次 tool calling)
-
- 这个示例演示:
- - LLM 可能多次调用 tool
- - 自动处理 tool calling 循环
- - 最终综合所有信息给出回答 */ async function example3() { console.log('=== 示例 3:多次 Tool Calling ===\n');
await createAIWithMCPTools( '[http://localhost:3000](http://localhost:3000/)', '比较用户001和用户002的角色有什么不同?', { systemPrompt: '你是一个数据分析助手,可以查询和比较用户信息。', } ); }
// ============================================ // 5. 运行示例 // ============================================
// 运行某个示例(取消注释想运行的示例) // example1(); // example2(); // example3();
/**
- 关键要点总结:
-
- 1. MCP 是什么?
- - 一个协议,用于描述和调用 AI Tools
- - 类似 OpenAPI 之于 REST API
-
- 2. 为什么不直接用 LangChain?
- - 代码更简洁(100 行 vs 多层抽象)
- - 完全控制每一步(易于调试和优化)
- - 更轻量(只依赖 Vercel AI SDK + Zod)
-
- 3. 核心流程:
- MCP Server → MCP Client → 格式转换 → Vercel AI SDK → LLM
-
- 4. 你需要实现的:
- - MCPClient 类(连接、获取、调用)
- - convertMCPToolToVercelTool 函数(格式转换)
- - 其余都是 Vercel AI SDK 自动处理
-
- 5. 生产环境建议:
- - 添加错误重试逻辑
- - 添加 tool 调用日志(监控、debug)
- - 缓存 tool 列表(避免重复请求)
- - 支持 stdio 类型的 MCP Server
- - 使用 json-schema-to-zod 库做更完善的转换 */