用FastMCP 3.x在30分钟内构建Python MCP服务器 — 一个@tool装饰器就够了

用FastMCP 3.x在30分钟内构建Python MCP服务器 — 一个@tool装饰器就够了

我实际安装了FastMCP 3.2.4,用@mcp.tool()、@mcp.resource()、@mcp.prompt()装饰器构建了可运行的MCP服务器。这是一份用30行Python实现Claude Desktop和Cursor可调用的AI工具服务器的实战指南。

从零开始实现MCP(Model Context Protocol)服务器比想象中要麻烦。stdio传输处理、JSON-RPC 2.0序列化、处理器注册 — 如果你走过用Streamable HTTP直接实现MCP服务器的过程,就知道那种感觉:“我只是想添加一个AI工具,为什么需要这么多样板代码?”

FastMCP正是为了解决这个问题而生的框架。今天,我在沙盒中用pip安装,并在30分钟内启动了一个实际可用的MCP服务器。

FastMCP是什么

FastMCP是构建在MCP Python SDK之上的高层框架,类似于Express.js封装Node的http模块。官方描述:「The fast, Pythonic way to build MCP servers and clients」。实际用下来,这句话是准确的。

先确认版本:

$ fastmcp version

FastMCP version:   3.2.4
MCP version:       1.27.0
Python version:    3.12.8
Platform:          macOS-15.6-arm64

我之前在待办列表里记的是「v2.0」,但已经到了3.x。MCP协议本身也到了1.27.0。这个版本差距说明:API发生了变化,文档未必跟上了。我不得不通过实际运行代码来验证,而不是相信旧文章。

安装和第一个服务器 — 真的就这些

pip install fastmcp

安装大约十秒。下面是我在沙盒中构建的第一个服务器 — 两个天气相关工具:

from fastmcp import FastMCP
from datetime import datetime

mcp = FastMCP("weather-tools", version="1.0.0")

@mcp.tool()
def get_current_time(timezone: str = "UTC") -> str:
    """返回当前时间。"""
    return f"当前时间 ({timezone}): {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"

@mcp.tool()
def calculate_temp(celsius: float) -> dict:
    """将摄氏度转换为华氏度和开尔文。"""
    return {
        "celsius": celsius,
        "fahrenheit": round(celsius * 9/5 + 32, 2),
        "kelvin": round(celsius + 273.15, 2)
    }

@mcp.resource("data://server-info")
def server_info() -> str:
    """返回服务器信息。"""
    return "FastMCP 3.x 天气服务器"

@mcp.prompt()
def weather_analysis(location: str) -> str:
    """天气分析提示模板。"""
    return f"分析{location}的天气并推荐合适的穿着。"

if __name__ == "__main__":
    mcp.run()  # 以stdio模式运行

就这些。给Python函数添加一个装饰器,它就成了MCP工具。类型提示自动转换为JSON Schema传递给Claude。

用CLI检查服务器配置:

$ fastmcp inspect server.py

Server
  Name:         weather-tools
  Version:      1.0.0
  Generation:   2

Components
  Tools:        2
  Prompts:      1
  Resources:    1
  Templates:    0

三个核心构建块:Tool、Resource、Prompt

FastMCP有三个核心概念。区分清楚这三者是设计好服务器的第一步。

@mcp.tool() — Claude直接调用的函数。接收参数、执行操作、返回结果。搜索、计算、文件操作、API调用等都放这里。当我想让Claude直接操作我的文件系统或API时,用@mcp.tool()

@mcp.resource() — 只读数据源。用data://file://https://等URI注册,Claude将其作为上下文读取。与工具不同,这是「读取」而非「执行」的概念。数据库模式、配置文件、文档等放这里,会进入Claude的上下文窗口。

@mcp.prompt() — 可复用的提示模板。接收参数,返回结构化的提示消息。在Claude Desktop或claude.ai中可以像斜杠命令一样使用。

Tool和Resource的区别让初学者困惑。我的标准很简单:有副作用就是Tool,只读就是Resource

用Context向客户端发送进度信息

当工具执行耗时操作时,可以实时向客户端推送进度。添加Context参数,FastMCP自动注入。

from fastmcp import FastMCP, Context

mcp = FastMCP("dev-tools")

@mcp.tool()
async def list_files(directory: str, ctx: Context) -> list[str]:
    """返回指定目录的文件列表。"""
    import os
    await ctx.info(f"正在读取目录: {directory}")  # 向客户端发送日志
    
    try:
        files = os.listdir(directory)
        await ctx.report_progress(100, 100, "complete")  # 报告进度
        return sorted(files)
    except FileNotFoundError:
        raise ValueError(f"目录不存在: {directory}")

我在沙盒中实际运行,确认了ctx.info()确实向客户端实时发送日志:

INFO  Received INFO from server: {'msg': '正在读取目录: /tmp', 'extra': None}

这在Claude Desktop中运行时,用户可以实时看到工具执行的步骤。从UX角度来看,这是相当重要的功能。

用FastMCP Client测试

