Skip to content

第5章 Tool:让 Agent 调用世界

如果说 Resource 让大模型拥有了"读"的能力,那么 Tool 就是赋予大模型"做"的能力。一个只能阅读文件的 Agent 是被动的信息消费者,而一个能够调用数据库查询、发送 HTTP 请求、操作文件系统的 Agent,才真正具备了改变世界的能力。

Tool 是 MCP 协议三大原语中最具"动作性"的一个。在协议设计中,它被明确定义为 model-controlled ——由大模型自主决定何时调用、调用哪个工具、传入什么参数。这与 Resource 的 application-controlled、Prompt 的 user-controlled 形成了清晰的控制权分层。理解 Tool 的协议设计与实现机制,是构建可靠 Agent 系统的关键一步。

5.1 Tool 在 MCP 中的定位

在 MCP 的架构中,Tool 代表的是 Server 端暴露给大模型的可调用函数。当用户向 Agent 提出一个需要外部操作的需求时(比如"查一下北京今天的天气"),大模型会自主分析上下文,从已注册的 Tool 列表中选择合适的工具,构造参数并发起调用。

这个过程中有一个关键的信任链条:

在这个流程中,有几个值得注意的设计原则:

  1. 大模型做选择,Client 做守门。大模型决定调用哪个 Tool、传什么参数,但 Client 应该(SHOULD)在实际发送 tools/call 之前向用户展示确认提示,确保 human-in-the-loop。
  2. Server 做执行,协议做规范。Server 负责实际执行工具逻辑并返回结果,但返回的格式严格遵循协议定义的 CallToolResult 结构。
  3. 工具是声明式的。每个 Tool 通过 JSON Schema 声明自己接受什么参数,大模型据此构造合法的调用请求。

5.2 Tool 的定义结构

一个 MCP Tool 的完整定义包含以下字段:

字段类型必需说明
namestring工具的唯一标识符,1-128 字符
titlestring人类可读的显示名称
descriptionstring功能描述,帮助大模型理解何时使用
inputSchemaJSON Schema输入参数的 JSON Schema 定义
outputSchemaJSON Schema输出结构的 JSON Schema 定义
annotationsToolAnnotations行为标注(只读、破坏性等)

工具名称的命名规范值得特别关注。协议规定名称应仅包含 A-Za-z0-9_-. 这些字符,区分大小写,不应包含空格或特殊字符。这是因为工具名称会直接出现在 JSON-RPC 的请求参数中,过于复杂的名称容易导致解析问题。合法的例子包括 getUserDATA_EXPORT_v2admin.tools.list

下面是一个典型的 Tool 定义:

json
{
  "name": "get_weather",
  "title": "天气查询",
  "description": "查询指定城市的当前天气信息",
  "inputSchema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "城市名称或邮政编码"
      }
    },
    "required": ["location"]
  },
  "annotations": {
    "readOnlyHint": true,
    "openWorldHint": true
  }
}

inputSchema 必须是一个合法的 JSON Schema 对象,不能为 null。对于没有参数的工具,推荐使用 { "type": "object", "additionalProperties": false },明确表示只接受空对象。默认遵循 JSON Schema 2020-12 标准,也可以通过 $schema 字段显式指定其他版本。

5.3 Tool 的生命周期

Tool 的完整生命周期包括三个阶段:发现、调用、变更通知。

5.3.1 发现阶段:tools/list

Client 通过发送 tools/list 请求来发现 Server 上注册的所有工具。这个请求支持分页,通过 cursor 参数实现大量工具的分批获取:

json
// 请求
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }

// 响应
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "tools": [
      {
        "name": "get_weather",
        "description": "查询天气信息",
        "inputSchema": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] }
      }
    ],
    "nextCursor": "page2"
  }
}

5.3.2 调用阶段:tools/call

当大模型决定调用某个工具时,Client 发送 tools/call 请求,包含工具名和参数:

