1. 工具是什么? #
工具(Tools) 是 MCP 服务器暴露的可执行函数,供大模型根据对话内容自动调用。
| 对比 | 资源(Resources) | 工具(Tools) |
|---|---|---|
| 作用 | 读数据 | 做事情 |
| 谁决定用 | 用户 / 应用 | 大模型 |
| 类比 | 打开文件看内容 | 按一下按钮执行操作 |
典型例子:查天气、搜数据库、发 HTTP 请求、创建 GitHub Issue。
你需要知道的两个协议方法:
| 方法 | 作用 | 客户端 SDK 写法 |
|---|---|---|
tools/list |
列出所有工具 | await session.list_tools() |
tools/call |
执行指定工具 | await session.call_tool(name, args) |
2. 从用户提问到工具执行 #
用户问「杭州天气怎么样」时,完整链路如下:
sequenceDiagram
participant U as 用户
participant App as AI应用
participant LLM as 大模型
participant S as MCP服务器
App->>S: tools/list
S-->>App: [get_weather, ...]
U->>App: 杭州天气怎么样?
App->>LLM: 问题 + 工具描述
LLM-->>App: 调用 get_weather(city=杭州)
App->>S: tools/call
S-->>App: {"temp":"22°C",...}
App->>LLM: 工具结果
LLM-->>App: 自然语言回答
App-->>U: 杭州今天 22°C,晴
在 Cursor 里,「App」就是编辑器内置的 MCP 客户端,工具列表获取和调用大多自动完成;你自己写服务器时,重点是把工具注册好、描述写清楚。
3. 工具长什么样? #
每个工具对外暴露三个核心字段(FastMCP 会根据函数自动生成):
| 字段 | 含义 | 举例 |
|---|---|---|
name |
唯一标识,调用时用 | get_weather |
description |
功能说明,帮模型决定何时调用 | 查询指定城市的天气 |
inputSchema |
参数的 JSON Schema | {city: string} |
3.1 list 响应示例(Inspector 里可见) #
{
"tools": [
{
"name": "get_weather",
"description": "查询指定城市的天气",
"inputSchema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名" }
},
"required": ["city"]
}
}
]
}3.2 call 请求与响应示例 #
请求:
{
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "city": "杭州" }
}
}响应:
{
"content": [
{ "type": "text", "text": "{\"city\": \"杭州\", \"temp\": \"22°C\"}" }
],
"isError": false
}4. 服务器端:注册工具 #
用 FastMCP 时,把 Python 函数用 `@mcp.tool()` 装饰,就是注册工具。无需手写 tools/list / tools/call。
4.1 最简工具(无参数) #
tool_server.py
# 导入 FastMCP 库,提供构建多工具服务器的能力
from mcp.server.fastmcp import FastMCP
# 创建一个 FastMCP 服务器实例,服务器名称为 "hello-server"
mcp = FastMCP(name="hello-server")
# 使用 @mcp.tool() 装饰器注册 greet 工具,工具名默认为函数名
@mcp.tool()
# 定义 greet 函数,接收可选参数 name,默认为 "World",返回问候字符串
def greet(name: str = "World") -> str:
# 返回格式化的问候字符串
return f"Hello, {name}!"
# 判断是否作为主模块运行
if __name__ == "__main__":
# 启动服务器,使用 stdio 作为传输方式
mcp.run(transport="stdio")4.2 带参数的工具 #
函数参数会自动变成 inputSchema。类型注解和 docstring 会帮助生成更准确的描述:
# 导入json库,用于进行JSON格式的数据编码和解码
import json
# 从mcp.server.fastmcp模块导入FastMCP类,用于创建MCP服务器
from mcp.server.fastmcp import FastMCP
# 创建一个FastMCP服务器实例,服务器名称为"weather-server"
mcp = FastMCP(name="weather-server")
# 使用mcp.tool装饰器注册天气查询工具
@mcp.tool()
# 定义get_weather函数,入参为城市名,返回类型为字符串
def get_weather(city: str) -> str:
# 函数文档字符串,描述该工具的用途
"""查询指定城市的当前天气。"""
# 构建包含城市、温度和天气状况的字典数据
data = {"city": city, "temp": "22°C", "condition": "晴"}
# 将字典数据转换为JSON字符串并返回,确保中文字符正常显示
return json.dumps(data, ensure_ascii=False)
# 如果当前模块作为主程序运行,则启动服务器
if __name__ == "__main__":
# 启动MCP服务器,指定传输方式为stdio(标准输入输出)
mcp.run(transport="stdio")4.3 自定义工具名和描述 #
# 导入 json 库,用于数据的 JSON 编解码
import json
# 从 mcp.server.fastmcp 模块导入 FastMCP,用来创建 MCP 服务器实例
from mcp.server.fastmcp import FastMCP
# 创建 FastMCP 服务器实例,命名为 "city-weather-server"
mcp = FastMCP(name="city-weather-server")
# 使用 mcp.tool 装饰器注册工具,指定工具名为 "search_city_weather",描述为"根据城市名查询实时天气"
@mcp.tool(name="search_city_weather", description="根据城市名查询实时天气")
# 定义 get_weather 函数,接收一个城市名参数 city,返回字符串类型
def get_weather(city: str) -> str:
# 为函数添加文档字符串,描述函数作用
"""根据城市名查询实时天气。"""
# 定义包含城市、温度和天气状况的字典,模拟返回结果
data = {
"city": city,
"temp": "25°C",
"condition": "多云"
}
# 将字典数据编码为 json 字符串并返回,ensure_ascii=False 保证中文正常显示
return json.dumps(data, ensure_ascii=False)
# 判断当前模块是否为主模块(直接运行而非被导入)
if __name__ == "__main__":
# 启动 MCP 服务器,指定传输方式为 stdio(标准输入输出)
mcp.run(transport="stdio")4.4 返回值怎么处理? #
| 你返回的类型 | 客户端看到什么 |
|---|---|
str |
content 里一条 type: text |
dict / list |
FastMCP 会序列化成文本 |
| 抛异常 | isError: true,错误信息在 content 里 |
4.5 启动服务器 #
npx @modelcontextprotocol/inspector uv run tool_server.py5. 客户端:列出并调用工具 #
初始化完成后,才能 list_tools 和 call_tool。
5.1 示例 #
# 异步客户端
# 导入异步IO模块
import asyncio
# 导入os模块用于路径操作
import os
# 导入sys模块以便使用解释器路径
import sys
# 从mcp包导入ClientSession和StdioServerParameters类
from mcp import ClientSession, StdioServerParameters
# 从mcp.client.stdio模块导入stdio_client函数
from mcp.client.stdio import stdio_client
# 定义主异步函数
async def main() -> None:
# 获取当前文件的绝对路径所在目录
base_dir = os.path.dirname(os.path.abspath(__file__))
# 构造server端脚本的绝对路径
server_path = os.path.join(base_dir, "tool_server.py")
# 创建StdioServerParameters对象,配置服务器执行命令和参数
server_params = StdioServerParameters(
command=sys.executable, # Python可执行文件
args=[server_path], # 指定要运行的服务端脚本
)
# 使用stdio_client启动异步通信,获取读写流
async with stdio_client(server_params) as (read, write):
# 创建客户端会话
async with ClientSession(read, write) as session:
# ① 必须先初始化
# 初始化会话
await session.initialize()
# ② 列出所有工具
# 列出所有可用工具
tools = await session.list_tools()
# 打印工具名称列表
print("工具列表:", [t.name for t in tools.tools])
# 打印某个工具的参数 schema(可选,调试用)
# 遍历所有工具,打印名称、描述和入参schema
for t in tools.tools:
print(f" - {t.name}: {t.description}")
print(f" 参数: {t.inputSchema}")
# ③ 调用工具
# 调用名为greet的工具并传递参数
result = await session.call_tool("greet", {"name": "MCP"})
# ④ 读取返回内容
# 遍历返回内容中的所有block
for block in result.content:
# 如果block对象具有text属性
if hasattr(block, "text"):
# 打印调用结果文本
print("调用结果:", block.text)
# 若工具执行失败,检查 isError
# 如果调用结果有错误,打印提示信息
if result.isError:
print("工具执行出错")
# 如果当前脚本是主程序入口
if __name__ == "__main__":
# 运行异步主函数
asyncio.run(main())5.2 调用带参数的工具 #
tool_client.py
# 导入操作系统相关模块
import os
# 导入与Python解释器交互的sys模块
import sys
# 导入异步IO模块
import asyncio
# 从mcp.rpc模块导入StdioServerParameters、stdio_client和ClientSession类
from mcp.rpc import StdioServerParameters, stdio_client, ClientSession
# 定义主异步函数
async def main():
# 获取当前文件所在的目录路径
base_dir = os.path.dirname(os.path.abspath(__file__))
# 拼接得到服务端脚本的完整路径
server_path = os.path.join(base_dir, "02_full_server.py")
# 创建StdioServerParameters对象,指定Python解释器和服务端脚本路径
server_params = StdioServerParameters(
command=sys.executable,
args=[server_path],
)
# 使用stdio_client建立与服务端的异步通信,获取读写流
async with stdio_client(server_params) as (read, write):
# 创建客户端会话对象
async with ClientSession(read, write) as session:
# 初始化会话(必须先调用)
await session.initialize()
# 异步调用名为"get_weather"的工具,传入参数{"city": "杭州"}
result = await session.call_tool("get_weather", {"city": "杭州"})
# 遍历返回结果内容中的每一个block
for block in result.content:
# 判断block对象是否有"text"属性
if hasattr(block, "text"):
# 打印每个结果文本
print(block.text)
# 调用get_weather示例返回: {"city": "杭州", "temp": "22°C", "condition": "晴"}
# 判断当前脚本是否作为主模块运行
if __name__ == "__main__":
# 运行异步主函数
asyncio.run(main())5.3 运行 #
uv run tool_client.py期望输出:
工具列表: ['greet']
- greet:
参数: {'properties': {'name': {'default': 'World', 'title': 'Name', 'type': 'string'}}, 'title': 'greetArguments', 'type': 'object'}
调用结果: Hello, MCP!5.4 list_tools() 返回什么? #
| 属性 | 含义 |
|---|---|
tools.tools |
工具对象列表 |
tool.name |
工具名 |
tool.description |
描述 |
tool.inputSchema |
参数 JSON Schema |
5.5 call_tool() 参数说明 #
# 第一个参数:工具名(字符串,与 @mcp.tool 注册名一致)
# 第二个参数:参数字典(键名与函数参数名一致)
result = await session.call_tool("get_weather", {"city": "上海"})| 常见错误 | 原因 |
|---|---|
Unknown tool |
工具名拼错,或服务器未注册该工具 |
| 参数无效 | 缺少必填参数,或类型不对 |
| 未 initialize | 忘了先调用 await session.initialize() |
6. 用 Inspector 测试工具 #
不写客户端代码,也能快速验证工具是否注册正确。
6.1 启动 #
npx @modelcontextprotocol/inspector uv run tool_server.py6.2 操作步骤 #
| 步骤 | 操作 |
|---|---|
| 1 | 浏览器打开 Inspector,确认 Connected |
| 2 | 切到 Tools 标签 |
| 3 | 选中工具(如 greet 或 get_weather) |
| 4 | 在参数表单里填值 |
| 5 | 点击执行,查看返回 JSON |
6.3 用 Inspector 排查问题 #
| 现象 | 可能原因 |
|---|---|
| Tools 列表为空 | 没有 `@mcp.tool()`,或服务器启动失败 |
| 执行报错 | 参数缺失、函数内部异常 |
| 连接失败 | 命令路径错误,先终端手动跑同样命令 |
7. 写好工具的三条建议 #
工具描述直接影响模型会不会、能不能正确调用。
| 建议 | 好的做法 | 差的做法 |
|---|---|---|
| 名称清晰 | get_weather、search_flights |
tool1、doStuff |
| 描述具体 | 查询指定城市的实时温度和天气状况 |
天气工具 |
| 参数有说明 | 用类型注解 + docstring | 参数名 a、b |
函数 docstring 示例:
@mcp.tool()
def search_flights(origin: str, destination: str, date: str) -> str:
"""搜索两个城市之间的可用航班。
Args:
origin: 出发城市,如「北京」
destination: 到达城市,如「上海」
date: 出发日期,格式 YYYY-MM-DD
"""
...