不需要实际的Claude Desktop就能测试服务器。FastMCP提供了进程内客户端。在实现智能体工作流模式时,这种方式也能让测试保持自包含。

import asyncio
from fastmcp import FastMCP
from fastmcp.client import Client

mcp = FastMCP("dev-tools")

@mcp.tool()
def search_text(text: str, pattern: str) -> dict:
    """在文本中搜索模式。"""
    import re
    matches = re.findall(pattern, text)
    return {"pattern": pattern, "matches": matches, "count": len(matches)}

@mcp.tool()
def word_count(text: str) -> dict:
    """返回单词数、字符数和行数。"""
    words = text.split()
    return {
        "words": len(words),
        "characters": len(text),
        "lines": len(text.splitlines())
    }

async def test():
    async with Client(mcp) as client:
        tools = await client.list_tools()
        print(f"已注册工具 ({len(tools)}个):")
        for t in tools:
            print(f"  [{t.name}] {t.description}")
        
        result = await client.call_tool("search_text", {
            "text": "FastMCP is fast. FastMCP is easy.",
            "pattern": "FastMCP"
        })
        print(f"\nsearch_text结果: {result.data}")
        # → {'pattern': 'FastMCP', 'matches': ['FastMCP', 'FastMCP'], 'count': 2}
        
        result2 = await client.call_tool("word_count", {
            "text": "Hello World from FastMCP 3.x"
        })
        print(f"word_count结果: {result2.data}")
        # → {'words': 5, 'characters': 27, 'lines': 1}

asyncio.run(test())

通过result.data直接访问结构化返回值。实际运行,零错误。

FastMCP CLI运行结果 — fastmcp version、inspect、工具调用测试

以HTTP服务器方式远程部署

除了本地stdio模式,还可以作为HTTP服务器运行。在Cursor实例之间共享MCP服务器或远程部署时很有用。

# HTTP模式(默认端口8000)
if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8000)
# 或直接用uvicorn运行
uvicorn server:mcp.http_app() --host 0.0.0.0 --port 8000

FastMCP的HTTP应用基于Starlette(内部是StarletteWithLifespan),可以挂载到FastAPI应用中:

from fastapi import FastAPI
from fastmcp import FastMCP

app = FastAPI()
mcp = FastMCP("my-tools")

@mcp.tool()
def my_tool() -> str:
    return "result"

# 将MCP服务器挂载到FastAPI应用
app.mount("/mcp", mcp.http_app())

Claude Desktop连接HTTP服务器的配置很简单:

{
  "mcpServers": {
    "my-tools": {
      "url": "http://localhost:8000/mcp/"
    }
  }
}

fastmcp CLI — 开发工作流中的实用命令

FastMCP附带CLI,我一开始不知道。运行fastmcp --help后发现功能相当丰富:

Commands:
  inspect      — 输出服务器组件摘要
  list         — 列出已注册的工具
  call         — 直接调用工具(调试用)
  install      — 自动注册到Claude Desktop / Cursor
  dev          — 启动带热重载的开发服务器
  discover     — 发现编辑器中配置的MCP服务器
  run          — 运行服务器

fastmcp install server.py --client claude据说会自动修改Claude Desktop配置,不用再手动编辑JSON。不过我没能在沙盒环境中直接测试(没有安装Claude Desktop),--client选项修改的具体路径建议查阅官方文档。

fastmcp dev看起来更实用——开发时代码修改不需要手动重启服务器。

类型提示就是API Schema

FastMCP中让我印象最深的功能:类型提示自动转换为JSON Schema。使用原始SDK,你需要为每个工具手写inputSchema字典。FastMCP将这个任务交给Python的类型系统来完成。

from typing import Literal
from pydantic import BaseModel

class FileFilter(BaseModel):
    extension: str
    min_size_kb: int = 0
    exclude_hidden: bool = True

@mcp.tool()
def list_files_advanced(
    directory: str,
    filter: FileFilter | None = None,
    sort_by: Literal["name", "size", "modified"] = "name",
    limit: int = 50
) -> list[dict]:
    """返回带过滤器和排序选项的文件列表。"""
    import os
    files = []
    for f in os.scandir(directory):
        if filter and filter.exclude_hidden and f.name.startswith("."):
            continue
        if filter and not f.name.endswith(f".{filter.extension}"):
            continue
        info = f.stat()
        size_kb = info.st_size / 1024
        if filter and size_kb < filter.min_size_kb:
            continue
        files.append({"name": f.name, "size_kb": round(size_kb, 2), "modified": info.st_mtime})
    key_map = {"name": "name", "size": "size_kb", "modified": "modified"}
    files.sort(key=lambda x: x[key_map[sort_by]])
    return files[:limit]

@mcp.tool()注册这个函数后,Claude自动知道FileFilter的结构、sort_by的有效值(name/size/modified)以及limit的默认值。支持Pydantic模型,所以复杂的嵌套输入也能直接使用。

文档字符串自动成为Claude看到的工具描述。一个写得好的docstring就是你发给模型的使用说明书。

实战示例:代码分析MCP服务器

