MCP Tool 设计细节:从“能封装”到“能被模型正确调用”

很多 MCP 实践文章会告诉你:把业务接口封装成 MCP Tool 就可以了

但在真实项目里,决定 MCP 是否好用的,往往不是"能不能封装",而是下面这些细节:

  • Tool 的 name / description / input_schema 是怎么生成的
  • 参数没有描述会不会影响模型调用
  • 为什么不建议 Tool 直接走 Controller
  • Python、TypeScript、Java 在实现方式上有什么差异

这篇文章重点讨论这些容易被忽略、但会直接影响 MCP 落地质量的问题。


一、核心原则:MCP Tool 是给模型设计的 API

业务接口不应该直接暴露给模型,而是应该通过 MCP Server 做一层"可控适配"。

这一层负责:

text 复制代码
参数校验
鉴权
限流
数据格式转换
敏感字段脱敏
异常转换
日志审计

最终暴露给模型的不是原始接口,而是一个清晰的 Tool:

json 复制代码
{
  "name": "query_order",
  "description": "根据订单 ID 查询订单详情",
  "input_schema": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "订单唯一标识"
      }
    },
    "required": ["order_id"]
  }
}

也就是说:

MCP Tool 不是传统 API 的简单转发,而是面向模型重新设计的业务能力。


二、Tool 三要素到底从哪里来?

一个 MCP Tool 通常包含三个关键元素:

text 复制代码
name          工具名
description   工具用途说明
input_schema  输入参数结构

不同语言的 MCP SDK 获取这三要素的方式不同。


1. Python:从函数自动推断

Python 里通常通过 @mcp.tool() 注册工具:

python 复制代码
from typing import Annotated
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("order-mcp-server")

@mcp.tool()
async def query_order(
    order_id: Annotated[str, "订单唯一标识"]
) -> dict:
    """根据订单 ID 查询订单详情"""
    return {
        "order_id": order_id,
        "status": "PAID",
        "amount": 199.0
    }

生成逻辑大致是:

text 复制代码
函数名        → name
docstring     → description
类型注解      → input_schema
Annotated     → 参数描述

如果不写 Annotated 或参数说明,schema 里可能只有类型,没有语义描述。


2. TypeScript:显式声明,最可控

TypeScript 通常通过 server.tool() 显式注册:

ts 复制代码
server.tool(
  "query_order",
  "根据订单 ID 查询订单详情",
  {
    orderId: z
      .string()
      .describe("订单唯一标识,来自订单系统"),
  },
  async ({ orderId }) => {
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            orderId,
            status: "PAID",
            amount: 199.0,
          }),
        },
      ],
    };
  }
);

这里三要素都是显式给出的:

text 复制代码
"query_order"                     → name
"根据订单 ID 查询订单详情"           → description
z.string().describe(...)          → input_schema

TypeScript 的好处是 schema 非常可控,尤其适合生产环境。


3. Java:注解 + 反射

在 Spring AI 中,可以通过 @Tool 暴露方法:

java 复制代码
@Tool(description = "根据订单 ID 查询订单详情")
public OrderDTO queryOrder(
    @Parameter(description = "订单唯一标识,来自订单系统")
    String orderId
) {
    return orderService.queryOrder(orderId);
}

生成逻辑一般是:

text 复制代码
方法名        → name
@Tool         → description
参数类型      → input_schema
@Parameter    → 参数描述

Java 的优势是适合已有 Spring Boot / 微服务体系,可以直接复用 Service 层业务逻辑。


三、参数没有 description,会不会出问题?

会,而且问题通常很明显。

比如下面这个 schema:

json 复制代码
{
  "type": "object",
  "properties": {
    "order_id": {
      "type": "string"
    }
  },
  "required": ["order_id"]
}

从程序角度看,它是合法的;但从模型角度看,它的信息不够。

模型只知道:

text 复制代码
order_id 是 string

但并不一定知道:

text 复制代码
这是订单系统里的订单 ID
不是用户 ID
不是支付流水 ID
不是物流单号

当参数更多时,问题会更严重。


典型问题 1:参数语义不清

json 复制代码
{
  "id": {
    "type": "string"
  }
}

模型很难判断这个 id 是什么。

更好的写法:

json 复制代码
{
  "order_id": {
    "type": "string",
    "description": "订单唯一标识,来自订单系统"
  }
}

典型问题 2:时间参数容易填错

不推荐:

json 复制代码
{
  "start": {
    "type": "string"
  },
  "end": {
    "type": "string"
  }
}

推荐:

json 复制代码
{
  "start_time": {
    "type": "string",
    "description": "查询开始时间,ISO8601 格式,例如 2024-01-01T00:00:00Z"
  },
  "end_time": {
    "type": "string",
    "description": "查询结束时间,ISO8601 格式,例如 2024-01-31T23:59:59Z"
  }
}

典型问题 3:枚举值不明确

不推荐:

json 复制代码
{
  "status": {
    "type": "string"
  }
}

推荐:

json 复制代码
{
  "status": {
    "type": "string",
    "enum": ["PAID", "UNPAID", "CANCELLED"],
    "description": "订单状态:PAID=已支付,UNPAID=未支付,CANCELLED=已取消"
  }
}

四、为什么不建议 MCP Tool 直接走 Controller?

很多人在封装时会这样写:

text 复制代码
MCP Tool → HTTP API → Controller → Service

看起来是在复用现有接口,但这通常不是最佳实践。


1. Controller 是 HTTP 协议层

Controller 通常处理的是:

text 复制代码
RequestBody
Header
Cookie
Session
HTTP 状态码
前端统一响应格式

例如:

json 复制代码
{
  "code": 0,
  "msg": "ok",
  "data": {
    "orderId": "123",
    "status": "PAID"
  }
}

