1. 为什么要把散落的 prompt 收进 FastMCP
如果你正在用 FastMCP 写 MCP 服务器,大概率遇到过这种局面:提示词一开始只是几个字符串常量,散落在server.py、tools.py、甚至某个utils.py里。等到提示词涨到十几条,改一个措辞要全局搜索,测试时还得手动拼参数,团队里两个人同时改同一个 prompt 直接冲突。更麻烦的是,MCP 客户端通过list_prompts和get_prompt协议方法访问提示词时,你希望它们能被动态发现、带参数、还能根据上下文生成不同内容,而不是一堆硬编码字符串。
FastMCP 的提示词管理模块就是解决这个问题的。它把提示词从「字符串」升级成「可注册、可检索、可渲染的资产」:你用@prompt装饰器把普通 Python 函数注册成提示词,PromptManager作为中央注册表统一管理,Prompt和Message类负责元数据与标准化输出。客户端请求时,系统按名字检索、注入参数、调用函数、把返回值转成消息数组,再通过 MCP 协议返回。
这篇文章面向需要把 prompt 收敛为可维护资产的 MCP 开发者。我会给出@prompt装饰器注册、PromptManager配置、settings.json/config.toml骨架的可复制片段,并演示通过 TaoToken 统一 Key/API 通道完成一次提示词加载与调用验证。读完你能拿到一套可复用的提示词管理配置模板,直接套到自己的 FastMCP 项目里。
2. 前置准备:FastMCP 环境与 TaoToken 统一通道
在动手写提示词管理之前,先把两件事准备好:FastMCP 运行环境,以及一个能统一管理模型调用的 API 通道。前者决定你的服务器能不能跑起来,后者决定你在验证提示词时不用为每个模型单独配 Key。
2.1 安装 FastMCP 与目录约定
FastMCP 的提示词系统位于src/mcp/server/fastmcp/prompts/目录下,核心是三个模块:manager.py负责注册与管理,base.py定义Prompt和Message的基础结构与渲染逻辑,server.py实现协议级接口。你不需要改源码,但理解这个分层有助于排障。
安装依赖:
pip install fastmcp我建议的项目结构如下,把提示词单独放一个包,避免和工具逻辑混在一起:
my-mcp-server/ ├── server.py ├── prompts/ │ ├── __init__.py │ ├── registry.py # @prompt 注册入口 │ └── templates.py # 具体提示词函数 ├── settings.json └── config.toml2.2 用 TaoToken 统一 Key 与 API 通道
验证提示词时,你往往要调用模型看输出是否符合预期。如果每个模型都单独配 Key、单独记 Base URL,验证成本会很高。TaoToken 提供统一的 Key 和 API 通道,把模型调用收敛到一个入口,适合在提示词调试阶段快速切换模型对比效果。
官网地址是https://taotoken.net/?utm_source=taotoken_aicg_blog_end,API 入口是https://taotoken.net/api。你需要在控制台创建一个 API Key,后续在环境变量里引用它,而不是硬编码进代码。
注意:API Key 属于敏感凭证,务必通过环境变量或密钥管理服务注入,不要提交到 Git 仓库。
创建 Key 的入口在控制台的 API Keys 页面,拿到形如sk-xxxx的字符串后,写入环境变量:
export TAOTOKEN_API_KEY="sk-你的实际Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"这样你的 FastMCP 服务器和验证脚本都从同一组环境变量读取,切换环境时只改一处。
3. 可复制配置:@prompt 装饰器与 PromptManager 骨架
这一节是全文的核心。我会先给出settings.json和config.toml的骨架,再写@prompt装饰器的注册代码,最后说明PromptManager如何接管注册与检索。
3.1 settings.json 与 config.toml 骨架
settings.json用来放服务器级配置,比如服务器名、传输方式、以及模型通道的引用。注意这里只放非敏感信息,Key 走环境变量。
{ "server": { "name": "prompt-manager-demo", "transport": "stdio", "log_level": "INFO" }, "model_channel": { "provider": "taotoken", "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "default_model": "claude-3-5-sonnet" }, "prompts": { "registry_module": "prompts.registry", "strict_args": true } }config.toml用来放提示词层面的默认值,比如默认分析深度、输出语言、最大上下文条数。这样提示词函数可以读取配置,而不是把魔法数字写死在函数体里。
[prompt_defaults] language = "zh-CN" analysis_depth = "basic" max_context_messages = 8 [prompt_defaults.overrides] "analyze_database_schema" = { analysis_depth = "deep" }读取配置的辅助函数可以这样写,放在prompts/__init__.py里:
import json import os import tomllib from pathlib import Path def load_settings(path: str = "settings.json") -> dict: with open(path, "r", encoding="utf-8") as f: return json.load(f) def load_prompt_config(path: str = "config.toml") -> dict: with open(path, "rb") as f: return tomllib.load(f) def get_api_key(settings: dict) -> str: env_name = settings["model_channel"]["api_key_env"] key = os.environ.get(env_name) if not key: raise RuntimeError(f"环境变量 {env_name} 未设置") return key3.2 @prompt 装饰器注册提示词
@prompt装饰器的作用是把普通函数转成可被 MCP 协议调用的提示词。它支持name、title、description等参数来自定义元数据。关键点:装饰器要写成@prompt()而不是@prompt,否则函数不会被实际注册。
下面是一个同步提示词和一个异步提示词的注册示例。异步版本演示了上下文注入和动态参数组合。
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.prompts import UserMessage, AssistantMessage from mcp.server.fastmcp.server import Context server = FastMCP("prompt-manager-demo") @server.prompt( name="code_review", title="代码审查", description="对给定代码片段做结构化审查,输出问题与建议" ) def code_review(code: str, language: str = "python") -> list: """同步提示词:返回消息数组。""" return [ UserMessage(content=f"请审查以下 {language} 代码:\n{code}"), UserMessage(content="请按「严重问题 / 改进建议 / 亮点」三段输出。"), ] @server.prompt( name="analyze_database_schema", title="数据库表结构分析", description="读取表结构资源并生成分析提示词" ) async def analyze_database_schema(table_name: str, context: Context) -> list: """异步提示词:支持上下文注入与外部资源读取。""" schema_resource = await context.read_resource(f"resource://schema/{table_name}") schema_text = "".join([chunk.content for chunk in schema_resource]) prefs = context.fastmcp.get_context().get("user_preferences", {}) depth = prefs.get("analysis_depth", "basic") return [ UserMessage(content=f"请分析以下 {table_name} 表的结构:\n{schema_text}"), AssistantMessage(content=f"分析深度要求:{depth}"), UserMessage(content="请指出潜在的设计问题和优化建议。"), ]这里有几个容易踩的点。第一,返回值必须是可被转换为Message对象的类型,返回裸字符串在某些版本会报转换错误。第二,Context参数的类型注解必须正确,否则注入会失败。第三,异步函数用async def,系统会自动await结果。
3.3 PromptManager 接管注册与检索
PromptManager是中央注册表,内部用字典存储提示词,查找是 O(1)。它提供add_prompt、get_prompt、list_prompts三个核心方法。FastMCP服务器类内部持有_prompt_manager,@prompt装饰器最终就是调用Prompt.from_function()创建Prompt实例,再add_prompt进去。
如果你想手动管理一批提示词,可以显式操作PromptManager:
from mcp.server.fastmcp.prompts.manager import PromptManager from mcp.server.fastmcp.prompts.base import Prompt manager = PromptManager() def build_prompt(name: str, title: str, description: str): def decorator(fn): prompt = Prompt.from_function( fn, name=name, title=title, description=description ) manager.add_prompt(prompt) return fn return decorator @build_prompt("summarize", "摘要生成", "把长文本压缩成要点") def summarize(text: str) -> list: return [UserMessage(content=f"请把以下内容总结为不超过5条要点:\n{text}")] # 检索 p = manager.get_prompt("summarize") print(p.name, p.description) print([item.name for item in manager.list_prompts()])注册流程里有一个细节:如果同名提示词已存在,PromptManager会发出警告而不是静默覆盖。这在团队协作时很有用,能避免两个人注册同名 prompt 导致行为漂移。检索时如果名字不存在,get_prompt会抛ValueError,所以客户端调用前最好先list_prompts确认。
4. 验证请求:通过 TaoToken 完成一次提示词加载与调用
配置写完了,得验证它真的能跑。这一节我用一个独立脚本,通过 TaoToken 的统一通道加载提示词、渲染消息、调用模型,确认整条链路通。
4.1 启动服务器并列出提示词
先启动 FastMCP 服务器,用 stdio 传输:
python server.py在另一个终端用 MCP 客户端连接,调用list_prompts:
import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def list_all_prompts(): params = StdioServerParameters(command="python", args=["server.py"]) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() result = await session.list_prompts() for p in result.prompts: print(f"{p.name} | {p.title} | {p.description}") asyncio.run(list_all_prompts())预期输出类似:
code_review | 代码审查 | 对给定代码片段做结构化审查,输出问题与建议 analyze_database_schema | 数据库表结构分析 | 读取表结构资源并生成分析提示词如果这里报「提示词未注册」,先检查装饰器是不是写成了@prompt而不是@prompt(),再确认函数所在模块确实被 import 执行过。
4.2 调用 get_prompt 并渲染消息
确认列表没问题后,调用get_prompt拿具体消息:
async def fetch_prompt(): params = StdioServerParameters(command="python", args=["server.py"]) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() result = await session.get_prompt( "code_review", arguments={"code": "def add(a,b): return a+b", "language": "python"} ) for msg in result.messages: print(msg.role, "->", msg.content.text[:80]) asyncio.run(fetch_prompt())预期输出:
user -> 请审查以下 python 代码: def add(a,b): return a+b user -> 请按「严重问题 / 改进建议 / 亮点」三段输出。到这一步,提示词的注册、检索、渲染链路已经验证完毕。接下来把渲染出的消息发给模型,确认输出符合预期。
4.3 通过 TaoToken 调用模型验证输出
用 TaoToken 的统一通道调用模型,Key 从环境变量读取:
import os import httpx def call_model(messages: list, model: str = "claude-3-5-sonnet") -> str: api_key = os.environ["TAOTOKEN_API_KEY"] base_url = os.environ.get("TAOTOKEN_BASE_URL", "https://taotoken.net/api") resp = httpx.post( f"{base_url}/v1/messages", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, json={ "model": model, "max_tokens": 1024, "messages": messages, }, timeout=60, ) resp.raise_for_status() return resp.json()["content"][0]["text"] if __name__ == "__main__": msgs = [ {"role": "user", "content": "请审查以下 python 代码:\ndef add(a,b): return a+b"}, {"role": "user", "content": "请按「严重问题 / 改进建议 / 亮点」三段输出。"}, ] print(call_model(msgs))实测下来,把提示词渲染结果直接喂给模型,输出结构稳定,说明提示词模板本身没有歧义。如果你要对比不同模型对同一提示词的响应,只改model参数即可,Key 和 Base URL 都不用动,这正是统一通道的价值。
5. 本篇常见错排查
提示词管理模块的报错大多集中在注册、参数、上下文和消息转换四类。下面按现象、原因、修复三步走。
5.1 提示词未注册
现象:list_prompts返回空,或get_prompt抛ValueError。
原因通常是装饰器写法错误。@prompt不带括号时,装饰器工厂没有被调用,函数不会进入注册流程。另一个原因是函数所在模块没有被 import,装饰器根本没执行。
修复:统一写成@server.prompt()或@server.prompt(name="..."),并确认模块在服务器启动时被加载。可以在注册后打印manager.list_prompts()做自检。
5.2 参数验证失败
现象:调用get_prompt时报参数不匹配。
原因是你传入的arguments与函数签名不一致,比如缺少必需参数,或参数名拼写错误。Prompt.from_function会提取函数签名生成PromptArgument列表,客户端必须按这个列表传参。
修复:先list_prompts查看每个提示词的参数元数据,再按名字传参。开启settings.json里的strict_args可以让不匹配直接报错而不是静默忽略。
5.3 上下文注入失败
现象:异步提示词里context为None,或报类型错误。
原因是Context参数的类型注解缺失或写错,系统无法识别该注入。另外,如果函数里有一个同名参数也叫context,会和注入冲突。
修复:确保context: Context的注解完整,且不要与其他参数重名。注入的Context可以访问read_resource和fastmcp.get_context(),用于读取外部资源和会话状态。
5.4 消息转换错误
现象:render阶段报无法转换为Message。
原因是提示词函数返回了不支持的类型,比如裸字符串、字典或None。系统期望返回Message对象列表,或可被转换的结构。
修复:统一返回UserMessage/AssistantMessage组成的列表。如果确实要返回字符串,确认当前 FastMCP 版本支持自动包装,否则手动包一层。
5.5 模型调用 401 或超时
现象:通过 TaoToken 调用时返回 401 或连接超时。
401 通常是TAOTOKEN_API_KEY未设置或值不对,检查环境变量是否在当前 shell 生效。超时则可能是base_url写错,确认是https://taotoken.net/api而不是其他路径。另外注意httpx的timeout设置,长提示词渲染后消息较多时适当调大。
6. 把提示词骨架沉淀为团队资产
走到这里,你已经有了settings.json和config.toml骨架、@prompt注册代码、PromptManager检索逻辑,以及一条通过 TaoToken 验证的完整链路。接下来要做的不是继续堆提示词,而是把骨架沉淀成团队能复用的资产。
我的做法是:每个提示词函数只负责「组装消息」,不负责「调用模型」。模型调用统一走 TaoToken 通道,Key 从环境变量注入,模型名从settings.json读取。这样提示词可以独立测试,模型可以随时切换,两者解耦。提示词的默认参数放config.toml,按名字做 overrides,避免把业务默认值写死在函数体里。
如果你要长期维护一批编码类或 Agent 类提示词,可以考虑用 Coding Plan 把模型调用额度统一管理,配合 API Keys 页面轮换 Key,接入文档里有完整的参数说明。验证单个提示词效果时,模型对话入口适合快速试;排障和接入细节则回到 API Keys 和接入文档。把这几件事分开,提示词管理才不会变成新的技术债。