json
// 请求
{
  "jsonrpc": "2.0", "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "北京" }
  }
}

// 成功响应
{
  "jsonrpc": "2.0", "id": 2,
  "result": {
    "content": [
      { "type": "text", "text": "北京当前天气:25°C,晴转多云,湿度 45%" }
    ],
    "isError": false
  }
}

5.3.3 变更通知:tools/list_changed

当 Server 动态增加或移除了工具时,如果在初始化时声明了 listChanged: true 能力,就应该发送通知让 Client 重新拉取工具列表:

json
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }

这个机制使得工具集可以是动态的——比如一个插件系统中,用户安装新插件后,对应的工具就会被注册并通过通知告知 Client。

5.4 SDK 中的 Tool 注册机制

5.4.1 TypeScript SDK:registerTool 方法

TypeScript SDK 中,McpServer 类提供了 registerTool 方法来注册工具。让我们看一个完整的注册示例:

typescript
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';

const server = new McpServer({ name: 'weather-server', version: '1.0.0' });

server.registerTool(
  'get_weather',
  {
    title: '天气查询',
    description: '查询指定城市的当前天气信息',
    inputSchema: z.object({
      location: z.string().describe('城市名称或邮政编码')
    }),
    annotations: {
      readOnlyHint: true,
      openWorldHint: true
    }
  },
  async ({ location }) => {
    const weather = await fetchWeather(location);
    return {
      content: [{ type: 'text', text: `${location}${weather.temp}°C` }]
    };
  }
);

在 SDK 内部,registerTool 调用了私有方法 _createRegisteredTool,这个方法完成了几个关键步骤:

  1. 名称校验:调用 validateAndWarnToolName(name) 检查名称是否符合规范。
  2. Schema 转换:通过 standardSchemaToJsonSchema() 将 Zod schema 等标准 schema 格式转换为 JSON Schema。
  3. 注册请求处理器:首次注册工具时,调用 setToolRequestHandlers()tools/listtools/call 设置处理逻辑。
  4. 发送变更通知:调用 sendToolListChanged() 通知已连接的 Client。

注册后的工具对象 RegisteredTool 还提供了动态管理能力——enable()disable()remove()update() 方法让工具可以在运行时被启用、禁用、移除或更新配置。

5.4.2 Python SDK:@tool 装饰器

Python SDK 采用了更符合 Python 风格的装饰器模式:

python
from mcp.server.mcpserver.server import MCPServer
from mcp.types import ToolAnnotations

server = MCPServer(name="weather-server")

@server.tool(
    description="查询指定城市的当前天气信息",
    annotations=ToolAnnotations(
        readOnlyHint=True,
        openWorldHint=True
    )
)
async def get_weather(location: str) -> str:
    """查询天气信息"""
    weather = await fetch_weather(location)
    return f"{location}{weather['temp']}°C"

Python SDK 的 @tool 装饰器内部做了什么?从源码可以看到完整的流程:

  1. 函数元数据提取:调用 func_metadata(fn) 解析函数签名,自动生成 Pydantic 模型作为参数校验的基础。
  2. JSON Schema 生成:通过 model_json_schema() 从 Pydantic 模型自动生成 JSON Schema,作为工具的 inputSchema
  3. Context 注入检测:通过 find_context_parameter(fn) 扫描函数签名,如果发现类型标注为 Context 的参数,就标记为 context 注入点,在调用时自动注入上下文对象。
  4. 工具注册:通过 ToolManager.add_tool() 将构造好的 Tool 对象注册到管理器中。

Python SDK 中一个巧妙的设计是 Context 注入。开发者只需在函数签名中添加一个 ctx: Context 参数,SDK 就会自动将 MCP 上下文注入,无需额外配置:

python
@server.tool()
async def search_and_log(query: str, ctx: Context) -> str:
    await ctx.info(f"正在搜索:{query}")
    await ctx.report_progress(50, 100)
    result = await do_search(query)
    return result

