Skip to content

第 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(推荐)

ts
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

ts
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 选择,同样支持 headersauthProvider

ts
const mcpClient = await createMCPClient({
  transport: {
    type: 'sse',
    url: 'https://my-server.com/sse',
    headers: { Authorization: 'Bearer my-api-key' },
  },
});

Stdio Transport(仅限本地)

ts
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 类型安全,且会加载全部工具:

ts
const tools = await mcpClient.tools();

Schema Definition(显式定义):为获得更好的类型安全与控制力,在客户端代码中显式定义工具及其输入 schema:

ts
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 获得类型化结果:

ts
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 部分完全透明:

ts
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:

ts
import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';

let mcpClient: MCPClient | undefined;

try {
  mcpClient = await createMCPClient({
    // ...
  });
} finally {
  await mcpClient?.close();
}

短生命周期的使用(如单次请求)应在响应结束后关闭客户端;长生命周期客户端保持打开,但确保应用终止时关闭。

瞬态失败重试

MCP 工具调用可能因瞬态原因失败(限流、临时过载、网关超时)。创建客户端时传入 maxRetries 即可对 tools/call 请求启用自动重试:

ts
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 客户端提供三个方法:

ts
// 列出所有可用资源
const resources = await mcpClient.listResources();

// 按 URI 读取特定资源内容
const resourceData = await mcpClient.readResource({
  uri: 'file:///example/document.txt',
});

// 列出可用的资源模板
const templates = await mcpClient.listResourceTemplates();

补全(Completions):当服务器声明 completions 能力时,可根据当前部分参数值向服务器请求自动补全建议:

ts
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) 是用户控制的模板(实验性功能):

ts
// 列出提示词
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 客户端只是把这些请求从服务器转发给你的应用代码,如何处理由你决定。

创建客户端时声明能力以启用:

ts
const mcpClient = await createMCPClient({
  transport: {
    type: 'sse',
    url: 'https://your-server.com/sse',
  },
  capabilities: {
    elicitation: {},
  },
});

注册处理函数:

ts
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 对比两个映射:

ts
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 工具定义漂移攻击

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 生产环境中推荐使用哪种 MCP transport?

2. Schema Discovery 相比 Schema Definition 的主要缺点是什么?

3. 为 MCP 工具定义 outputSchema 后,客户端的行为是?

4. 关于 rug pull 攻击的检测,下列说法正确的是?

🛠️ 动手实践

  1. @ai-sdk/mcp 连接一个公开的 MCP 服务器(HTTP transport),分别用 Schema Discovery 和 Schema Definition 两种方式加载工具,对比 IDE 中的类型提示差异。
  2. 给一个本地 stdio MCP 服务器编写调用代码:用 streamText + onEnd 关闭客户端,再改造成 try/finally 的非流式版本。
  3. 实现一个最小 rug pull 防护:首次连接时保存 fingerprintTools 基线到 JSON 文件,之后每次启动时用 detectToolDrift 对比并在检测到漂移时打印告警。