第 9 章 · 多步工具循环与 MCP
本章目标:
- 理解 Model Context Protocol (MCP) 的作用:通过标准化接口发现并使用外部服务的工具、资源与提示词
- 掌握
createMCPClient的三种 transport(HTTP / SSE / stdio)及各自适用场景- 学会用
mcpClient.tools()做 Schema Discovery 或显式 Schema Definition- 了解资源(Resources)、补全(Completions)与 Elicitation 机制
- 掌握工具定义漂移(rug pull)攻击的检测方法
9.1 什么是 Model Context Protocol
AI SDK 支持连接 Model Context Protocol (MCP) 服务器,以访问其工具、资源和提示词。这让你的 AI 应用能够通过标准化接口发现并使用各种服务的能力——一次接入,处处可用。
MCP 客户端同时支持旧的基于初始化的协议版本和新的无状态 MCP 2026-07-28 版本。内置 stdio transport 会先用 server/discover 探测,对旧服务器自动回退到 initialize 握手。
9.2 初始化 MCP Client
生产部署推荐使用 HTTP transport(如 StreamableHTTPClientTransport)。stdio transport 只能用于连接本地服务器,无法部署到生产环境。
HTTP Transport(推荐)
import { createMCPClient } from '@ai-sdk/mcp';
const mcpClient = await createMCPClient({
transport: {
type: 'http',
url: 'https://your-server.com/mcp',
// 可选:配置 HTTP headers
headers: { Authorization: 'Bearer my-api-key' },
// 可选:提供 OAuth client provider 以自动授权
authProvider: myOAuthClientProvider,
// 可选:允许重定向响应(默认 'error' 以防 SSRF)
redirect: 'follow',
},
});也可以使用 MCP 官方 TypeScript SDK 的 StreamableHTTPClientTransport:
import { createMCPClient } from '@ai-sdk/mcp';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const url = new URL('https://your-server.com/mcp');
const mcpClient = await createMCPClient({
transport: new StreamableHTTPClientTransport(url, {
sessionId: 'session_123',
}),
});SSE Transport
SSE 是另一种基于 HTTP 的 transport 选择,同样支持 headers 与 authProvider:
const mcpClient = await createMCPClient({
transport: {
type: 'sse',
url: 'https://my-server.com/sse',
headers: { Authorization: 'Bearer my-api-key' },
},
});Stdio Transport(仅限本地)
import { createMCPClient } from '@ai-sdk/mcp';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
// 或使用 AI SDK 自带的 stdio transport:
// import { Experimental_StdioMCPTransport as StdioClientTransport } from '@ai-sdk/mcp/mcp-stdio';
const mcpClient = await createMCPClient({
transport: new StdioClientTransport({
command: 'node',
args: ['src/stdio/dist/server.js'],
}),
});⚠️ stdio transport 只应用于本地服务器开发调试。
9.3 在生成中使用 MCP 工具
mcpClient.tools() 充当 MCP 工具与 AI SDK 工具之间的适配器。它支持两种方式:
Schema Discovery(自动发现):自动列出服务器提供的所有工具,并根据服务器提供的 schema 推断输入参数类型。简单且自动跟随服务器变化,但没有 TypeScript 类型安全,且会加载全部工具:
const tools = await mcpClient.tools();Schema Definition(显式定义):为获得更好的类型安全与控制力,在客户端代码中显式定义工具及其输入 schema:
import { z } from 'zod';
const tools = await mcpClient.tools({
schemas: {
'get-data': {
inputSchema: z.object({
query: z.string().describe('The data query'),
format: z.enum(['json', 'text']).optional(),
}),
},
// 无参数的工具应使用空对象:
'tool-with-no-args': {
inputSchema: z.object({}),
},
},
});显式定义后客户端只拉取你声明的工具,并获得完整的 IDE 自动补全。
类型化的工具输出
当 MCP 服务器返回 structuredContent 时(遵循 MCP 规范),可以定义 outputSchema 获得类型化结果:
import { z } from 'zod';
const tools = await mcpClient.tools({
schemas: {
'get-weather': {
inputSchema: z.object({
location: z.string(),
}),
// 定义 outputSchema 获得类型化结果
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
humidity: z.number(),
}),
},
},
});
const result = await tools['get-weather'].execute(
{ location: 'New York' },
{ messages: [], toolCallId: 'weather-1' },
);
console.log(`Temperature: ${result.temperature}°C`);提供 outputSchema 后:客户端从工具结果中提取 structuredContent、运行时按 schema 校验、结果具备完整类型安全。若服务器未返回 structuredContent,则回退为解析文本内容中的 JSON;两者都不可用或校验失败时抛错。
9.4 完整示例:流式生成中调用 MCP 工具
下面把 MCP 工具接入 streamText。模型既可以通过 Vercel AI Gateway 构造,也可以换成自定义 OpenAI 兼容 Provider(见第 3 章),两种写法对 MCP 部分完全透明:
import { streamText, createGateway } from 'ai';
import { createMCPClient } from '@ai-sdk/mcp';
// 方式一:Vercel AI Gateway
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const model = gateway('openai/gpt-5');
// 方式二:自定义 OpenAI 兼容 Provider(二选一即可)
// import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
// const myProvider = createOpenAICompatible({
// name: 'my-provider',
// baseURL: process.env.OPENAI_COMPATIBLE_BASE_URL ?? '',
// apiKey: process.env.OPENAI_COMPATIBLE_API_KEY ?? '',
// });
// const model = myProvider('gpt-4o-mini');
const mcpClient = await createMCPClient({
transport: {
type: 'http',
url: 'https://your-server.com/mcp',
},
});
const tools = await mcpClient.tools();
const result = streamText({
model,
tools,
prompt: 'What is the weather in Brooklyn, New York?',
onEnd: async () => {
await mcpClient.close();
},
});流式场景下可在 onEnd 回调里关闭客户端;非流式场景用 try/finally:
import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
let mcpClient: MCPClient | undefined;
try {
mcpClient = await createMCPClient({
// ...
});
} finally {
await mcpClient?.close();
}短生命周期的使用(如单次请求)应在响应结束后关闭客户端;长生命周期客户端保持打开,但确保应用终止时关闭。
瞬态失败重试
MCP 工具调用可能因瞬态原因失败(限流、临时过载、网关超时)。创建客户端时传入 maxRetries 即可对 tools/call 请求启用自动重试:
const mcpClient = await createMCPClient({
transport: {
type: 'http',
url: 'https://your-server.com/mcp',
},
maxRetries: 2,
});重试默认关闭。内置重试只针对瞬态 HTTP 与网络错误;JSON-RPC 应用层错误(如无效工具参数)立即抛出不重试;isError: true 的成功响应也直接返回给模型不重试。
⚠️ 仅对重试安全的 MCP 工具启用重试。重试非幂等工具(如发送邮件、创建记录)可能造成副作用重复执行。
9.5 资源、补全与提示词
资源(Resources) 是应用驱动的数据源——由你的应用决定何时获取并作为上下文传给模型(不同于由模型控制的工具)。MCP 客户端提供三个方法:
// 列出所有可用资源
const resources = await mcpClient.listResources();
// 按 URI 读取特定资源内容
const resourceData = await mcpClient.readResource({
uri: 'file:///example/document.txt',
});
// 列出可用的资源模板
const templates = await mcpClient.listResourceTemplates();补全(Completions):当服务器声明 completions 能力时,可根据当前部分参数值向服务器请求自动补全建议:
const completion = await mcpClient.complete({
ref: {
type: 'ref/resource',
uri: 'file:///{path}',
},
argument: {
name: 'path',
value: 'doc',
},
});
console.log(completion.completion.values);对于有多个参数的资源模板或提示词,可通过 context.arguments 传入已解析的值。若连接的服务器不支持 completions 能力,客户端抛出 MCPClientError。
提示词(Prompts) 是用户控制的模板(实验性功能):
// 列出提示词
const prompts = await mcpClient.experimental_listPrompts();
// 获取提示词消息,可传入服务器定义的参数
const prompt = await mcpClient.experimental_getPrompt({
name: 'code_review',
arguments: { code: 'function add(a, b) { return a + b; }' },
});9.6 处理 Elicitation 请求
Elicitation 是 MCP 服务器在工具执行期间向客户端请求额外信息的机制。例如服务器可能需要用户输入来完成注册表单,或对敏感操作进行确认。MCP 客户端只是把这些请求从服务器转发给你的应用代码,如何处理由你决定。
创建客户端时声明能力以启用:
const mcpClient = await createMCPClient({
transport: {
type: 'sse',
url: 'https://your-server.com/sse',
},
capabilities: {
elicitation: {},
},
});注册处理函数:
import { ElicitationRequestSchema } from '@ai-sdk/mcp';
mcpClient.onElicitationRequest(ElicitationRequestSchema, async request => {
// request.params.message: 描述需要什么输入
// request.params.requestedSchema: 定义预期输入结构的 JSON schema
const userInput = await getInputFromUser(
request.params.message,
request.params.requestedSchema,
);
return {
action: 'accept', // 或 'decline' 或 'cancel'
content: userInput, // 仅 action 为 'accept' 时必填
};
});处理函数必须返回带 action 字段的对象:'accept'(用户提供信息,需含 content)、'decline'(用户拒绝)、'cancel'(取消操作)。
9.7 检测工具定义漂移(rug pull)
MCP 服务器在你首次连接时发送工具定义(名称、描述、输入 schema),你通常会在此时审查批准。但协议并不阻止服务器稍后对同名工具返回不同的定义——比如描述中携带注入指令,或输入 schema 被加宽多出一个字段。由于 SDK 每次调用都会使用你传入的工具,后续 mcpClient.tools() 返回的变更定义会被不加比较地使用。这就是 MCP「rug pull」类攻击。
AI SDK 提供两个函数来固定已批准的定义并检测变化:fingerprintTools 把每个工具的安全相关字段(字符串 description、解析后的输入 schema 和 title)摘要成稳定的「工具名 → 摘要」映射;detectToolDrift 对比两个映射:
import { fingerprintTools, detectToolDrift } from 'ai';
// 信任时刻(首次连接、人工审核):捕获并持久化基线
const baseline = await fingerprintTools(await mcpClient.tools());
// 之后每次拉取,在把工具交给 generateText 之前:
const tools = await mcpClient.tools();
const drift = detectToolDrift(await fingerprintTools(tools), baseline);
if (drift.changed.length || drift.added.length) {
// 已固定的定义发生了变化,或出现了新工具。
// 按你的策略阻断、重新审批或告警——不要默默把 tools 传给模型。
}💡 它检测的是工具描述、输入 schema 或标题的篡改——即 prompt injection 和 schema 加宽向量。无法检测名称、描述、schema 均不变但行为/端点被替换的情况,因为工具在 MCP 服务器上远程运行,这种变化对客户端不可见。SDK 不负责持久化基线或阻断调用——这些是你的应用的责任。
本章小结
- MCP 让 AI 应用通过标准接口使用外部服务的工具、资源与提示词;
createMCPClient支持 HTTP(推荐)、SSE 与 stdio 三种 transport mcpClient.tools()支持 Schema Discovery(自动同步但无类型)与 Schema Definition(显式声明、全类型安全)- 通过
outputSchema可以获得经运行时校验的类型化工具输出 - 流式场景在
onEnd中关闭客户端,非流式用 try/finally;maxRetries可对瞬态错误自动重试,但非幂等工具慎用 - 资源由应用驱动读取,Elicitation 由应用处理服务器发起的信息请求
- 用
fingerprintTools+detectToolDrift固定基线,防御 rug pull 工具定义漂移攻击
🛠️ 动手实践
- 用
@ai-sdk/mcp连接一个公开的 MCP 服务器(HTTP transport),分别用 Schema Discovery 和 Schema Definition 两种方式加载工具,对比 IDE 中的类型提示差异。 - 给一个本地 stdio MCP 服务器编写调用代码:用
streamText+onEnd关闭客户端,再改造成 try/finally 的非流式版本。 - 实现一个最小 rug pull 防护:首次连接时保存
fingerprintTools基线到 JSON 文件,之后每次启动时用detectToolDrift对比并在检测到漂移时打印告警。