这是一个我会实际使用的例子 — 分析Python文件的开发工具MCP服务器:

from fastmcp import FastMCP, Context
import ast
import os

mcp = FastMCP("code-analyzer", version="1.0.0")

@mcp.tool()
async def analyze_python_file(filepath: str, ctx: Context) -> dict:
    """用AST分析Python文件,返回函数和类的列表。"""
    await ctx.info(f"正在分析: {filepath}")
    if not os.path.exists(filepath):
        raise ValueError(f"文件不存在: {filepath}")
    with open(filepath, "r", encoding="utf-8") as f:
        source = f.read()
    tree = ast.parse(source)
    functions, classes = [], []
    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            functions.append({
                "name": node.name, "line": node.lineno,
                "args": [a.arg for a in node.args.args],
                "docstring": ast.get_docstring(node)
            })
        elif isinstance(node, ast.ClassDef):
            classes.append({"name": node.name, "line": node.lineno})
    await ctx.report_progress(100, 100, "complete")
    return {"total_lines": source.count("\n") + 1, "functions": functions, "classes": classes}

@mcp.tool()
def count_todo_comments(filepath: str) -> dict:
    """查找文件中的TODO/FIXME/HACK注释。"""
    markers = ["TODO", "FIXME", "HACK", "XXX"]
    results = {m: [] for m in markers}
    with open(filepath, "r", encoding="utf-8") as f:
        for i, line in enumerate(f, 1):
            for marker in markers:
                if f"# {marker}" in line:
                    results[marker].append({"line": i, "text": line.strip()})
    return {k: v for k, v in results.items() if v}

@mcp.resource("data://project-structure")
def project_structure() -> str:
    """返回当前目录的Python文件列表。"""
    py_files = []
    for root, dirs, files in os.walk("."):
        dirs[:] = [d for d in dirs if not d.startswith(".")]
        for f in files:
            if f.endswith(".py"):
                py_files.append(os.path.join(root, f))
    return "\n".join(py_files[:50])

if __name__ == "__main__":
    mcp.run()

将这个服务器连接到Claude Desktop后,可以用自然语言问:「显示这个文件的所有类」或「有多少个TODO注释?」用户不需要写一行Python。这就是MCP工具服务器的意义所在。

与直接使用MCP SDK相比有什么不同

直接用Streamable HTTP实现MCP服务器相比,差异很明显。

直接实现:

  • 创建Server实例
  • 分别注册@server.list_tools() / @server.call_tool()
  • 手动解析输入参数
  • anyio.run() + stdio_server()组合运行

FastMCP:

  • 一个FastMCP实例
  • @mcp.tool()直接将函数注册为工具
  • 从类型提示自动生成JSON Schema
  • mcp.run()一行搞定

代码行数减少是次要的。核心是可以专注于业务逻辑,不需要关心传输层如何工作。

不过我也感受到了限制。FastMCP以控制自由度为代价换取便利性。如果需要自定义低级MCP消息、使用非标准传输,就得从FastMCP的抽象层下面重新挖出被隐藏的东西。这种情况还是直接用MCP Python SDK,就像MCP代码执行实战案例那种需要精细控制的场景。

可行性判断 — 什么时候选FastMCP

我的结论是:

使用FastMCP的场景:构建与标准MCP客户端(Claude、Cursor、VS Code)集成的服务器时。特别是快速原型化团队内AI工具,或者将现有Python函数作为MCP工具暴露时。

直接使用SDK的场景:需要自定义传输、非标准消息格式,或者FastMCP不支持的MCP功能时。像需要精细控制的MCP代码执行场景一样。

FastMCP有一点令人遗憾 — 3.x版本代码变化快,文档没有完全跟上。文档里像get_tools()这样的方法看似存在,但实际已经改成list_tools()了。建议养成直接看源代码或dir(mcp)的习惯,而不是依赖旧文章。

上线前还有一点 — 建议同时看看用MCP Gateway控制智能体流量的方法。暴露服务器后,迟早需要控制哪些工具被如何调用的层。

总结

FastMCP 3.x是Python开发者构建MCP服务器的最快方式。pip install fastmcp一行,@mcp.tool()一个装饰器,mcp.run()一行。30分钟内就能构建Claude Desktop可调用的AI工具服务器。

MCP生态系统正在快速成熟。我的MCP服务器工具包涵盖了已有的工具。构建之前先看看是否已有现成的,但如果需要自定义,FastMCP让这件事真的很快。

今天在沙盒中确认的版本:FastMCP 3.2.4、MCP 1.27.0。这个领域变化很快 — 正式使用前请查阅FastMCP官方文档确认最新API。

阅读其他语言版本

这篇文章有帮助吗?

您的支持能帮助我创作更好的内容。请我喝杯咖啡吧。

关于作者

jw

Kim Jangwook

AI/LLM专业全栈开发者

凭借10年以上的Web开发经验,构建AI代理系统、LLM应用程序和自动化解决方案。分享Claude Code、MCP和RAG系统的实践经验。

返回博客列表