深入 FastMCP 源码:认识 tool()、resource() 和 prompt() 装饰器
在使用 FastMCP 开发 MCP 服务器时经常会用到
@mcp.tool()等装饰器。虽然它们用起来很简单,但当作黑匣子总让人感觉「不得劲」。接下来我们将深入相关的源码实现,别担心,不会钻没有意义的「兔子洞」,你可以通过这篇文章了解到:
- 如何简单启动本地的 MCP Server 和 MCP Inspector
- 这些装饰器具体做了什么
- @mcp.tool()
- @mcp.resource()
- @mcp.prompt()
MCP 官方 Python SDK 地址:https://github.com/modelcontextprotocol/python-sdk。
由于平台限制,本文大多数链接无法直接跳转,如果对蓝色链接感兴趣,可以访问原文:
https://github.com/Hoper-J/AI-Guide-and-Demos-zh_CN/blob/master/Guide/深入 FastMCP 源码:认识 tool()、resource() 和 prompt() 装饰器.md
深入 FastMCP 源码:认识 tool()、resource() 和 prompt() 装饰器
1. 安装库
需要注意的是,Python >= 3.10 才可以安装 MCP
pip install mcp
2. server.py 示例
下面是一个简化的 server.py 示例:
from mcp.server.fastmcp import FastMCP
# 初始化 FastMCP server
mcp = FastMCP(
name="weather",
#host="0.0.0.0",
#port="8234"
)
@mcp.tool()
def get_weather(city: str) -> str:
"""获取指定城市的天气信息"""
# 简单模拟数据,实际应用中应该调用对应的API
weather_data = {
"北京": "晴天,温度 22°C",
"上海": "多云,温度 25°C",
"广州": "小雨,温度 28°C",
"深圳": "阴天,温度 26°C"
}
return weather_data.get(city, f"{city} 的天气数据暂不可用")
@mcp.prompt()
def weather_assistant():
"""提供天气查询的对话模板"""
return "我是你的天气助手,可以帮你查询各个城市的天气情况。请告诉我你想了解哪个城市的天气?"
@mcp.resource("resource://cities")
def get_cities():
"""返回支持查询天气的城市列表"""
cities = ["北京", "上海", "广州", "深圳"]
return f"Cities: {', '.join(cities)}"
@mcp.resource("resource://{city}/weather")
def get_weather(city: str) -> str:
return f"Weather for {city}"
if __name__ == "__main__":
mcp.run(transport="stdio")
将其保存为 server.py 后,可以使用以下命令直接进行调试:
mcp dev server.py
# 如果克隆了仓库,可以指定 demos 文件夹下的路径,比如:mcp dev demos/mcp/server.py
mcp dev 会在运行 MCP 服务器的同时启动 MCP Inspector:
MCP Inspector 界面配置如下图左框:
配置项(Command + Arguments)实际对应于能够运行服务器的命令,所以并不局限,有很多组合可以使用:
| Command | Arguments |
|---|---|
| mcp | run server.py |
| python | server.py |
| uv | run server.py |
| uv | run mcp run server.py |
| uv | run --with mcp mcp run server.py |
| uv | run python server.py |
最后三行命令实际只是 uv 对前两行命令的封装(uv 可以替代 pip/conda,目前已经被广泛使用)。
连接成功后,你可以在 MCP Inspector 中看到注册的 Resources、Prompts 和 Tools:
| Resources | Prompts | Tools |
|---|---|---|
3. 什么是 FastMCP?
官方仓库中对应的路径为 src/mcp/server/fastmcp:
从 from mcp.server.fastmcp import FastMCP 开始,既然能够直接 import FastMCP,那先查看 __init__.py:
"""FastMCP - 一个更人性化的 MCP 服务器接口。"""
from importlib.metadata import version
from .server import Context, FastMCP
from .utilities.types import Image
__version__ = version("mcp")
__all__ = ["FastMCP", "Context", "Image"]
可以看到 FastMCP 是从当前文件夹的 server.py 中导入的,所以接下来查看 server.py(省略部分初始化逻辑):
class FastMCP:
def __init__(
self,
name: str | None = None,
instructions: str | None = None,
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,
token_verifier: TokenVerifier | None = None,
event_store: EventStore | None = None,
*,
tools: list[Tool] | None = None,
**settings: Any,
):
...
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
...
初始化(__init__)的代码中有三个很眼熟的部分:
_tool_manager:管理工具(Tools)_resource_manager:管理资源(Resources)_prompt_manager:管理提示词(Prompts)
这三者分别对应于之后要介绍的装饰器。
4. 装饰器
4.1 什么是装饰器?
装饰器可以理解为一个接受函数作为参数,并返回一个新函数的函数。这可以让我们在不修改原函数代码的情况下添加通用的行为,通过一个简单的例子来理解:
def decorator(func):
"""一个简单的装饰器示例"""
def wrapper(*args, **kwargs):
print(f"调用函数 {func.__name__} 之前")
result = func(*args, **kwargs)
print(f"调用函数 {func.__name__} 之后\n")
return result
return wrapper
# 方式1:使用 @ 语法糖
@decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Xiaoming")
# 方式2:直接调用装饰器函数
def say_hello(name):
print(f"Hello, {name}!")
say_hello = decorator(say_hello)
say_hello("Xiaoming")
输出:
调用函数 say_hello 之前
Hello, Xiaoming!
调用函数 say_hello 之后
调用函数 say_hello 之前
Hello, Xiaoming!
调用函数 say_hello 之后
在 FastMCP 中,装饰器的工作方式类似,但并不是简单地 print,而是将函数注册到对应的管理器中:
@mcp.tool()- 将函数注册为工具@mcp.resource()- 将函数注册为资源@mcp.prompt()- 将函数注册为提示词模板
接下来会着重讲解 tool() 装饰器,resource() 和 prompt() 的处理逻辑基本是 tool() 的简化版本。
Note
使用
@mcp.*的格式是因为初始化mcp=FastMCP(),如果变量名从mcp改为了server,即:server=FastMCP(),那么装饰器就应该使用@server.*的格式。
4.2 @mcp.tool()
@mcp.tool() 装饰器用于将 Python 函数自动注册为当前 mcp 服务器中的工具。摘选之前的片段:
@mcp.tool()
def get_weather(city: str) -> str:
"""获取指定城市的天气信息"""
# 简单模拟数据,实际应用中应该调用对应的API
weather_data = {
"北京": "晴天,温度 22°C",
"上海": "多云,温度 25°C",
"广州": "小雨,温度 28°C",
"深圳": "阴天,温度 26°C"
}
return weather_data.get(city, f"{city} 的天气数据暂不可用")
这段代码实际上会:
- 自动提取函数的参数类型信息(下面的信息由之后的
debug_func_metadata打印),以及文档字符串。
函数签名分析:
完整签名: (city: str) -> str
返回类型: <class 'str'>
参数数量: 1
参数名: city
原始注解: <class 'str'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
类型化注解: <class 'str'>
字段信息: annotation=<class 'str'>, default=PydanticUndefined
- 生成参数的 JSON Schema(函数名 + Arguments)。
🏗️ 创建 Pydantic 模型:
模型名称: get_weatherArguments
基类: <class 'mcp.server.fastmcp.utilities.func_metadata.ArgModelBase'>
✅ 模型创建成功: <class '__main__.get_weatherArguments'>
get_weatherArguments JSON Schema:
{
"properties":{
"city":{
"title":"City",
"type":"string"
}
},
"required":[
"city"
],
"title":"get_weatherArguments",
"type":"object"
}
- 将函数注册为 MCP 工具(
self._tools[tool.name] = tool)。
另外,mcp.tool() 可以接受以下参数(此处参数解释参考源代码的 tool 和 func_metadata):
- name: 可选的工具名称,默认为函数名
- title: 可选的工具标题(用于人类阅读)
- description: 可选的工具功能描述,默认使用函数的文档字符串
- annotations: 可选的 ToolAnnotations,提供额外的工具信息
- structured_output:控制工具输出是结构化还是非结构化的
None: 基于函数的返回类型注解自动检测True: 无条件创建结构化工具(在返回类型注解允许的情况下)
如果是结构化,会根据函数的返回类型注释创建 Pydantic 模型。支持各种返回类型:
- BaseModel 子类(直接使用)
- 原始类型(str、int、float、bool、bytes、None)- 包装在带有 ‘result’ 字段的模型中
- TypedDict - 转换为具有相同字段的 Pydantic 模型
- 数据类和其他带注释的类 - 转换为 Pydantic 模型
- 泛型类型(list、dict、Union 等)- 包装在带有 ‘result’ 字段的模型中
False: 无条件创建非结构化工具
Note
如果你只想了解如何使用
@mcp.tool(),可以跳过下面的源码部分。
4.2.1 溯源码
class FastMCP:
...
def tool(
self,
name: str | None = None,
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
structured_output: bool | None = None,
) -> Callable[[AnyFunction], AnyFunction]:
"""用于注册工具的装饰器。
工具可以通过添加 Context 类型注解的参数来可选地请求一个 Context 对象。
Context 提供对 MCP 功能的访问,包括日志记录、进度报告和资源访问。
"""
# 检查装饰器是否被正确使用(需要带括号调用)
if callable(name):
raise TypeError(
"The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool"
)
def decorator(fn: AnyFunction) -> AnyFunction:
self.add_tool(
fn,
name=name,
title=title,
description=description,
annotations=annotations,
structured_output=structured_output,
)
return fn
return decorator
这里的 self.add_tool() 就是 mcp.add_tool(),所以也可以不使用装饰器达到一样的目的:
def get_weather():
pass
mcp = FastMCP(name="weather")
mcp.add_tool(get_weather) # 和 get_weather 使用@mcp.tool()效果一样
这一行为最终调用的是 self._tool_manager.add_tool(),self._tool_manager 在 __init__() 中对应的是 tools/tool_manager.py 中的 ToolManager 类:
class ToolManager:
"""管理 FastMCP 工具."""
def __init__(
self,
warn_on_duplicate_tools: bool = True,
*,
tools: list[Tool] | None = None,
):
self._tools: dict[str, Tool] = {}
if tools is not None:
for tool in tools:
if warn_on_duplicate_tools and tool.name in self._tools:
logger.warning(f"Tool already exists: {tool.name}")
self._tools[tool.name] = tool
self.warn_on_duplicate_tools = warn_on_duplicate_tools
def add_tool(
self,
fn: Callable[..., Any],
name: str | None = None,
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
structured_output: bool | None = None,
) -> Tool:
"""添加 tool 到 server。"""
tool = Tool.from_function(
fn,
name=name,
title=title,
description=description,
annotations=annotations,
structured_output=structured_output,
)
existing = self._tools.get(tool.name)
if existing:
if self.warn_on_duplicate_tools:
logger.warning(f"Tool already exists: {tool.name}")
return existing
self._tools[tool.name] = tool
return tool
我们不需要关注 ToolManager 是怎么进行管理的,这不重要,重要的是装饰器怎么处理我们自定义的函数。
整个 @mcp.tool() 装饰器的工作按执行顺序可以拆分为:
- 装饰器检查调用方式是否正确(必须带括号),然后将被装饰的函数传递给
ToolManager.add_tool()。
class FastMCP:
...
def tool(self, name, ...):
if callable(name):
raise TypeError(
"The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool"
)
def decorator(fn: AnyFunction) -> AnyFunction:
self.add_tool(
fn, # 被装饰的函数
name=name,
title=title,
description=description,
annotations=annotations,
structured_output=structured_output,
)
return fn
return decorator
Tool.from_function()处理函数元数据。
class ToolManager:
...
def add_tool(self, ...):
tool = Tool.from_function(
fn, # 被装饰的函数
name=name,
title=title,
description=description,
annotations=annotations,
structured_output=structured_output,
)
...
tool = Tool.from_function(…)是当前最重要的处理部分,对应代码位于 tools/base.py,其主要步骤如下(代码按顺序拼接等价于 Tool.from_function(),出于讲解目的将其进行了拆分):
a. 解析函数签名,提取参数信息。
class Tool(BaseModel):
...
@classmethod
def from_function(
cls, # 这里的 cls 就是 Tool 类本身,不是实例
fn: Callable[..., Any],
name: str | None = None,
title: str | None = None,
description: str | None = None,
context_kwarg: str | None = None,
annotations: ToolAnnotations | None = None,
structured_output: bool | None = None,
) -> Tool:
"""从函数创建工具."""
from mcp.server.fastmcp.server import Context
func_name = name or fn.__name__
# Lambda 函数必须提供 name 参数
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")
# 如果没有传入 description,则使用函数的文档字符串
func_doc = description or fn.__doc__ or ""
is_async = _is_async_callable(fn)
...
b. 自动检测 Context 参数,inspect.signature() 会遍历函数的所有参数,检查参数类型是否为 Context 的子类,如果是的话会记录为 context_kwarg,这个参数会被传入 func_metadata() 的 skip_names(可以跳过这一步的理解,等真正涉及到的时候再探究),不会出现在工具对应的 JSON Schema 中。
class Tool(BaseModel):
...
@classmethod
def from_function(...):
...
# 自动检测 Context 参数
if context_kwarg is None:
sig = inspect.signature(fn)
for param_name, param in sig.parameters.items():
# 跳过泛型类型
if get_origin(param.annotation) is not None:
continue
# 检查参数类型是否是 Context 的子类
if issubclass(param.annotation, Context):
context_kwarg = param_name
break
c. 生成参数的 JSON Schema:
class Tool(BaseModel):
...
@classmethod
def from_function(...):
...
# 生成函数元数据,包括参数的 JSON Schema
func_arg_metadata = func_metadata(
fn,
skip_names=[context_kwarg] if context_kwarg is not None else [],
structured_output=structured_output,
)
# 从 Pydantic 模型生成 JSON Schema
parameters = func_arg_metadata.arg_model.model_json_schema(by_alias=True)
func_metadata 相关源代码位于 utilities/func_metadata.py,这里我们进行主体逻辑的抽取打印(完整 debug_func_metadata 函数见附录):
def func(
data, # 无类型注解
format: str = "json", # 有注解+默认值
count: Optional[int] = None, # 复杂类型+默认值
validate: bool = True # 基础类型+默认值
): # 无返回类型注解'
"""展示各种注解情况"""
return data
debug_func_metadata(func, skip_names="count")
输出:
============================================================
🔍 开始解析函数: func
文档字符串: 展示各种注解情况
============================================================
📋 函数签名分析:
完整签名: (data, format: str = 'json', count: Optional[int] = None, validate: bool = True)
返回类型: <class 'inspect._empty'>
参数数量:4
🔧 参数处理详情:
跳过的参数名: ['c', 'o', 'u', 'n', 't']
[1] 参数名: data
原始注解: <class 'inspect._empty'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
⚠️ 处理: 无类型注解,默认为 Any
🔄 类型化注解: typing.Annotated[typing.Any, FieldInfo(annotation=NoneType, required=True), WithJsonSchema(json_schema={'title': 'data', 'type': 'string'}, mode=None)]
✅ 字段信息: annotation=typing.Any, default=PydanticUndefined
[2] 参数名: format
原始注解: <class 'str'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: json
🔄 类型化注解: <class 'str'>
✅ 字段信息: annotation=<class 'str'>, default=json
[3] 参数名: count
原始注解: typing.Optional[int]
参数种类: POSITIONAL_OR_KEYWORD
默认值: None
⏭️ 跳过此参数
[4] 参数名: validate
原始注解: <class 'bool'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: True
🔄 类型化注解: <class 'bool'>
✅ 字段信息: annotation=<class 'bool'>, default=True
⚠️ 冲突处理: 参数名 'validate' 与 BaseModel 方法冲突
-> 使用内部名称: field_validate
📊 参数处理总结:
总参数数:4
处理参数数:3
模型字段: ['data', 'format', 'field_validate']
🏗️ 创建 Pydantic 模型:
模型名称: funcArguments
基类: <class 'mcp.server.fastmcp.utilities.func_metadata.ArgModelBase'>
✅ 模型创建成功: <class '__main__.funcArguments'>
📄 funcArguments JSON Schema:
{
"properties":{
"data":{
"title":"data",
"type":"string"
},
"format":{
"default":"json",
"title":"Format",
"type":"string"
},
"validate":{
"default":true,
"title":"Validate",
"type":"boolean"
}
},
"required":[
"data"
],
"title":"funcArguments",
"type":"object"
}
🎯 返回值处理:
structured_output 参数: None
返回注解: <class 'inspect._empty'>
经过_get_typed_annotation处理后的类型: <class 'inspect._empty'>
ℹ️ 未创建输出模型
wrap_output: False
✨ func_metadata 处理完成!
最终结果: arg_model=<class '__main__.funcArguments'> output_schema=None output_model=None wrap_output=False
============================================================
d. 创建 Tool 实例(Tool.from_function 的 return cls(…))。
class Tool(BaseModel):
...
@classmethod
def from_function(...):
return cls( # 使用 cls() 创建 Tool 实例,等价于 Tool()
fn=fn,
name=func_name,
title=title,
description=func_doc,
parameters=parameters,
fn_metadata=func_arg_metadata,
is_async=is_async,
context_kwarg=context_kwarg,
annotations=annotations,
)
- 将 Tool 实例注册到工具管理器中。
class ToolManager:
...
def add_tool(...) -> Tool:
"""添加 tool 到 server。"""
tool = Tool.from_function(...)
existing = self._tools.get(tool.name)
if existing:
if self.warn_on_duplicate_tools:
logger.warning(f"Tool already exists: {tool.name}")
return existing
self._tools[tool.name] = tool
return tool
4.3 @mcp.resource()
@mcp.resource() 装饰器用于定义可供访问的资源,需要注意的是:
- 必须提供一个资源 URI(如
@mcp.resource("resource://cities"))- 资源可以是静态的(每次调用返回相同内容)或动态的(根据参数填充内容)。
- 静态对应于 MCP Inspector 中的
Resources,动态对应于Resources Templates,以下面两个资源为例进行展示:
- 静态对应于 MCP Inspector 中的
- 资源可以是静态的(每次调用返回相同内容)或动态的(根据参数填充内容)。
from mcp.server.fastmcp import FastMCP
# 初始化 FastMCP server
mcp = FastMCP("cities")
@mcp.resource("resource://cities")
def get_cities():
"""返回支持查询天气的城市列表"""
cities = ["北京", "上海", "广州", "深圳"]
return f"Cities: {', '.join(cities)}"
@mcp.resource("resource://{city}/weather")
def get_weather(city: str) -> str:
return f"Weather for {city}"
if __name__ == "__main__":
mcp.run(transport="stdio")
此时 MCP Inspector 的 Resources 模块显示如下:
4.3.1 溯源码
查看 server.py 中的 resource 方法:
class FastMCP:
...
def resource(
self,
uri: str,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> Callable[[AnyFunction], AnyFunction]:
"""用于将函数注册为资源的装饰器。
当资源被读取时,将调用被装饰的函数来动态生成资源内容。
函数可以返回:
- str: 文本内容
- bytes: 二进制内容
- 其他类型: 将自动转换为 JSON 格式
如果 URI 包含参数占位符(如 "resource://{param}")或者函数本身有参数,
该资源将被注册为模板资源。
参数:
uri: 资源的 URI(如 "resource://my-resource" 或 "resource://{param}")
name: 可选的资源名称
title: 可选的资源标题(用于人类阅读)
description: 可选的资源描述
mime_type: 可选的 MIME 类型
使用示例:
# 静态资源
@server.resource("resource://my-resource")
def get_data() -> str:
return "Hello, world!"
# 参数化模板资源
@server.resource("resource://{city}/weather")
def get_weather(city: str) -> str:
return f"Weather for {city}"
"""
# 检查装饰器是否被正确使用(需要带括号调用)
if callable(uri):
raise TypeError(
"The @resource decorator was used incorrectly. Did you forget to call it? Use @resource('uri') instead of @resource"
)
def decorator(fn: AnyFunction) -> AnyFunction:
# 通过 URI 中的 "{}" 和函数自身的参数来检查是否是模版
has_uri_params = "{" in uri and "}" in uri
has_func_params = bool(inspect.signature(fn).parameters)
if has_uri_params or has_func_params:
# (有参数)提取 URI 参数和函数参数
uri_params = set(re.findall(r"{(\w+)}", uri))
func_params = set(inspect.signature(fn).parameters.keys())
# 验证 URI 参数和函数参数是否匹配
if uri_params != func_params:
raise ValueError(
f"Mismatch between URI parameters {uri_params} and function parameters {func_params}"
)
# 注册为模板资源,调用 _resource_manager.add_template()
self._resource_manager.add_template(
fn=fn,
uri_template=uri,
name=name,
title=title,
description=description,
mime_type=mime_type,
)
else:
# (无参数)注册为普通资源
resource = FunctionResource.from_function(
fn=fn,
uri=uri,
name=name,
title=title,
description=description,
mime_type=mime_type,
)
self.add_resource(resource) # 调用 self._resource_manager.add_resource(resource)
return fn
return decorator
ResourceManager 的实现位于 resources/resource_manager.py:
class ResourceManager:
"""管理 FastMCP 资源。"""
def __init__(self, warn_on_duplicate_resources: bool = True):
self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {}
self.warn_on_duplicate_resources = warn_on_duplicate_resources
def add_resource(self, resource: Resource) -> Resource:
"""向管理器中添加资源。
参数:
resource: 要添加的 Resource 实例
返回:
当前添加的资源。如果具有相同 URI 的资源已存在,则返回现有的资源。
"""
logger.debug(
"Adding resource",
extra={
"uri": resource.uri,
"type": type(resource).__name__,
"resource_name": resource.name,
},
)
existing = self._resources.get(str(resource.uri))
if existing:
if self.warn_on_duplicate_resources:
logger.warning(f"Resource already exists: {resource.uri}")
return existing
self._resources[str(resource.uri)] = resource
return resource
def add_template(
self,
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> ResourceTemplate:
"""根据函数添加模版。"""
template = ResourceTemplate.from_function(
fn,
uri_template=uri_template,
name=name,
title=title,
description=description,
mime_type=mime_type,
)
self._templates[template.uri_template] = template
return template
...
对于静态资源,add_resource 方法会直接将 FunctionResource 实例存储在 _resources 字典中。对于动态资源,add_template 方法会创建 ResourceTemplate 实例并存储在 _templates 字典中。
- 静态:
FunctionResource位于 resources/types.py:
class FunctionResource(Resource):
"""通过包装函数来延迟加载数据的资源。
函数只有在资源被读取时才会被调用,允许对可能昂贵的数据进行延迟加载。
这在列出资源时特别有用,因为函数不会被调用,直到资源被实际访问。
函数可以返回:
- str 表示文本内容(默认)
- bytes 表示二进制内容
- 其他类型将被转换为 JSON
"""
fn: Callable[[], Any] = Field(exclude=True)
...
@classmethod
def from_function(
cls,
fn: Callable[..., Any],
uri: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> "FunctionResource":
"""从函数创建 FunctionResource。"""
func_name = name or fn.__name__
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")
# 确保参数被正确转换
fn = validate_call(fn)
return cls(
uri=AnyUrl(uri),
name=func_name,
title=title,
description=description or fn.__doc__ or "",
mime_type=mime_type or"text/plain",
fn=fn,
)
- 动态:
ResourceTemplate位于 resources/templates.py:
class ResourceTemplate(BaseModel):
"""动态创建资源的模板。"""
uri_template: str = Field(description="URI template with parameters (e.g. weather://{city}/current)")
name: str = Field(description="Name of the resource")
title: str | None = Field(description="Human-readable title of the resource", default=None)
description: str | None = Field(description="Description of what the resource does")
mime_type: str = Field(default="text/plain", description="MIME type of the resource content")
fn: Callable[..., Any] = Field(exclude=True)
parameters: dict[str, Any] = Field(description="JSON schema for function parameters")
@classmethod
def from_function(
cls,
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> ResourceTemplate:
"""从函数创建模板。"""
func_name = name or fn.__name__
if func_name == "<lambda>":
raise ValueError("You must provide a name for lambda functions")
# 从 TypeAdapter 获取 schema - 如果函数没有正确的类型注解会失败
parameters = TypeAdapter(fn).json_schema()
# 确保参数被正确转换
fn = validate_call(fn)
return cls(
uri_template=uri_template,
name=func_name,
title=title,
description=description or fn.__doc__ or "",
mime_type=mime_type or"text/plain",
fn=fn,
parameters=parameters,
)
4.4 @mcp.prompt()
@mcp.prompt() 装饰器用于定义提示词模板,这部分的实现只是简单维护了一个字典。
4.4.1 溯源码
查看 server.py 中的 prompt 方法:
class FastMCP:
...
def prompt(
self,
name: str | None = None,
title: str | None = None,
description: str | None = None,
annotations: PromptAnnotations | None = None,
) -> Callable[[AnyFunction], AnyFunction]:
"""注册提示词的装饰器。
参数:
name: 可选的提示词名称(默认使用函数名)
title: 可选的提示词人类可读标题
description: 可选的提示词功能描述
使用示例:
@server.prompt()
def analyze_table(table_name: str) -> list[Message]:
schema = read_table_schema(table_name)
return [
{
"role": "user",
"content": f"Analyze this schema:\n{schema}"
}
]
@server.prompt()
async def analyze_file(path: str) -> list[Message]:
content = await read_file(path)
return [
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": f"file://{path}",
"text": content
}
}
}
]
"""
# 同样的验证逻辑
if callable(name):
raise TypeError(
"The @prompt decorator was used incorrectly. Did you forget to call it? Use @prompt() instead of @prompt"
)
def decorator(func: AnyFunction) -> AnyFunction:
prompt = Prompt.from_function(
func,
name=name,
title=title,
description=description
)
self.add_prompt(prompt) # 调用 self._prompt_manager.add_prompt(prompt)
return func
return decorator
PromptManager 的实现位于 prompts/prompt_manager.py:
class PromptManager:
"""管理 FastMCP 提示词。"""
def __init__(self, warn_on_duplicate_prompts: bool = True):
self._prompts: dict[str, Prompt] = {}
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
def add_prompt(self, prompt: Prompt) -> Prompt:
"""添加提示词到管理器。"""
logger.debug(f"Adding prompt: {prompt.name}")
existing = self._prompts.get(prompt.name)
if existing:
if self.warn_on_duplicate_prompts:
logger.warning(f"Prompt already exists: {prompt.name}")
return existing
self._prompts[prompt.name] = prompt
return prompt
def get_prompt(self, name: str) -> Prompt | None:
"""根据名称获取提示词。"""
return self._prompts.get(name)
def list_prompts(self) -> list[Prompt]:
"""列出所有已注册的提示词。"""
return list(self._prompts.values())
5. 附录
5.1 debug_func_metadata.py
官方源码:utilities/func_metadata.py
调试文件下载:debug_func_metadata.py
#!/usr/bin/env python3
"""
func_metadata 调试工具,可以保存为 debug_func_metadata.py 执行
"""
import inspect
import json
from typing import Any, Callable, Sequence, Optional, List, Dict
from pydantic import BaseModel, Field, create_model
from pydantic.fields import FieldInfo
from typing_extensions import Annotated
from mcp.server.fastmcp.utilities.func_metadata import (
FuncMetadata,
ArgModelBase,
_get_typed_signature,
_get_typed_annotation,
_try_create_model_and_schema,
InvalidSignature,
PydanticUndefined,
WithJsonSchema
)
def print_json(data, title="JSON数据"):
"""打印JSON数据"""
try:
print(f"\n📄 {title}:")
print(json.dumps(data, indent=2, ensure_ascii=False))
except Exception as e:
print(f"❌ JSON打印失败: {e}")
print(f"原始数据类型: {type(data)}")
print(f"原始数据: {data}")
def debug_func_metadata(
func: Callable[..., Any],
skip_names: Sequence[str] = (),
structured_output: bool | None = None,
) -> Any:
"""
调试版本的 func_metadata 实现,会用到一些 from_function 中的逻辑,比如:func.__name__,func.__doc__ ...
"""
print(f"\n{'='*60}")
print(f"🔍 开始解析函数: {func.__name__}")
print(f" 文档字符串: {func.__doc__}")
print(f"{'='*60}")
try:
# 从这里开始 func_metadata()
# 步骤1: 获取函数签名
sig = _get_typed_signature(func)
params = sig.parameters
print(f"\n📋 函数签名分析:")
print(f" 完整签名: {sig}")
print(f" 返回类型: {sig.return_annotation}")
print(f" 参数数量: {len(params)}")
# 准备构建动态 Pydantic 模型的参数字典
dynamic_pydantic_model_params: dict[str, Any] = {}
globalns = getattr(func, "__globals__", {})
print(f"\n🔧 参数处理详情:")
print(f" 跳过的参数名: {list(skip_names)}")
# 步骤2: 遍历每个参数
processed_count = 0
for idx, param in enumerate(params.values()):
print(f"\n [{idx+1}] 参数名: {param.name}")
print(f" 原始注解: {param.annotation}")
print(f" 参数种类: {param.kind}")
print(f" 默认值: {param.default}")
# 验证参数名
if param.name.startswith("_"):
print(f" ❌ 错误: 参数名不能以 '_' 开头")
raise InvalidSignature(f"{func.__name__} 的参数 {param.name} 不能以 '_' 开头")
if param.name in skip_names:
print(f" ⏭️ 跳过此参数")
continue
processed_count += 1
annotation = param.annotation
# 处理 `x: None` 或 `x: None = None` 的情况
if annotation is None:
print(f" 📝 处理: 类型为 None,添加默认值字段")
annotation = Annotated[
None,
Field(default=param.default if param.default is not inspect.Parameter.empty else PydanticUndefined),
]
if annotation is inspect.Parameter.empty:
print(f" ⚠️ 处理: 无类型注解,默认为 Any")
annotation = Annotated[
Any,
Field(),
# 🤷 默认将无类型参数视为字符串
WithJsonSchema({"title": param.name, "type": "string"}),
]
# 获取类型化注解
typed_annotation = _get_typed_annotation(annotation, globalns)
print(f" 🔄 类型化注解: {typed_annotation}")
# 创建字段信息
field_info = FieldInfo.from_annotated_attribute(
typed_annotation,
param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,
)
print(f" ✅ 字段信息: annotation={field_info.annotation}, default={field_info.default}")
# 处理参数名与 BaseModel 内置方法冲突的情况,这是必要的,因为 Pydantic 会因此发出警告
# 例如:'dict' 或 'json' 等
if hasattr(BaseModel, param.name) and callable(getattr(BaseModel, param.name)):
print(f" ⚠️ 冲突处理: 参数名 '{param.name}' 与 BaseModel 方法冲突")
# 使用别名机制避免警告
field_info.alias = param.name
field_info.validation_alias = param.name
field_info.serialization_alias = param.name
# 内部使用带前缀的参数名
internal_name = f"field_{param.name}"
dynamic_pydantic_model_params[internal_name] = (field_info.annotation, field_info)
print(f" -> 使用内部名称: {internal_name}")
else:
dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
print(f"\n📊 参数处理总结:")
print(f" 总参数数: {len(params)}")
print(f" 处理参数数: {processed_count}")
print(f" 模型字段: {list(dynamic_pydantic_model_params.keys())}")
# 步骤3: 动态创建一个 Pydantic 模型来表示函数参数
arguments_model_name = f"{func.__name__}Arguments"
print(f"\n🏗️ 创建 Pydantic 模型:")
print(f" 模型名称: {arguments_model_name}")
print(f" 基类: {ArgModelBase}")
arguments_model = create_model(
arguments_model_name,
**dynamic_pydantic_model_params,
__base__=ArgModelBase,
)
print(f" ✅ 模型创建成功: {arguments_model}")
# 生成并打印 JSON Schema
try:
# 这部分对应于 func_metadata() 之后的那行代码,提前进行查看
schema = arguments_model.model_json_schema(by_alias=True)
print_json(schema, f"{arguments_model_name} JSON Schema")
except Exception as e:
print(f"❌ Schema 生成失败: {e}")
# 步骤4: 处理返回值(完全按照原版本逻辑)
print(f"\n🎯 返回值处理:")
print(f" structured_output 参数: {structured_output}")
print(f" 返回注解: {sig.return_annotation}")
if structured_output is False:
print(f" 🔚 明确不需要结构化输出")
result = FuncMetadata(arg_model=arguments_model)
print(f" ✅ 返回元数据: {result}")
return result
# 基于返回类型注释设置结构化输出支持
if sig.return_annotation is inspect.Parameter.empty and structured_output is True:
print(f" ❌ 错误: 要求结构化输出但无返回注解")
raise InvalidSignature(f"函数 {func.__name__}: 结构化输出需要返回注释")
output_info = FieldInfo.from_annotation(_get_typed_annotation(sig.return_annotation, globalns))
annotation = output_info.annotation
print(f" 经过_get_typed_annotation处理后的类型: {annotation}")
output_model, output_schema, wrap_output = _try_create_model_and_schema(
annotation, func.__name__, output_info
)
if output_model:
print(f" ✅ 输出模型创建成功: {output_model}")
if output_schema:
print_json(output_schema, "返回值 JSON Schema")
else:
print(f" ℹ️ 未创建输出模型")
print(f" wrap_output: {wrap_output}")
# 模型创建失败或产生警告 - 无结构化输出
if output_model is None and structured_output is True:
print(f" ❌ 结构化输出失败: 返回类型不可序列化")
raise InvalidSignature(
f"函数 {func.__name__}: 返回类型 {annotation} 不支持结构化输出"
)
# 创建最终结果
result = FuncMetadata(
arg_model=arguments_model,
output_schema=output_schema,
output_model=output_model,
wrap_output=wrap_output,
)
print(f"\n✨ func_metadata 处理完成!")
print(f" 最终结果: {result}")
print(f"{'='*60}\n")
return result
except Exception as e:
print(f"❌ 处理过程中出错: {e}")
import traceback
print(f"详细错误信息:\n{traceback.format_exc()}")
return None
def test():
"""测试各种类型的函数"""
# 混合注解
print("\n\n📌 测试1: 混合类型注解")
def func(
data, # 无类型注解
format: str = "json", # 有注解+默认值
count: Optional[int] = None, # 复杂类型+默认值
validate: bool = True # 基础类型+默认值
): # 无返回类型注解
"""展示各种注解情况"""
return data
debug_func_metadata(func, skip_names="count")
# 前缀参数测试
print("\n\n📌 测试2: 前缀参数冲突")
def prefix_func(_private: str, field_test: int) -> str:
"""前缀参数"""
return "test"
debug_func_metadata(prefix_func)
print("\n\n📌 测试3: 结构化输出对比")
def add(a: int, b: int) -> str:
return a + b
print("📌 无结构化")
debug_func_metadata(add, structured_output=False)
print("\n\n📌 结构化")
debug_func_metadata(add, structured_output=True)
if __name__ == "__main__":
test()
输出:
📌 测试1: 混合类型注解
============================================================
🔍 开始解析函数: func
文档字符串: 展示各种注解情况
============================================================
📋 函数签名分析:
完整签名: (data, format: str = 'json', count: Optional[int] = None, validate: bool = True)
返回类型: <class 'inspect._empty'>
参数数量:4
🔧 参数处理详情:
跳过的参数名: ['c', 'o', 'u', 'n', 't']
[1] 参数名: data
原始注解: <class 'inspect._empty'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
⚠️ 处理: 无类型注解,默认为 Any
🔄 类型化注解: typing.Annotated[typing.Any, FieldInfo(annotation=NoneType, required=True), WithJsonSchema(json_schema={'title': 'data', 'type': 'string'}, mode=None)]
✅ 字段信息: annotation=typing.Any, default=PydanticUndefined
[2] 参数名: format
原始注解: <class 'str'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: json
🔄 类型化注解: <class 'str'>
✅ 字段信息: annotation=<class 'str'>, default=json
[3] 参数名: count
原始注解: typing.Optional[int]
参数种类: POSITIONAL_OR_KEYWORD
默认值: None
⏭️ 跳过此参数
[4] 参数名: validate
原始注解: <class 'bool'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: True
🔄 类型化注解: <class 'bool'>
✅ 字段信息: annotation=<class 'bool'>, default=True
⚠️ 冲突处理: 参数名 'validate' 与 BaseModel 方法冲突
-> 使用内部名称: field_validate
📊 参数处理总结:
总参数数:4
处理参数数:3
模型字段: ['data', 'format', 'field_validate']
🏗️ 创建 Pydantic 模型:
模型名称: funcArguments
基类: <class 'mcp.server.fastmcp.utilities.func_metadata.ArgModelBase'>
✅ 模型创建成功: <class '__main__.funcArguments'>
📄 funcArguments JSON Schema:
{
"properties":{
"data":{
"title":"data",
"type":"string"
},
"format":{
"default":"json",
"title":"Format",
"type":"string"
},
"validate":{
"default":true,
"title":"Validate",
"type":"boolean"
}
},
"required":[
"data"
],
"title":"funcArguments",
"type":"object"
}
🎯 返回值处理:
structured_output 参数: None
返回注解: <class 'inspect._empty'>
经过_get_typed_annotation处理后的类型: <class 'inspect._empty'>
ℹ️ 未创建输出模型
wrap_output: False
✨ func_metadata 处理完成!
最终结果: arg_model=<class '__main__.funcArguments'> output_schema=None output_model=None wrap_output=False
============================================================
📌 测试2: 前缀参数冲突
============================================================
🔍 开始解析函数: prefix_func
文档字符串: 前缀参数
============================================================
📋 函数签名分析:
完整签名: (_private: str, field_test: int) -> str
返回类型: <class 'str'>
参数数量:2
🔧 参数处理详情:
跳过的参数名: []
[1] 参数名: _private
原始注解: <class 'str'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
❌ 错误: 参数名不能以 '_' 开头
❌ 处理过程中出错: prefix_func 的参数 _private 不能以 '_' 开头
详细错误信息:
Traceback (most recent call last):
File "/tmp/ipython-input-16-54014459.py", line 78, in debug_func_metadata
raise InvalidSignature(f"{func.__name__} 的参数 {param.name} 不能以 '_' 开头")
mcp.server.fastmcp.exceptions.InvalidSignature: prefix_func 的参数 _private 不能以 '_' 开头
📌 测试3: 结构化输出对比
📌 无结构化
============================================================
🔍 开始解析函数: add
文档字符串: None
============================================================
📋 函数签名分析:
完整签名: (a: int, b: int) -> str
返回类型: <class 'str'>
参数数量:2
🔧 参数处理详情:
跳过的参数名: []
[1] 参数名: a
原始注解: <class 'int'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
🔄 类型化注解: <class 'int'>
✅ 字段信息: annotation=<class 'int'>, default=PydanticUndefined
[2] 参数名: b
原始注解: <class 'int'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
🔄 类型化注解: <class 'int'>
✅ 字段信息: annotation=<class 'int'>, default=PydanticUndefined
📊 参数处理总结:
总参数数:2
处理参数数:2
模型字段: ['a', 'b']
🏗️ 创建 Pydantic 模型:
模型名称: addArguments
基类: <class 'mcp.server.fastmcp.utilities.func_metadata.ArgModelBase'>
✅ 模型创建成功: <class '__main__.addArguments'>
📄 addArguments JSON Schema:
{
"properties":{
"a":{
"title":"A",
"type":"integer"
},
"b":{
"title":"B",
"type":"integer"
}
},
"required":[
"a",
"b"
],
"title":"addArguments",
"type":"object"
}
🎯 返回值处理:
structured_output 参数: False
返回注解: <class 'str'>
🔚 明确不需要结构化输出
✅ 返回元数据: arg_model=<class '__main__.addArguments'> output_schema=None output_model=None wrap_output=False
📌 结构化
============================================================
🔍 开始解析函数: add
文档字符串: None
============================================================
📋 函数签名分析:
完整签名: (a: int, b: int) -> str
返回类型: <class 'str'>
参数数量:2
🔧 参数处理详情:
跳过的参数名: []
[1] 参数名: a
原始注解: <class 'int'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
🔄 类型化注解: <class 'int'>
✅ 字段信息: annotation=<class 'int'>, default=PydanticUndefined
[2] 参数名: b
原始注解: <class 'int'>
参数种类: POSITIONAL_OR_KEYWORD
默认值: <class 'inspect._empty'>
🔄 类型化注解: <class 'int'>
✅ 字段信息: annotation=<class 'int'>, default=PydanticUndefined
📊 参数处理总结:
总参数数:2
处理参数数:2
模型字段: ['a', 'b']
🏗️ 创建 Pydantic 模型:
模型名称: addArguments
基类: <class 'mcp.server.fastmcp.utilities.func_metadata.ArgModelBase'>
✅ 模型创建成功: <class '__main__.addArguments'>
📄 addArguments JSON Schema:
{
"properties":{
"a":{
"title":"A",
"type":"integer"
},
"b":{
"title":"B",
"type":"integer"
}
},
"required":[
"a",
"b"
],
"title":"addArguments",
"type":"object"
}
🎯 返回值处理:
structured_output 参数: True
返回注解: <class 'str'>
经过_get_typed_annotation处理后的类型: <class 'str'>
✅ 输出模型创建成功: <class 'mcp.server.fastmcp.utilities.func_metadata.addOutput'>
📄 返回值 JSON Schema:
{
"properties":{
"result":{
"title":"Result",
"type":"string"
}
},
"required":[
"result"
],
"title":"addOutput",
"type":"object"
}
wrap_output: True
✨ func_metadata 处理完成!
最终结果: arg_model=<class '__main__.addArguments'> output_schema={'properties':{'result':{'title': 'Result', 'type': 'string'}}, 'required':['result'], 'title': 'addOutput', 'type': 'object'} output_model=<class 'mcp.server.fastmcp.utilities.func_metadata.addOutput'> wrap_output=True
============================================================