5.4.3 两套 SDK 的设计对比

两者的核心差异在于 Schema 的来源:TypeScript SDK 依赖开发者显式传入 Zod 等 Standard Schema 格式;Python SDK 则利用 Python 的类型标注 + Pydantic 自动推导。最终两者都会将 Schema 转换为标准的 JSON Schema 格式注册到协议层。

5.5 输入校验机制

输入校验是 Tool 安全执行的第一道防线。SDK 在收到 tools/call 请求后、执行实际处理函数之前,会对 arguments 进行 Schema 校验。

TypeScript SDK 的校验逻辑在 validateToolInput 方法中:

typescript
private async validateToolInput(tool, args, toolName) {
    if (!tool.inputSchema) {
        return undefined;  // 无 schema 则跳过校验
    }
    const parseResult = await validateStandardSchema(tool.inputSchema, args ?? {});
    if (!parseResult.success) {
        throw new ProtocolError(
            ProtocolErrorCode.InvalidParams,
            `Input validation error: Invalid arguments for tool ${toolName}: ${parseResult.error}`
        );
    }
    return parseResult.data;
}

Python SDK 的校验发生在 Tool.run() 方法中,通过 call_fn_with_arg_validation 完成:

python
async def run(self, arguments, context, convert_result=False):
    try:
        result = await self.fn_metadata.call_fn_with_arg_validation(
            self.fn, self.is_async, arguments,
            {self.context_kwarg: context} if self.context_kwarg else None,
        )
        return result
    except Exception as e:
        raise ToolError(f"Error executing tool {self.name}: {e}") from e

Python SDK 利用 Pydantic 的 model_validate 进行校验,这意味着不仅能检查类型,还能执行自定义验证器(validator)。如果参数校验失败,两个 SDK 都会抛出带有 InvalidParams 错误码的 ProtocolError,最终以 JSON-RPC 错误响应的形式返回给 Client。

5.6 Tool Annotations:Agent 安全的元数据体系

Tool Annotations 是 MCP 协议中一个精心设计的安全机制。它为每个工具提供了行为层面的元数据标注,让 Client 和 Agent 框架能在调用工具之前就了解其潜在影响。

标注字段默认值含义典型场景
readOnlyHintfalse工具是否不修改环境查询类:数据库 SELECT、API GET
destructiveHinttrue工具是否可能执行破坏性操作删除数据、覆盖文件、发送不可撤回消息
idempotentHintfalse相同参数重复调用是否无副作用PUT 操作、幂等更新
openWorldHinttrue工具是否与开放世界交互Web 搜索(开放)vs 内存工具(封闭)

这些标注为什么对 Agent 安全如此重要?考虑以下场景:

场景一:自动重试策略。当一个 Tool 调用因网络超时失败时,Agent 是否应该自动重试?如果 idempotentHint: true,重试是安全的;如果 idempotentHint: falsedestructiveHint: true(比如"发送邮件"工具),盲目重试可能导致重复发送。

场景二:确认提示策略。一个标记了 readOnlyHint: true 的工具(如数据库查询)可以在无需用户确认的情况下自动执行;而标记了 destructiveHint: true 的工具(如删除文件)则应该弹出确认对话框。

场景三:并行执行策略。多个 readOnlyHint: true 的工具可以安全地并行调用以提升响应速度;而 destructiveHint: true 的工具通常应该串行执行,避免竞态条件。

协议特别强调:这些标注只是"提示"(hint),不是"保证"。来自不受信任的 Server 的标注不应被盲目采信。一个恶意 Server 完全可以将一个破坏性工具标记为 readOnlyHint: true 来绕过确认提示。因此,Client 实现必须结合 Server 的信任等级来决策。

5.7 错误处理的双层设计

MCP Tool 的错误处理分为两个层次,这个设计深刻影响了 Agent 的错误恢复策略。

