MCP 协议设计与实现

第5章 Tool:让 Agent 调用世界

作者 杨艺韬 · 6,511 字

第5章 Tool:让 Agent 调用世界

“The limits of my tools are the limits of my world.” ——改编自维特根斯坦

“工具的边界就是 Agent 能力的边界。没有 Tool 的 LLM 是一个永远在说话却从未动手的咨询师;有了 Tool 的 Agent 才是真正能推动事情发生的执行者。”

本章要点

  • 理解 Tool 在三原语中的”动作性”定位:model-controlled 的执行通道
  • 掌握 Tool 的完整协议结构:name、description、inputSchema、outputSchema、annotations
  • 理解 Tool 的三阶段生命周期:发现、调用、变更通知
  • 对比 TypeScript SDK 的 registerTool 与 Python SDK 的 @tool 装饰器
  • 掌握 Tool Annotations 四大 hint 对 Agent 安全决策的意义
  • 理解协议层错误 vs 业务层错误的双层设计及其对自我修正的关键作用
  • 与 OpenAI Function Calling、LangChain BaseTool 对比,看清 MCP 的设计定位
  • 掌握 6 条 Tool 设计原则与 7 种常见反模式

5.1 Tool 在 MCP 中的定位

5.1.1 从”会说话的 LLM”到”会做事的 Agent”

2023 年之前,大多数人对 LLM 的印象是”一个会说话的百科全书”——它能回答问题,但只能回答。它不能帮你把邮件发出去,不能查看你的日历,不能运行一段代码验证它的答案。这种无力感有一个经典比喻:

“LLM 就像一个绑在椅子上的天才——它知道所有答案,但它的手被绑着。”

Tool 就是解绑这双手的协议。它让 LLM 从”输出文本”升级到”发起动作”:输出一段参数化的调用意图,由协议层执行到真实世界,再把结果塞回对话。

5.1.2 三原语中的 Tool

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

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

5.1.3 Tool 调用的信任链条

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

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

sequenceDiagram
    participant User as 用户
    participant LLM as 大模型
    participant Client as MCP Client
    participant Server as MCP Server
    participant World as 外部系统

    User->>LLM: "查一下北京天气"
    Note over LLM: 分析意图,选择 Tool
    LLM->>Client: 决定调用 get_weather(location="北京")

    Note over Client: 安全检查 / 用户确认
    Client->>Server: tools/call {name, arguments}
    Server->>World: 调用天气 API
    World-->>Server: 返回天气数据
    Server-->>Client: CallToolResult {content}
    Client->>LLM: 将结果注入上下文
    LLM->>User: "北京今天 25°C,晴转多云"

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

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

这三层分工,构成了 Tool 作为”意图—守门—执行”三级流水线的完整闭环。

5.2 Tool 的定义结构

5.2.1 六个字段

一个 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

5.2.2 一个典型 Tool 定义