但模型真正需要的是:

json 复制代码
{
  "orderId": "123",
  "status": "PAID"
}

所以 Tool 直接走 Controller,会引入多余的协议包装。


2. 可能出现"自己调用自己"的反模式

如果 MCP Server 和业务系统在同一个项目里,却这样调用:

text 复制代码
Tool → HTTP 调用本机接口 → Controller → Service

就会多一跳网络调用,增加延迟和复杂度。


3. 安全上下文容易错位

Controller 往往依赖 HTTP 上下文:

text 复制代码
Header
Token
Session
Cookie
Request IP

而 MCP Tool 调用时,上下文来源不同,需要单独设计用户身份、租户、权限和审计逻辑。


推荐结构

更推荐这样:

text 复制代码
Controller → Service → Repository / RPC / HTTP

MCP Tool  → Service → Repository / RPC / HTTP

也就是:

text 复制代码
Controller 和 MCP Tool 是两个入口
Service 是统一业务逻辑

这样既能复用业务逻辑,又不会把模型调用和 HTTP 协议层强耦合。


五、Python / TypeScript / Java 的差异

语言 注册方式 schema 控制 适合场景
Python 装饰器自动推断 中等 原型、轻量工具
TypeScript 显式声明 网关、中台、生产服务
Java 注解 + 反射 中到强 Spring Boot、企业系统

简单理解:

text 复制代码
Python:写起来最快
TypeScript:schema 最可控
Java:最适合复用企业业务系统

六、Tool 设计最佳实践

1. Tool 名称要表达业务动作

推荐:

text 复制代码
query_order
cancel_order
create_invoice
search_customer

不推荐:

text 复制代码
call_api
do_request
execute

Tool 名称应该让模型一眼知道"这个工具能做什么"。


2. 参数名要自解释

推荐:

text 复制代码
order_id
customer_id
start_time
end_time

不推荐:

text 复制代码
id
type
start
end

参数越模糊,模型越容易填错。


3. 每个参数都尽量写 description

特别是以下类型:

text 复制代码
ID 类参数
时间类参数
枚举类参数
分页参数
金额参数
布尔参数

比如:

json 复制代码
{
  "include_cancelled": {
    "type": "boolean",
    "description": "是否包含已取消订单,true=包含,false=不包含"
  }
}

4. 输出字段要裁剪

不要把业务接口原样返回给模型。

不推荐:

json 复制代码
{
  "userPhone": "13800000000",
  "internalRemark": "...",
  "riskScore": 87,
  "debugInfo": "..."
}

推荐:

json 复制代码
{
  "orderId": "123",
  "status": "PAID",
  "amount": 199.0
}

原则是:

text 复制代码
模型只需要完成任务所需的信息
不需要内部字段、敏感字段和调试字段

5. 参数太多时要拆 Tool

如果一个 Tool 有十几个参数,模型调用成功率通常会下降。

不推荐:

text 复制代码
search_order(
  order_id,
  user_id,
  status,
  start_time,
  end_time,
  min_amount,
  max_amount,
  page,
  size,
  sort,
  channel,
  tenant_id
)

可以拆成:

text 复制代码
query_order_by_id
search_orders_by_user
search_orders_by_status

七、一个实用判断标准

判断一个 Tool schema 是否合格,可以用一句话:

如果一个新同事只看这个 schema 都不知道怎么填,那模型大概率也会填错。

所以,Tool schema 不是单纯给程序看的,而是给模型"理解和决策"用的。


八、总结

业务接口封装成 MCP Tool,并不是把接口换个协议暴露出去。

更准确地说,它是在做三件事:

text 复制代码
1. 把业务接口抽象成模型可理解的能力
2. 把输入输出设计成模型容易使用的结构
3. 把调用过程纳入安全、权限、审计和治理

最后可以用这句话总结:

MCP Tool 不是传统接口封装,而是给模型设计 API。

这意味着,写 MCP Tool 时,你不是只在写代码,而是在告诉模型:

text 复制代码
什么时候用这个工具
需要传什么参数
返回结果应该如何理解

只有把这些细节设计好,MCP 才能从"能调用"变成"稳定、准确、可落地"。

相关推荐
Beginner x_u2 小时前
MCP 实践 01|从 0 搭建 MCP Server:读取简历与 JD,并用 MCP Inspector 测试
ai·node.js·mcp
GoodTimeGGB16 小时前
🚀 2024-2026最新AI热词终极科普:按时间线读懂Agent时代完整进化
agent·mcp·openclaw·harness·hermes
不叫猫先生1 天前
多平台 Web Scraping 实战指南:用 Bright Data + MCP 实现自动化数据采集(2026)
爬虫·数据采集·mcp
夜影风1 天前
AI智能体的本质:从“会回答“到“会完成“的范式革命
人工智能·ai agent
suixinm1 天前
Agent 设计模式:从 ReAct、CodeAct 到 Agentic Rag 与多智能体
设计模式·ai·react·rag·ai agent·agent智能体·multi-agent
JaydenAI2 天前
[Deep Agents:LangChain的Agent Harness-02]构建抽象的文件系统
python·langchain·ai编程·ai agent·deep agents·harness
zhangshuang-peta2 天前
OpenClaw 这类框架解决了什么问题?又没解决什么问题?
人工智能·ai agent·mcp·peta
YJlio2 天前
OpenClaw v2026.4.8 更新解析:扩展加载修复、通道配置优化、Slack 代理支持与升级避坑
gateway·自动化运维·版本更新·ai agent·openclaw·slack·插件兼容
mpr0xy2 天前
简单好用的AI提示词模版:目标,输入,输出
人工智能·ai·openai·提示词·ai agent