协议层错误(Protocol Errors)

以标准 JSON-RPC error 形式返回,表示请求本身有问题:

json
{
  "jsonrpc": "2.0", "id": 3,
  "error": {
    "code": -32602,
    "message": "Unknown tool: invalid_tool_name"
  }
}

典型原因包括:调用了不存在的工具、请求格式不合法、Server 内部错误。这类错误通常意味着大模型"选错了工具"或者"构造了错误的请求",重试价值有限。

业务层错误(Tool Execution Errors)

以正常的 CallToolResult 形式返回,但 isError 字段为 true

json
{
  "jsonrpc": "2.0", "id": 4,
  "result": {
    "content": [
      { "type": "text", "text": "日期格式错误:请使用 YYYY-MM-DD 格式" }
    ],
    "isError": true
  }
}

这类错误的关键特征是包含可操作的反馈信息。大模型读到"日期格式错误"后可以自行修正参数格式重新调用,这构成了 Agent 的自我纠错循环。协议规定 Client 应该(SHOULD)将业务层错误传递给大模型以启用自我修正;对于协议层错误,Client 可以(MAY)传递给大模型,但恢复成功率通常较低。

在 TypeScript SDK 中,这种双层设计体现得很清晰。tools/call 处理器中,如果工具执行抛出异常(非 ProtocolError),SDK 会自动将其包装为 isError: true 的业务层错误:

typescript
// TypeScript SDK 内部实现
try {
    const args = await this.validateToolInput(tool, request.params.arguments, name);
    const result = await this.executeToolHandler(tool, args, ctx);
    return result;
} catch (error) {
    if (error instanceof ProtocolError) {
        throw error;  // 协议层错误直接抛出
    }
    // 其他异常包装为业务层错误
    return {
        content: [{ type: 'text', text: error.message }],
        isError: true
    };
}

5.8 Tool 返回值的内容类型

Tool 的返回值通过 content 数组承载,支持多种内容类型的混合返回:

内容类型type 值说明
文本text最常用,纯文本结果
图片imageBase64 编码的图片数据
音频audioBase64 编码的音频数据
资源链接resource_link指向 MCP Resource 的 URI
嵌入资源resource内联的 Resource 内容

一个工具可以在单次调用中返回多种类型的内容。比如一个"生成图表"工具可以同时返回图片和描述文本:

json
{
  "content": [
    { "type": "text", "text": "2024年销售趋势图:Q1-Q4 同比增长 23%" },
    { "type": "image", "data": "iVBORw0KGgo...", "mimeType": "image/png" }
  ]
}

此外,MCP 还支持结构化输出(Structured Content)。如果工具定义了 outputSchema,它可以在返回 content 的同时提供 structuredContent 字段,包含符合 Schema 的 JSON 对象。为了向后兼容,提供结构化输出的工具也应该在 content 中放入序列化后的 JSON 文本。

5.9 与其他工具调用格式的对比

MCP Tool 并非市场上唯一的工具调用方案。将它与几个主流方案进行对比,能更清晰地理解其设计取舍。

OpenAI Function Calling

OpenAI 的函数调用方案将工具定义直接嵌入 Chat Completions API 的请求体中:

json
{
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "parameters": { "type": "object", "properties": { "location": { "type": "string" } } }
    }
  }]
}

核心差异:OpenAI Function Calling 是模型 API 层面的特性,工具定义与模型请求耦合在一起。MCP Tool 则是协议层面的抽象,工具注册在独立的 Server 上,通过标准协议与任意 Client/LLM 交互。这意味着同一个 MCP Server 的工具可以被不同的大模型(Claude、GPT、Gemini)通过不同的 Client 调用,而 OpenAI Function Calling 天然绑定在 OpenAI 的 API 上。

此外,MCP 提供了 Tool Annotations 这样的安全元数据,以及 tools/list_changed 这样的动态发现机制,这些在 OpenAI 的方案中没有对应设计。

