Tools原语深度解析:从定义到调用全流程

📅 2026/8/14 16:15:56
Tools原语深度解析:从定义到调用全流程
摘要MCP Tools原语深度解析从工具定义的JSON Schema到call_tool调用的完整流程。涵盖参数类型、返回格式、错误处理、工具列表发现机制和最佳实践。Tools原语深度解析从定义到调用全流程上周我在做一个内部运维助手的demo想让大模型帮我查线上服务的健康状态。我一开始图省事直接把几个HTTP接口塞给Function Calling结果换了模型厂商就得重写一遍schema写到第三个的时候我已经麻了。后来我把这些接口改造成MCP的Tools原语用FastMCP统一注册模型只要支持MCP就能自动发现和调用。这篇我就把Tools从协议定义到调用流程完整拆一遍附带可直接跑的代码和我踩过的几个坑。Tools到底是个什么东西MCP规范里Tools原语的定义很直白它让Server暴露一组可被语言模型调用的函数。每个工具用唯一的name标识配套一个inputSchema描述参数模型根据用户意图自己决定要不要调、调哪个。这里有个关键设计点要记住。Tools是model-controlled的也就是说工具的发现和调用由模型自主完成。模型在对话过程中看到工具列表结合上下文判断我现在需要调check_service_health然后发起调用。这一点和Resources、Prompts的定位完全不同后面几篇会细讲。Tools的完整生命周期分三步。第一步是发现客户端发tools/list请求拿到所有可用工具。第二步是调用模型选定工具后客户端发tools/call带上arguments。第三步是结果处理Server返回content数组模型把它读进上下文继续对话。inputSchema的JSON Schema规范每个Tool必须有一个inputSchema它是一个标准的JSON Schema对象。规范要求type固定为objectproperties里定义每个参数的类型和描述required数组列出必填项。下面是规范里get_weather工具的inputSchema结构。{type:object,properties:{location:{type:string,description:City name or zip code}},required:[location]}用FastMCP的好处是你不用手写JSON Schema框架根据Python函数的类型注解自动生成。你写def add(a: int, b: int)FastMCP就帮你生成带a和b两个integer参数的schema。我之前踩过一个坑函数参数没写类型注解FastMCP默认当string处理模型传了个数字进来字符串拼接就出错了。所以每个参数都要写清楚类型。新版规范还加了outputSchema用JSON Schema约束工具的返回结构。配合structuredContent字段一起用客户端拿到的是结构化JSON对象方便程序化处理。这对需要把工具结果喂给下游系统的场景特别有用。FastMCP会根据函数的返回类型注解自动生成outputSchema你写- dict它就帮你搞定。工具注册和发现机制工具注册在FastMCP里就是加个装饰器的事。每个被mcp.tool装饰的函数自动注册成工具函数名当工具名docstring当描述。发现机制走的是tools/list这个JSON-RPC方法。客户端初始化连接后发tools/listServer返回当前所有工具的元数据列表。如果Server声明了listChanged能力工具列表变化时还会主动推notifications/tools/list_changed通知客户端再重新拉一次列表。我做过一个动态工具的实验运行时根据配置文件增删工具然后手动触发list_changed通知。客户端这边如果没处理这个通知就会一直用旧的工具列表新加的工具死活调不到。所以客户端一定要监听这个通知收到就重新list一遍。调用流程与错误处理调用工具发tools/call请求参数是工具name和arguments对象。Server执行完返回content数组和isError标志。这里有个特别容易搞混的地方MCP的Tools有两套错误机制。第一套是协议级错误走标准JSON-RPC的error字段。比如工具名不存在、参数格式不对这类错误直接返回error对象比如code -32602表示Unknown tool。第二套是工具执行错误返回正常result但isError设为true。比如工具内部调API失败了这时候不算协议错误属于工具自己执行出了问题content里放上错误说明文本。我一开始没分清这两层工具内部抛异常的时候直接让框架返回了JSON-RPC error结果客户端那边的处理逻辑以为工具不存在报了个误导性的错误。后来我把工具内部的异常catch住改成返回isErrortrue的文本结果客户端就能正确区分工具调用失败和工具本身不存在了。完整代码下面是一个完整的可运行示例包含Server端和客户端测试脚本。Server提供两个工具一个查服务状态一个批量检查。客户端用FastMCP的Client做in-process测试。server.py# server.py MCP Tools原语完整示例# 运行方式 python server.py# 依赖安装 pip install fastmcpfromfastmcpimportFastMCPfrompydanticimportFieldimportrandom# 创建MCP服务器实例, 给它起个名字mcpFastMCP(nameOpsToolsServer)# 抽出公共逻辑, 避免工具之间代码重复# 这个函数不会被注册成工具, 因为没有加装饰器def_check_one(service_name:str)-dict:内部辅助函数, 检查单个服务的健康状态.# 模拟已知服务列表, 实际项目从配置或注册中心读取known_services[user-service,order-service,payment-service]ifservice_namenotinknown_services:# 返回unknown状态, 由调用方决定怎么处理return{service:service_name,status:unknown,message:f服务{service_name}不在已知列表中,}# 模拟随机健康状态, 真实场景替换成HTTP健康检查is_healthyrandom.random()0.3latency_msround(random.uniform(10,200),1)return{service:service_name,status:healthyifis_healthyelseunhealthy,latency_ms:latency_ms,checked_at:2026-08-09T10:00:00Z,}mcp.tooldefcheck_service_health(service_name:strField(description要检查的服务名称, 比如user-service),)-dict:检查指定服务的健康状态, 返回状态码和响应时间. 这个工具模拟查询线上服务的健康检查接口, 实际项目中把_check_one里的逻辑替换成真实HTTP调用即可. # 直接复用辅助函数, 保持工具函数本身的简洁return_check_one(service_name)mcp.tooldefbatch_check_services(services:list[str]Field(description要批量检查的服务名称列表),)-dict:批量检查多个服务的健康状态, 返回汇总结果. 接收服务名列表, 逐个调用检查逻辑, 最后统计健康和不健康的数量, 方便一次性看全局. results[]healthy_count0forsvcinservices:# 复用单个检查逻辑result_check_one(svc)results.append(result)ifresult.get(status)healthy:healthy_count1return{total:len(services),healthy:healthy_count,unhealthy:len(services)-healthy_count,details:results,}if__name____main__:# 以stdio模式启动服务器, 供MCP客户端连接# 也可以换 transportsse 走HTTP, 看你的部署需求mcp.run()client_test.py# client_test.py 客户端测试脚本# 运行方式 python client_test.py# 这个脚本通过FastMCP Client以stdio方式连接上面的server.pyimportasynciofromfastmcpimportClientasyncdefmain():# 直接传server.py路径, Client会自动用stdio启动它asyncwithClient(server.py)asclient:# 第一步, 发现工具, 相当于发tools/list请求toolsawaitclient.list_tools()print( 发现的工具 )fortintools:print(f 名称{t.name})print(f 描述{t.description})print()# 第二步, 调用单个工具, 相当于发tools/call请求print( 调用 check_service_health )resultawaitclient.call_tool(check_service_health,{service_name:user-service},)# structured_content是结构化输出, FastMCP根据返回类型自动生成print(f 结果{result.structured_content})print()# 第三步, 调用批量工具, 一次查多个服务print( 调用 batch_check_services )resultawaitclient.call_tool(batch_check_services,{services:[user-service,order-service,payment-service]},)print(f 汇总{result.structured_content})if__name____main__:asyncio.run(main())效果验证把两个文件放同一目录先装好fastmcp然后跑client_test.py。输出大致是这样的。 发现的工具 名称 check_service_health 描述 检查指定服务的健康状态, 返回状态码和响应时间. 名称 batch_check_services 描述 批量检查多个服务的健康状态, 返回汇总结果. 调用 check_service_health 结果 {service: user-service, status: healthy, latency_ms: 42.3, checked_at: 2026-08-09T10:00:00Z} 调用 batch_check_services 汇总 {total: 3, healthy: 2, unhealthy: 1, details: [...]}客户端先list到两个工具再分别调用拿到结构化的返回结果。如果你接的是真实的Claude Desktop或Cursor这类支持MCP的客户端模型会自动读工具列表用户问查一下user-service状态时模型自己决定调check_service_health。与Function Calling的深度对比很多人问我MCP的Tools和OpenAI Function Calling到底什么区别我做了个表格对比。维度MCP ToolsFunction Calling协议归属开放标准, 厂商无关各家私有规范工具定义inputSchema是标准JSON Schema各家用自己的schema格式发现机制运行时动态tools/list发现静态写死在每次请求里传输层独立Server进程, stdio/SSE/HTTP内嵌在模型API请求里错误处理协议错误和执行错误两层统一返回error模型绑定任何支持MCP的模型都能用绑定特定厂商最核心的区别在解耦。Function Calling的工具定义和模型API绑死你换一家模型供应商schema格式可能要改调用方式也要改。MCP把工具抽成独立的Server进程模型只要会说MCP协议就能用你的工具工具一次编写到处跑。我自己的体感是小项目用Function Calling上手快但工具超过五六个、又要支持多个模型客户端的时候MCP的维护成本明显更低。我那个运维助手后来接了Claude和Gemini两个客户端工具代码一行没改。常见问题与避坑坑1参数没写类型注解导致schema退化。FastMCP靠类型注解生成inputSchema漏写注解的参数会被当成string。模型传数字进来做的是字符串操作结果就错了。每个参数都写明类型用Field加description既准确又能帮模型理解参数含义。坑2工具内部异常和协议错误混淆。工具执行失败应该返回isErrortrue的结果让框架返回JSON-RPC error会误导客户端。前者告诉客户端工具调用了但失败了后者让客户端以为调用本身有问题。用try-except包住业务逻辑返回结构化的错误信息。坑3忘记处理list_changed通知。动态增删工具后不发或不处理notifications/tools/list_changed客户端用旧列表新工具调不到。Server端确保声明listChanged能力并触发通知客户端监听后重新拉列表。坑4同步工具阻塞事件循环。FastMCP的同步工具默认跑在线程池里但你的工具如果调了别的asyncio代码或持有GIL很久还是会卡。I/O密集的工具尽量用async def让事件循环自己调度。坑5outputSchema和structuredContent不匹配。新版规范支持outputSchema但你返回的structuredContent必须严格匹配schema否则客户端校验失败。定义了outputSchema就要保证返回结构对得上类型注解写准。小结Tools是MCP五大原语里使用频率最高的一个它让模型获得执行能力。核心要点有三个inputSchema用标准JSON Schema描述参数调用走tools/list和tools/call两步错误处理分协议级和执行级两层。和Function Calling相比MCP Tools的优势在协议开放、运行时动态发现、和模型解耦。下一篇我们看Resources原语它解决的是让模型读数据的问题和Tools形成互补。相关推荐MCP三大原语初体验Tools、Resources、Prompts一个都不少工具开发实战参数校验、错误处理与异步工具Resources原语让AI读取你的数据