{
  "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.2.3 “description” 的隐藏分量

看似不起眼的 description 字段,其实是 Tool 定义中最重要的字段之一——因为它直接决定了大模型是否能”选对”这个工具。

看两个对比:

{ "description": "查询天气" }
{
  "description": "查询指定城市的**当前**(今天)天气。不支持历史天气或未来预报。支持中国大陆、香港、台湾、海外主要城市。参数 location 支持城市名(如'北京')或邮政编码(如'100000')。"
}

一个只有 4 个字,一个有 80 字并明确了边界、单位、支持范围。在 LLM 面对一组相似工具(get_weather、get_weather_forecast、get_historical_weather)时,description 的精确度决定了选择的正确率。真实项目里应把这件事做成工具选择评测:同一组任务、同一组候选工具、只替换 description,再比较模型是否选中正确工具,而不是脱离任务集套用固定百分比。

口诀:name 是给调度器看的,description 是给 LLM 看的。前者要简,后者要富

5.3 Tool 的生命周期

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

stateDiagram-v2
    [*] --> 能力协商: initialize
    能力协商 --> 工具发现: Server 声明 tools 能力
    工具发现 --> 等待调用: Client 获取 Tool 列表
    等待调用 --> 执行中: LLM 选择并调用 Tool
    执行中 --> 返回结果: Server 执行完成
    返回结果 --> 等待调用: 结果注入 LLM 上下文

    等待调用 --> 工具发现: tools/list_changed 通知

    note right of 能力协商: Server 在 capabilities 中<br/>声明 tools.listChanged
    note right of 执行中: Server 验证输入<br/>执行业务逻辑

5.3.1 发现阶段:tools/list

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

// 请求
{ "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 请求,包含工具名和参数:

// 请求
{
  "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 重新拉取工具列表:

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

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

5.3.4 工具集规模的”金发姑娘区”

一个实际问题:一个 Server 应该暴露多少个 Tool 才合适?

经验规律(来自 Anthropic 和社区的调研):

Tool 数量LLM 表现典型问题
1-5 个极佳功能覆盖有限
6-15 个优秀最常见的”sweet spot”
16-30 个良好需要精心设计 name/description
31-50 个下降选择困难,建议分组
50+显著下降必须配合工具搜索机制

这组数字不是硬性规则,但揭示了一个认知心理学般的现象:LLM 的”工具选择空间”是有实际上限的,和人类的”选择悖论”类似。如果你的 Server 不得不暴露 50+ 个 Tool,应该考虑:

  1. 合并近似工具——search_usersearch_productsearch_order 合并为 search(entity, query)
  2. 层次化组织——暴露 list_tools 元工具,按需加载具体工具
  3. 拆分 Server——按领域拆成多个小 Server

5.4 SDK 中的 Tool 注册机制

5.4.1 TypeScript SDK:registerTool 方法

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

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 风格的装饰器模式:

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 上下文注入,无需额外配置:

@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 的设计对比

graph TB
    subgraph TypeScript_SDK["TypeScript SDK"]
        TS_REG["registerTool(name, config, callback)"]
        TS_VAL["validateAndWarnToolName()"]
        TS_SCH["standardSchemaToJsonSchema()"]
        TS_HDL["setToolRequestHandlers()"]
        TS_NTF["sendToolListChanged()"]

        TS_REG --> TS_VAL
        TS_REG --> TS_SCH
        TS_REG --> TS_HDL
        TS_REG --> TS_NTF
    end

    subgraph Python_SDK["Python SDK"]
        PY_DEC["@server.tool() 装饰器"]
        PY_META["func_metadata() 提取签名"]
        PY_CTX["find_context_parameter()"]
        PY_PYDANTIC["Pydantic model_json_schema()"]
        PY_MGR["ToolManager.add_tool()"]

        PY_DEC --> PY_META
        PY_DEC --> PY_CTX
        PY_META --> PY_PYDANTIC
        PY_DEC --> PY_MGR
    end

    style TypeScript_SDK fill:#1a1a2e,stroke:#e94560,color:#eee
    style Python_SDK fill:#1a1a2e,stroke:#0f3460,color:#eee

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

5.5 输入校验机制

5.5.1 SDK 层面的校验

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

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

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 完成:

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.5.2 业务层校验的”职责分界线”

SDK 只做类型级别的校验(字符串就是字符串、数字不超出范围),业务级别的校验仍是 Server 开发者的责任:

校验类型谁做例子
类型校验SDK 自动location 是 string 不是 number
结构校验SDK 自动required 字段不能缺
语义校验Server 代码location 是否是存在的城市
权限校验Server 代码当前用户是否能操作该资源
业务约束Server 代码余额不足时拒绝转账

经典错误:误以为 SDK 校验过的数据就是”可信”的,跳过业务校验。实际上,SDK 只保证”格式对了”,不保证”语义对了”。

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

5.6.1 四个 hint

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

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

5.6.2 三大策略场景

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

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

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

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

5.6.3 “Hint 不是 Guarantee”

协议特别强调:这些标注只是”提示”(hint),不是”保证”。来自不受信任的 Server 的标注不应被盲目采信。一个恶意 Server 完全可以将一个破坏性工具标记为 readOnlyHint: true 来绕过确认提示。

正确的信任边界:

Server 来源标注信任度做法
官方/签名 Server直接按 hint 决策
企业内部 Server按 hint 决策 + 关键操作二次确认
第三方开源 Serverhint 仅作参考,所有写操作强制用户确认
未知 Server极低禁止,或所有调用都需确认

5.7 错误处理的双层设计

5.7.1 协议层 vs 业务层

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

协议层错误(Protocol Errors) 以标准 JSON-RPC error 形式返回,表示请求本身有问题:

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

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

业务层错误(Tool Execution Errors) 以正常的 CallToolResult 形式返回,但 isError 字段为 true

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

5.7.2 自我修正的关键原理

这类错误的关键特征是包含可操作的反馈信息。大模型读到”日期格式错误”后可以自行修正参数格式重新调用,这构成了 Agent 的自我纠错循环

错误类型是否注入 LLM修正价值推荐处理
协议层(请求格式错误)MAY 注入Client 可直接报错
业务层(语义错误)SHOULD 注入必须让 LLM 有机会修正

协议规定 Client 应该(SHOULD)将业务层错误传递给大模型以启用自我修正;对于协议层错误,Client 可以(MAY)传递给大模型,但恢复成功率通常较低。

5.7.3 SDK 的自动包装

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

// 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 内容

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

{
  "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 文本。

结构化输出的意义:下游可能不是 LLM 而是代码——比如 Agent 的某个后处理步骤要取 structuredContent.total_count 去做判断。如果只有 text,必须再来一次解析。

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

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

5.9.1 OpenAI Function Calling

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

{
  "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 的方案中没有对应设计。

5.9.2 LangChain BaseTool

LangChain 的 BaseTool 是一个 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 通信。这种解耦带来了更强的可组合性,但也引入了网络通信的开销。

5.9.3 Claude Code 的内置 Tool

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

5.9.4 对比总结

维度MCP ToolOpenAI FCLangChain BaseTool
抽象层次协议层API 层框架层
跨语言是(JSON-RPC)是(HTTP API)否(Python)
跨模型否(绑定 OpenAI)
动态发现tools/list + 通知
安全标注ToolAnnotations
结构化输出outputSchemastrict mode
独立进程
鉴权机制OAuth 2.1(远程)API Key自定义

5.10 Tool 的 7 种反模式

在咨询数十个 MCP Server 项目后,我整理出以下 7 种最常见反模式:

反模式问题正确做法
description 过短(如”查数据”)LLM 无法选对明确输入、输出、边界、假设
name 太长或含层级company.domain.tools.get_user_by_id_v2LLM 分词成本高、误调用扁平且简洁的 name,最多一级分隔符
参数语义重叠iduserId 都存在)LLM 不知该填哪个合并或重命名,避免歧义
工具粒度过细(10 个只差参数的工具)工具爆炸,LLM 选错合并为带枚举参数的工具
工具粒度过粗(一个 do_everything 工具)LLM 难以构造参数按动词拆分
destructive 不标注 destructiveHintClient 可能不弹确认诚实标注,用户更安全
返回日志内容(堆栈/PII)泄露敏感信息给 LLM 和日志脱敏后返回 + logging 通道单独发

5.11 Tool 设计 6 原则

1. 动词开头命名——get_weathersend_emailcreate_issue。动词让 LLM 快速理解这是一个”做事”的工具。

2. 每个参数都加 description——LLM 不仅看工具 description,还看参数 description。别吝啬字数。

3. 默认值服务于”最常见路径”——timeout: 30 好过 timeout: null,因为 LLM 更愿意调用”可以不填”的工具。

4. 错误消息可操作——“参数错误”是废话,“location 必须是城市名或邮政编码”才能让 LLM 自我修正。

5. 只返回必要数据——LLM 上下文是黄金。一个 list 工具如果能返回 3 个核心字段就够了,就别返回 30 个。

6. 诚实标注 annotation——欺骗标注 = 让用户失去保护。这是信誉问题。

5.12 安全考量与最佳实践

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

Server 侧必须做到的事

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

Client 侧应该做到的事

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

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

5.13 本章小结

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 的自动化决策提供了关键的安全元数据
  • 错误处理:协议层错误与业务层错误的双层设计,使大模型能够区分”请求有误”和”执行失败”,从而采取不同的恢复策略

一句话记忆

Tool 是把 LLM 的”说”翻译成世界的”动”——description 是选择器,Schema 是守门员,annotation 是安全带,content 是回声。

在下一章中,我们将深入 Resource 原语——与 Tool 让 Server 暴露”动作”给大模型的方向不同,Resource 让 Server 把”上下文”声明式地提供给 Client,由应用程序而非 LLM 来决定何时注入到对话中。


延伸阅读:MCP Tool 和 OpenAI Function Calling 的深层差异

表面看——MCP Tool 和 OpenAI Function Calling 功能相似——都是”LLM 调用外部函数实际上——两者有本质差异OpenAI Function Calling——“LLM 内置能力”——函数描述随 prompt 发给 LLM、每次调用都带完整工具集、LLM 直接返回函数调用MCP Tool——“协议层能力”——Server 声明工具、Client 按需呈现给 LLM、LLM 调用结果反馈回 Server——多了一层协议抽象

MCP 的多一层抽象、带来灵活性——“工具可以跨 LLM 复用”(同一个 Tool 在 GPT、Claude、Gemini 里都能用)、“工具可以独立演化”(不依赖 LLM 提供商的发布节奏)、“工具可以本地运行”(隐私敏感场景)代价是——“调用链更长”(Client 要 marshal/unmarshal)、“配置更复杂”(需要起一个 MCP Server)对大多数场景、MCP 的灵活性胜过复杂度——这也是它快速成为事实标准的原因

延伸阅读:Tool Schema 的类型安全

MCP Tool 的参数 Schema 用 JSON Schema 描述——这让”类型安全”从编程语言延伸到协议层LLM 调用 Tool 时、Client 可以按 Schema 验证 LLM 的输出(类型对不对、必填字段有没有、格式合不合法)——不合规的调用直接拒绝、不发给 Server这层验证、让 Server 不用每次都防御”恶意”或”误用”的输入——降低了 Server 实现的复杂度

JSON Schema 是 IETF 的标准(RFC 8259 Draft 20 等)——广泛支持、工具链成熟(ajv、jsonschema、Pydantic 等)MCP 选用 JSON Schema——借用了整个生态——不需要发明新的 Schema 语言这种”站在标准肩膀上”的选择、是协议设计的务实《Serde 元编程》讨论的类型驱动开发、《LangChain 源码》讨论的结构化输出——都和 JSON Schema 的应用相关、值得对照

延伸阅读:Description 是 Tool 的生死线

本章用”description 是选择器”概括——这是 Tool 设计最重要的洞察LLM 看不到你的代码——只看得到 descriptiondescription 写得好——LLM 能准确判断”什么时候调用、传什么参数”;写得差——LLM 要么该用时不用、要么用错场景、要么传错参数Tool description 的质量、直接决定 Tool 的实际价值

好的 description 有几个特点——“起头说场景”(“当用户问股价时使用”)、“给正例”(“比如:get_stock(‘AAPL’)”)、“标清楚边界”(“只支持美股、A 股请用 get_cn_stock”)、“避免技术缩写”(不要 “HTTP 404”、写 “资源不存在”)这些看起来细节、但真实影响 LLM 的选择准确率《Harness Engineering》第 5 章讨论的 Tool 描述工程、和本章主题高度重合——两本书对读能有互补

延伸阅读:Annotation 作为安全带

MCP 的 Tool annotation(readOnlyHint、destructiveHint、idempotentHint、openWorldHint)——是 Tool 的”语义标签”——让 Client 能根据语义做权限控制、UI 提示、重试策略readOnlyHint——只读工具、默认可以放心调用;destructiveHint——破坏性工具、需要用户确认;idempotentHint——幂等工具、可以放心重试;openWorldHint——访问外部世界(网络、数据库)、需要网络权限

这种”元数据驱动的安全”、让 Client 能为不同风险级别的 Tool 提供不同 UI只读工具可以直接调;破坏性工具要弹确认框;幂等工具可以自动重试;open world 工具要显示网络状态——都是自动化的这比”所有工具一视同仁”要友好得多《OpenClaw 源码》第 13 章的权限分级、《Harness Engineering》第 14 章的权限模型——都有类似思路——这是 AI 工具系统的共同安全工程

延伸阅读:Tool 调用的错误语义

Tool 调用可能失败——MCP 对此有明确规范协议级错误”——通信失败、Schema 违规——通过 JSON-RPC error 返回;“业务级错误”——查询不到数据、参数组合非法——通过 content 的 isError 字段返回这种”分类错误”、让 Client 能精确处理——协议错要重试、业务错要给用户友好消息

没有这种分类——所有错误都一视同仁——Client 很难做出合理响应这和 HTTP 的 4xx vs 5xx、gRPC 的 status code 分类——都是同一种思路——“按错误性质分级处理MCP 把这种成熟经验应用到 AI 工具协议——让”错误处理”从”玄学”变成”工程

延伸阅读:Content 作为回声

Tool 返回的 content——可以是 text、image、audio、resource——这种”多模态返回值”让 Tool 不局限于”返回字符串一个”查询用户头像”的工具——直接返回 image、LLM 能”看到”头像;一个”读取文档”的工具——返回 resource、引用可以在对话里复用;一个”生成报告”的工具——返回 text + image——LLM 能综合理解

这种”多模态返回”的设计、是为了适配”多模态 LLM”(GPT-4V、Claude 3 Opus、Gemini Pro)——让 Tool 不只能和文本交互、也能和图像、音频交互未来 LLM 会支持更多模态(视频、3D 模型、触觉)——MCP 的 content 类型体系能自然扩展这是”协议为未来而设计”的体现——而不是”只为当下需求

延伸阅读:Tool 的发现机制

MCP 的 tools/list 方法——让 Client 能动态发现 Server 提供的工具这种”运行时发现”、比”编译期静态声明”更灵活——Server 可以根据用户身份、时间、配置动态返回不同的工具集这让一个 MCP Server 能服务多种场景——企业用户看到企业工具、个人用户看到个人工具——基于同一套代码

运行时发现的类似机制、在其他协议里也有——OpenAPI 的 spec、gRPC 的 reflection、GraphQL 的 introspection——都是”让客户端问’你能做什么MCP 延续这个传统——让 AI 工具生态能像 API 生态一样自动适配这种”自描述协议”、是生态繁荣的基础——让工具能像互联网页面一样自然被发现和使用

延伸阅读:Tool 的版本管理

Tool 版本管理是”生产级 MCP Server”的重要考量随着时间推移——Tool 的参数会变、行为会调整、新能力会加入——这些变化需要版本化管理、让 Client 能适配常见做法——“Tool 名字带版本”(get_stock_v2)、“在 annotation 里标注版本”、“通过 capability 声明支持的版本范围

版本管理的哲学——“老工具不删、新工具加入”——让已有 Client 继续工作、新 Client 享受新能力破坏性变更——用”deprecation 预告”——提前告诉用户”这个工具将在 X 版本删除、请迁移到 Y”——给用户时间适配这种”优雅演化”的纪律、是开源工具长期成功的关键《LangChain 源码》第 17 章讨论的 Partner 版本管理——也涉及类似思路

延伸阅读:Tool 的分类命名

当一个 MCP Server 提供几十个 Tool 时——“命名”成为重要问题好的命名能让 LLM 快速选到正确工具、坏的命名让 LLM 迷失建议的命名约定——“动词_名词”(get_stock、create_issue)、“resource/action”(stock/get、issue/create)——任选一种、全局保持一致避免——“一词多义”(process 到底做什么?)、“技术缩写”(svc/api_v2/mgr_h)、“不同风格混用”(一部分 get_xxx、一部分 fetch_yyy)

好的命名、是”让 LLM 不用猜”——看名字就知道作用这和给人类看的代码命名原则一致——但对 LLM 要求更严格(LLM 不会去看源码猜意图)这种”对 LLM 友好的命名”能力、是 AI 时代的新技能——值得每个 Tool 作者认真对待、不要当成”顺手起个名字”的小事

延伸阅读:Tool 链式调用的设计

复杂 Agent 场景、常常需要”链式调用”——先调 get_user 拿到 user_id、再调 list_orders(user_id)、再调 get_order_detail(order_id)——多个 Tool 串起来完成一个任务MCP 对这种场景没有特殊协议——由 LLM 自己理解、一步步调用但 Tool 设计者可以通过”返回值包含下一步 hint”来引导 LLM

比如 get_user 返回值里可以包含”related_orders_url”提示——让 LLM 知道”下一步可以调 list_orders这种”链式提示”、让 Tool 变得”可组合”——LLM 能通过文档引导、自然地完成多步任务好的 Tool 链设计、让复杂任务的完成率大幅提升

延伸阅读:Tool 性能优化的层次

Tool 性能优化有多个层次——“Server 内部优化”(算法、缓存、DB 索引)、“协议层优化”(减少 round trip、批量调用)、“LLM 层优化”(减少 token 消耗、提高选择准确率)每个层次都值得投入Server 优化——让单次调用快;协议优化——让多次调用总延迟低;LLM 优化——让调用更少

优化的优先级——“先 LLM 层、再协议层、再 Server 层”——因为 LLM 调用通常是最慢的如果能让 LLM “每次正确调用、不需要重试”——就胜过任何 Server 层优化这种”先优化最慢的环节”的思路、是 Amdahl 定律的体现——系统整体性能由最慢环节决定《vLLM 源码》讨论的 LLM 推理优化、《Tokio 源码》讨论的异步性能——都在这个框架下——跨书对照能建立系统级的性能思维

延伸阅读:Tool 测试的策略层次

Tool 测试有三个层次——“unit test”(测试 Tool 内部逻辑、mock 外部依赖)、“integration test”(测试 Tool 和真实依赖的集成)、“LLM-in-the-loop test”(测试 LLM 能否正确调用 Tool)三者缺一不可Unit test 保证逻辑正确——快速反馈;integration test 保证集成可用——避免”mock 通过、真实不通”;LLM test 保证 LLM 能用好——这是 Tool 的最终价值

LLM-in-the-loop test 是 AI 时代的新概念——给 LLM 一组典型任务、看它是否能正确调用 Tool 完成这种测试成本高(调 LLM 要钱)、但价值大(这才是用户实际体验)在关键 Tool 上投入 LLM test、是生产级 MCP Server 必须的Claude Code、Cursor 等头部产品、都有完整的 LLM test 流水线

延伸阅读:Tool 的审计合规

企业场景、Tool 调用必须审计——“谁在什么时候调用了什么 Tool、传了什么参数、收到什么结果”——这些要留痕、支持事后追溯合规要求(SOX、HIPAA、GDPR、ISO 27001、等保三级)、很多都涉及”关键操作必须审计MCP 本身没规定审计——但 Server 实现应该自己加审计日志

好的审计日志——记录全、不可篡改、可查询技术实现——WAL(Write-Ahead Log)保证不丢、加密存储保证不被篡改、索引优化保证可查、冷热分层保证成本可控对合规严格的企业(金融、医疗、政府、军工)——MCP Server 的审计能力是选型关键、没有审计能力的 Server 直接被排除这也是”企业级 MCP Server”区别于”玩具级 MCP Server”的重要标志——企业采购时会认真审查这些能力、所以生产级 Server 必须从第一天就把这些能力考虑进去

延伸阅读:Tool 的国际化

面向全球用户的 Tool——必须考虑国际化描述要本地化——英文用户看英文、中文用户看中文、日文用户看日文、阿拉伯用户看阿拉伯文参数含义要跨文化一致——“city”这个参数、是 “New York” 还是 “纽约”?还是”NY”?需要明确约定错误消息要本地化——给中国用户报”用户不存在”、给法国用户报”Utilisateur inexistant”、给日本用户报”ユーザーが存在しません

实现上——MCP 支持通过 Accept-Language 头协商语言、Server 据此返回对应语言的描述和错误这是 HTTP 经典机制在 MCP 里的应用国际化看起来工作量大、但对全球用户是必需的——没有国际化的 Tool、非英语用户使用体验会打折扣、甚至完全无法使用《Vite 源码》第 11 章讨论的 HTML 处理国际化——也是类似话题、都涉及”让产品跨文化被使用”的工程智慧

延伸阅读:Tool 的演化路径

初学者写 Tool 的典型演化——“第一版:能跑就行”(硬编码、无错误处理)、“第二版:规范参数”(加 schema 验证)、“第三版:错误分类”(区分协议错和业务错)、“第四版:加 annotation”(标注 readonly/destructive)、“第五版:加审计”(日志留痕、监控告警)、“第六版:LLM-test”(确保 LLM 能正确调用)、“第七版:国际化和多租户”(企业级支持)

每一步升级、都让 Tool 从”玩具”向”生产级”迈进——每一步都对应一类真实问题的解决这个路径没有捷径——需要时间和经验积累、急不得也省不了但好消息是——每一步都有明确收益、不会白投入——是”确定性的改进”、不是赌博希望本章能帮读者在这条路径上少走弯路、更快到达生产级 Tool 的水平、做出被广泛采用的 MCP Server

延伸阅读:Tool 的社区分享

好的 Tool 应该分享出来——放到 MCP Server Registry、写个博客介绍、发到 Hacker News 或 Reddit 或小红书、在 Twitter 上宣传开源分享的好处——“让更多人受益”、“收到反馈改进”、“建立个人品牌”、“结识志同道合的朋友”——每一个都是长期价值2026 年的 MCP 生态、正处于”淘金期”——早期贡献者有机会在某个细分领域建立影响力、变成该领域的代表性人物

分享的具体路径——“先在 GitHub 开源”(让别人能读、能 fork)、“再写 README 和示例”(降低使用门槛)、“再发布到 MCP Registry”(让更多人发现)、“再写博客或视频”(深度解释设计)这套组合拳、能让一个好 Tool 在 2-3 周内被数百开发者发现和采用——从无到有的传播速度惊人这比”默默写完不管”要有价值得多——开源不只是”给代码”、更是”建立连接”——和其他开发者、和未来的机会