LangChain BaseTool

LangChain 的 BaseTool 是一个 Python 类抽象:

python
class GetWeatherTool(BaseTool):
    name = "get_weather"
    description = "查询天气信息"
    args_schema = WeatherInput  # Pydantic Model

    def _run(self, location: str) -> str:
        return fetch_weather(location)

LangChain 的工具是框架内的概念,与特定的编程语言和运行时绑定。MCP Tool 是跨进程、跨语言的协议概念——Server 可以用 Rust 编写,Client 可以用 TypeScript 编写,两者通过 JSON-RPC 通信。这种解耦带来了更强的可组合性,但也引入了网络通信的开销。

Claude Code 的内置 Tool

Claude Code 中的工具(如 Read、Edit、Bash)也是 Tool 的一种体现,但它们是 Client 本地注册的工具,不经过 MCP Server。它们的定义格式与 MCP Tool 高度相似(name、description、inputSchema),但生命周期完全在 Client 进程内管理。这体现了 MCP 的一个设计哲学:协议定义的是交互格式,不限制工具的来源——可以是远程 Server,也可以是本地 Client 内置。

对比总结

维度MCP ToolOpenAI FCLangChain BaseTool
抽象层次协议层API 层框架层
跨语言是(JSON-RPC)是(HTTP API)否(Python)
跨模型否(绑定 OpenAI)
动态发现tools/list + 通知
安全标注ToolAnnotations
结构化输出outputSchemastrict mode

5.10 安全考量与最佳实践

协议在安全方面提出了明确的要求,这些要求直接影响生产环境中 Tool 的设计与部署。

Server 侧必须做到的事

  • 验证所有工具输入,不信任来自 Client 的任何参数
  • 实施访问控制,确保工具只能被授权的 Client 调用
  • 对工具调用进行速率限制,防止被恶意利用
  • 清洗工具输出,避免敏感信息泄露

Client 侧应该做到的事

  • 在敏感操作前向用户显示确认提示
  • 在调用 Server 之前向用户展示工具输入,防止恶意或意外的数据泄露
  • 对工具调用实施超时控制
  • 记录工具使用日志以供审计

一个经常被忽视的安全风险是:大模型构造的工具参数本身可能包含注入攻击。比如一个执行 SQL 查询的工具,如果 Server 端没有做参数化查询,大模型构造的 query 参数就可能包含 SQL 注入。MCP 协议通过要求 Server 验证所有输入来防范这类风险,但最终的安全保障取决于具体的实现质量。

5.11 本章小结

Tool 是 MCP 协议中赋予大模型行动能力的核心原语。本章从协议规范到 SDK 实现,系统梳理了 Tool 的完整技术栈:

  • 定义层面:Tool 通过 name、description、inputSchema(JSON Schema)构成声明式的函数接口,大模型据此理解"能做什么"和"怎么调用"。
  • 生命周期tools/list 发现工具 -> tools/call 调用工具 -> tools/list_changed 动态更新,构成完整的工具管理循环。
  • SDK 实现:TypeScript SDK 的 registerTool 和 Python SDK 的 @tool 装饰器,虽然 API 风格不同,但内核一致——名称校验、Schema 生成、请求处理器注册、变更通知。
  • 安全体系:Tool Annotations 的四个 hint(readOnly、destructive、idempotent、openWorld)为 Agent 的自动化决策提供了关键的安全元数据。
  • 错误处理:协议层错误与业务层错误的双层设计,使大模型能够区分"请求有误"和"执行失败",从而采取不同的恢复策略。

在下一章中,我们将深入 Sampling 原语——与 Tool 让 Server 暴露能力给大模型的方向相反,Sampling 让 Server 反向请求 Client 侧的大模型完成推理,构成了 MCP 协议中独特的双向能力调用架构。

基于 VitePress 构建