Appearance
第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 列表中选择合适的工具,构造参数并发起调用。
这个过程中有一个关键的信任链条:
在这个流程中,有几个值得注意的设计原则:
- 大模型做选择,Client 做守门。大模型决定调用哪个 Tool、传什么参数,但 Client 应该(SHOULD)在实际发送
tools/call之前向用户展示确认提示,确保 human-in-the-loop。 - Server 做执行,协议做规范。Server 负责实际执行工具逻辑并返回结果,但返回的格式严格遵循协议定义的
CallToolResult结构。 - 工具是声明式的。每个 Tool 通过 JSON Schema 声明自己接受什么参数,大模型据此构造合法的调用请求。
5.2 Tool 的定义结构
一个 MCP Tool 的完整定义包含以下字段:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
name | string | 是 | 工具的唯一标识符,1-128 字符 |
title | string | 否 | 人类可读的显示名称 |
description | string | 否 | 功能描述,帮助大模型理解何时使用 |
inputSchema | JSON Schema | 是 | 输入参数的 JSON Schema 定义 |
outputSchema | JSON Schema | 否 | 输出结构的 JSON Schema 定义 |
annotations | ToolAnnotations | 否 | 行为标注(只读、破坏性等) |
工具名称的命名规范值得特别关注。协议规定名称应仅包含 A-Z、a-z、0-9、_、-、. 这些字符,区分大小写,不应包含空格或特殊字符。这是因为工具名称会直接出现在 JSON-RPC 的请求参数中,过于复杂的名称容易导致解析问题。合法的例子包括 getUser、DATA_EXPORT_v2、admin.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,这个方法完成了几个关键步骤:
- 名称校验:调用
validateAndWarnToolName(name)检查名称是否符合规范。 - Schema 转换:通过
standardSchemaToJsonSchema()将 Zod schema 等标准 schema 格式转换为 JSON Schema。 - 注册请求处理器:首次注册工具时,调用
setToolRequestHandlers()为tools/list和tools/call设置处理逻辑。 - 发送变更通知:调用
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 装饰器内部做了什么?从源码可以看到完整的流程:
- 函数元数据提取:调用
func_metadata(fn)解析函数签名,自动生成 Pydantic 模型作为参数校验的基础。 - JSON Schema 生成:通过
model_json_schema()从 Pydantic 模型自动生成 JSON Schema,作为工具的inputSchema。 - Context 注入检测:通过
find_context_parameter(fn)扫描函数签名,如果发现类型标注为Context的参数,就标记为 context 注入点,在调用时自动注入上下文对象。 - 工具注册:通过
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 result5.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 ePython SDK 利用 Pydantic 的 model_validate 进行校验,这意味着不仅能检查类型,还能执行自定义验证器(validator)。如果参数校验失败,两个 SDK 都会抛出带有 InvalidParams 错误码的 ProtocolError,最终以 JSON-RPC 错误响应的形式返回给 Client。
5.6 Tool Annotations:Agent 安全的元数据体系
Tool Annotations 是 MCP 协议中一个精心设计的安全机制。它为每个工具提供了行为层面的元数据标注,让 Client 和 Agent 框架能在调用工具之前就了解其潜在影响。
| 标注字段 | 默认值 | 含义 | 典型场景 |
|---|---|---|---|
readOnlyHint | false | 工具是否不修改环境 | 查询类:数据库 SELECT、API GET |
destructiveHint | true | 工具是否可能执行破坏性操作 | 删除数据、覆盖文件、发送不可撤回消息 |
idempotentHint | false | 相同参数重复调用是否无副作用 | PUT 操作、幂等更新 |
openWorldHint | true | 工具是否与开放世界交互 | Web 搜索(开放)vs 内存工具(封闭) |
这些标注为什么对 Agent 安全如此重要?考虑以下场景:
场景一:自动重试策略。当一个 Tool 调用因网络超时失败时,Agent 是否应该自动重试?如果 idempotentHint: true,重试是安全的;如果 idempotentHint: false 且 destructiveHint: 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 | 最常用,纯文本结果 |
| 图片 | image | Base64 编码的图片数据 |
| 音频 | audio | Base64 编码的音频数据 |
| 资源链接 | 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 Tool | OpenAI FC | LangChain BaseTool |
|---|---|---|---|
| 抽象层次 | 协议层 | API 层 | 框架层 |
| 跨语言 | 是(JSON-RPC) | 是(HTTP API) | 否(Python) |
| 跨模型 | 是 | 否(绑定 OpenAI) | 是 |
| 动态发现 | tools/list + 通知 | 无 | 无 |
| 安全标注 | ToolAnnotations | 无 | 无 |
| 结构化输出 | outputSchema | strict 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 协议中独特的双向能力调用架构。