ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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固定为object,properties里定义每个参数的类型和描述,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/list,Server返回当前所有工具的元数据列表。如果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住,改成返回isError=true的文本结果,客户端就能正确区分"工具调用失败"和"工具本身不存在"了。

完整代码

下面是一个完整的可运行示例,包含Server端和客户端测试脚本。Server提供两个工具,一个查服务状态,一个批量检查。客户端用FastMCP的Client做in-process测试。

server.py

# server.py MCP Tools原语完整示例# 运行方式 python server.py# 依赖安装 pip install fastmcpfromfastmcpimportFastMCPfrompydanticimportFieldimportrandom# 创建MCP服务器实例, 给它起个名字mcp=FastMCP(name="OpsToolsServer")# 抽出公共逻辑, 避免工具之间代码重复# 这个函数不会被注册成工具, 因为没有加装饰器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_healthy=random.random()>0.3latency_ms=round(random.uniform(10,200),1)return{"service":service_name,"status":"healthy"ifis_healthyelse"unhealthy","latency_ms":latency_ms,"checked_at":"2026-08-09T10:00:00Z",}@mcp.tooldefcheck_service_health(service_name:str=Field(description="要检查的服务名称, 比如user-service"),)->dict:"""检查指定服务的健康状态, 返回状态码和响应时间. 这个工具模拟查询线上服务的健康检查接口, 实际项目中把_check_one里的逻辑替换成真实HTTP调用即可. """# 直接复用辅助函数, 保持工具函数本身的简洁return_check_one(service_name)@mcp.tooldefbatch_check_services(services:list[str]=Field(description="要批量检查的服务名称列表"),)->dict:"""批量检查多个服务的健康状态, 返回汇总结果. 接收服务名列表, 逐个调用检查逻辑, 最后统计健康和不健康的数量, 方便一次性看全局. """results=[]healthy_count=0forsvcinservices:# 复用单个检查逻辑result=_check_one(svc)results.append(result)ifresult.get("status")=="healthy":healthy_count+=1return{"total":len(services),"healthy":healthy_count,"unhealthy":len(services)-healthy_count,"details":results,}if__name__=="__main__":# 以stdio模式启动服务器, 供MCP客户端连接# 也可以换 transport="sse" 走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请求tools=awaitclient.list_tools()print("=== 发现的工具 ===")fortintools:print(f" 名称{t.name}")print(f" 描述{t.description}")print()# 第二步, 调用单个工具, 相当于发tools/call请求print("=== 调用 check_service_health ===")result=awaitclient.call_tool("check_service_health",{"service_name":"user-service"},)# structured_content是结构化输出, FastMCP根据返回类型自动生成print(f" 结果{result.structured_content}")print()# 第三步, 调用批量工具, 一次查多个服务print("=== 调用 batch_check_services ===")result=awaitclient.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,工具内部异常和协议错误混淆。工具执行失败应该返回isError=true的结果,让框架返回JSON-RPC error会误导客户端。前者告诉客户端"工具调用了但失败了",后者让客户端以为"调用本身有问题"。用try-except包住业务逻辑,返回结构化的错误信息。

坑3,忘记处理list_changed通知。动态增删工具后,不发或不处理notifications/tools/list_changed,客户端用旧列表,新工具调不到。Server端确保声明listChanged能力并触发通知,客户端监听后重新拉列表。

坑4,同步工具阻塞事件循环。FastMCP的同步工具默认跑在线程池里,但你的工具如果调了别的asyncio代码或持有GIL很久,还是会卡。I/O密集的工具尽量用async def,让事件循环自己调度。

坑5,outputSchema和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读取你的数据
返回列表