MCP 实践篇:用 FastMCP 快速构建工具服务器与 Agent

目录

  • 前言
  • [一、FastMCP 概述](#一、FastMCP 概述)
    • 简介
    • [stdio和HTTP/SSE 模式](#stdio和HTTP/SSE 模式)
      • [stdio 模式:](#stdio 模式:)
      • [HTTP/SSE 模式](#HTTP/SSE 模式)
  • [二、FastMCP 核心架构](#二、FastMCP 核心架构)
    • [2.1 服务器端(Server)](#2.1 服务器端(Server))
      • [2.1.1 核心入口:FastMCP 类](#2.1.1 核心入口:FastMCP 类)
      • [2.1.2 三大能力组件](#2.1.2 三大能力组件)
        • [Tools(工具)------ 可执行动作](#Tools(工具)—— 可执行动作)
        • [Resources(资源)------ 只读数据](#Resources(资源)—— 只读数据)
        • [Prompts(提示模板)------ 可复用提示词](#Prompts(提示模板)—— 可复用提示词)
      • [2.1.3 传输模式无缝切换](#2.1.3 传输模式无缝切换)
    • [2.2 客户端(Client)](#2.2 客户端(Client))
      • [2.2.1 创建与连接](#2.2.1 创建与连接)
      • [2.2.2 生命周期管理](#2.2.2 生命周期管理)
      • [2.2.3 核心操作](#2.2.3 核心操作)
        • [1. 列出服务器能力](#1. 列出服务器能力)
        • [2. 调用工具(Tool)](#2. 调用工具(Tool))
        • [3. 读取资源(Resource)](#3. 读取资源(Resource))
        • [4. 获取提示模板(Prompt)](#4. 获取提示模板(Prompt))
  • [三、实战:基于 FastMCP + OpenAI SDK 实现极简 Agent](#三、实战:基于 FastMCP + OpenAI SDK 实现极简 Agent)
    • [3.1 核心执行流程](#3.1 核心执行流程)
    • [3.2 核心代码](#3.2 核心代码)

前言

承接上一篇《MCP 的前世今生:从 "为每个工具写 Schema" 到统一协议》,本文从理论落地实践,以目前 Python 生态最主流的 FastMCP 框架为核心,讲解其架构组成与开发流程。


一、FastMCP 概述

简介

FastMCP 是一套Python 的 MCP 开发框架,它对底层 MCP(Model Context Protocol)协议做了高度封装,让开发者无需关心 JSON-RPC 消息格式、握手协商、传输层细节,只需专注业务逻辑即可快速构建标准的 MCP 服务。

它既可以用来编写 MCP 服务端(对外提供工具、资源、提示),也自带完整的客户端 SDK,用于编程方式连接任意 MCP 服务器。

为什么选择 FastMCP:

在众多 MCP 实现中,FastMCP 是目前生产环境最常用的方案。

  1. 极致简单,开发效率高
    采用与 FastAPI 一致的设计哲学:装饰器 + 类型注解 + 文档字符串自动生成JSON-RPC协议内容。
  2. 功能完备,开箱即用
    它是一个生产级框架,内置了 stdio、HTTP/SSE 等多种传输协议,支持服务聚合、代理,并提供便捷的客户端用于测试。你无需"重复造轮子"。
  3. 官方认可,生态主流
    全球 70% 的 MCP 服务器基于它构建,日均百万次下载量。选择它意味着拥有强大的社区支持和最佳实践。

stdio和HTTP/SSE 模式

传输方式 通信原理 典型适用场景 特性
stdio 以子进程启动服务器,通过标准输入 / 输出通信 本地桌面客户端(Claude Desktop、Cursor)、私有本地工具 低延迟、有会话状态、无需网络
http 通过 HTTP/SSE 协议与远程服务器网络通信 分布式部署、共享工具服务、服务端 Agent 调用 可远程访问、易扩缩容、独立部署

stdio 模式:

原理:

进程的 stdio 概念:每个进程启动时,操作系统会自动分配三个标准数据流:

stdin(标准输入,用于接收外部写入的数据)

stdout(标准输出,用于输出普通结果)

stderr(标准错误,用于输出错误日志)。

父子进程通信时,父进程可以往子进程的 stdin 写数据,子进程的输出会从 stdout 和 stderr 流出。

流程:

客户端创建子进程运行 MCP 服务器 → 将 JSON-RPC 消息写入子进程的 stdin → 从子进程的 stdout 读取响应 → 子进程退出时关闭连接。适合本地命令行工具、脚本集成场景,简单轻量但无法跨机器访问。

HTTP/SSE 模式

原理:

MCP 服务器作为独立 Web 服务运行,同时提供两个核心端点------/mcp(HTTP POST 端点)处理客户端发起的请求式通信,/sse(Server-Sent Events 端点)建立长连接实现服务端主动推送。

流程:

  1. 客户端 GET /sse 建立长连接,服务器返回一个消息端点地址(如 /message)。
  2. 客户端向该端点 POST JSON-RPC 请求。
  3. 服务器处理请求后,通过之前建立的 SSE 长连接把响应推回客户端。

二、FastMCP 核心架构

FastMCP 的整体架构分为服务器端和客户端两大核心部分,二者通过标准 MCP 协议通信,完全解耦。

2.1 服务器端(Server)

服务器端是工具能力的提供者,负责对外暴露能力、处理调用请求。其核心由一个入口类 + 三类能力组件构成。

MCP 服务端的核心就是一个事件循环,所有请求都由这个事件循环接收和调度。

2.1.1 核心入口:FastMCP 类

FastMCP 是服务器的核心实例,负责管理所有注册的能力、处理连接生命周期、序列化与反序列化协议消息。

python 复制代码
from fastmcp import FastMCP

# 初始化服务器,name 为服务标识,instructions 为服务说明
mcp = FastMCP(
    name="WeatherServer",
    instructions="提供城市天气查询与城市信息服务"
)

2.1.2 三大能力组件

服务器通过三个装饰器注册不同类型的能力,这也是日常开发最核心的三个 API:

Tools(工具)------ 可执行动作

对应 MCP 协议的工具调用能力

python 复制代码
@mcp.tool()
def get_weather(city: str, include_forecast: bool = False) -> str:
    """
    查询指定城市的实时天气信息
    Args:
        city: 城市名称,如北京、上海
        include_forecast: 是否返回未来3天预报
    """
    # 业务逻辑:调用第三方天气接口
    return f"{city} 今日晴朗,22~28°C"

框架自动从函数签名、类型注解、docstring 生成符合 MCP 规范的 JSON Schema

支持同步函数与 async def 异步函数,如果是同步函数,事件循环会直接将同步工具扔到内置线程池执行,不阻塞事件循环。

python 复制代码
客户端请求(JSON-RPC)
        ↓
   【事件循环】 ← 核心调度器
        ↓
   判断工具类型
        ↓
   ┌────┴────┐
   ↓         ↓
异步工具    同步工具
(直接执行) (线程池)
   ↓         ↓
   └────┬────┘
        ↓
   返回响应给客户端
Resources(资源)------ 只读数据

用于向大模型提供结构化、无副作用的只读数据(配置、元数据、文件内容等),模型可以读取资源注入上下文。

python 复制代码
# 使用 URI 模板标识资源,支持路径参数
@mcp.resource("city://{city}/info")
def get_city_info(city: str) -> dict:
    """获取城市基础信息"""
    return {"city": city, "province": "直辖市", "population": "2189万"}

同样也支持异步方式

Prompts(提示模板)------ 可复用提示词

预定义的标准化提示词模板,支持接收参数动态生成提示内容,用于封装固定的交互模式(如代码审查、数据清洗、报告生成等)。

python 复制代码
@mcp.prompt()
def data_analyst_prompt(data: str) -> str:
    """数据分析场景提示模板"""
    return f"""请作为数据分析师,对以下数据进行总结:
{data}
要求:提炼核心结论 + 1条趋势判断"""

2.1.3 传输模式无缝切换

服务端的业务代码与传输方式完全解耦,仅需修改启动参数,即可在两种通信模式间切换,业务逻辑零改动:

python 复制代码
if __name__ == "__main__":
    # 模式1:stdio 模式,本地进程间通信,适合桌面客户端对接
    # mcp.run(transport="stdio")
    
    # 模式2:HTTP 模式,网络访问,适合远程部署与多客户端共享
    mcp.run(transport="http", host="0.0.0.0", port=8000)

HTTP 模式启动后,默认服务端点为 http://127.0.0.1:8000/mcp,基于流式 HTTP 协议实现双向通信,同时向下兼容传统 SSE 模式。

2.2 客户端(Client)

客户端是 MCP 能力的使用者,负责连接服务器、发现能力、适配模型。它是连接「MCP 工具」与「大模型 / Agent 框架」的桥梁。

核心特性:

  1. 自动传输协商:根据传入的服务器源自动推断并选择合适的传输机制。
  2. 完整的协议支持:封装了 MCP 协议的全部能力------工具调用、资源读取、提示模板获取。
  3. 类型安全:返回结构化的 Python 对象,支持 datetime、UUID 等复杂类型的自动反序列化。
  4. 可重入上下文管理器:支持多个并发的 async with client 块,通过引用计数和后台会话管理实现高效的会话复用。

2.2.1 创建与连接

Client 的构造方式极其灵活,支持多种服务器源,并自动推断传输方式:

python 复制代码
from fastmcp import Client, FastMCP

# 1. 连接同一进程内的内存服务器(理想用于测试)
server = FastMCP("TestServer")
client = Client(server)          # 自动使用 In-Memory Transport[reference:12]

# 2. 连接远程 HTTP 服务器(生产部署)
client = Client("https://example.com/mcp")  # 自动使用 HTTP Transport[reference:13]

# 3. 连接本地 Python 脚本(stdio 模式)
client = Client("my_mcp_server.py")         # 自动使用 STDIO Transport[reference:14]

⚠️ STDIO 模式的关键注意点:STDIO 服务器运行在隔离环境中,不会继承你 shell 的环境变量。API Key、路径等配置必须显式传递:

python 复制代码
# ❌ 错误:服务器看不到 shell 中 export 的变量
# export API_KEY="secret"
# client = Client("my_server.py")

# ✅ 正确:显式传递环境变量
client = Client("my_server.py", env={"API_KEY": "secret", "DEBUG": "true"})[reference:18]

2.2.2 生命周期管理

所有客户端操作最好在 async with 上下文管理器中执行,以确保连接的正确建立和释放

python 复制代码
import asyncio
from fastmcp import Client

async def main():
    client = Client("https://example.com/mcp")
    
    async with client:  # 进入时建立连接,退出时自动清理
        # 在这里执行所有操作
        await client.ping()  # 验证连接是否正常
        tools = await client.list_tools()
        # ...

asyncio.run(main())

2.2.3 核心操作

1. 列出服务器能力

连接建立后,可以枚举服务器暴露的所有能力。

python 复制代码
async with client:
    # 列出所有可用工具
    tools = await client.list_tools()
    for tool in tools:
        print(f"工具: {tool.name} - {tool.description}")
    
    # 列出所有资源
    resources = await client.list_resources()
    
    # 列出所有提示模板
    prompts = await client.list_prompts()
2. 调用工具(Tool)

call_tool() 是客户端最核心的方法,按名称执行服务器端工具并返回结构化结果

python 复制代码
async with client:
    # 基本调用:参数以字典形式传入
    result = await client.call_tool("add", {"a": 5, "b": 3})
    
    # 访问结构化数据(自动反序列化)
    print(result.data)              # 8[reference:25]
    
    # 访问原始内容块
    print(result.content[0].text)   # "8"[reference:26]

超时控制与进度监控

python 复制代码
async with client:
    # 设置超时(超过2秒自动中止)
    result = await client.call_tool(
        "long_running_task", 
        {"param": "value"},
        timeout=2.0
    )
    
    # 监听进度通知
    result = await client.call_tool(
        "long_running_task", 
        {"param": "value"},
        progress_handler=my_progress_handler
    )

结构化结果(v2.10.0+) :FastMCP 客户端能够将服务器返回的 JSON 数据自动还原为完整的 Python 对象,包括 datetime、UUID 等复杂类型:

因为服务器端工具返回json结果时,在返回的JSON-RPC数据中,会将数据类型一起发送给客户端,进而能够解析并反序列化为python对象。

python 复制代码
from datetime import datetime
from uuid import UUID

async with client:
    result = await client.call_tool("get_weather", {"city": "London"})
    
    # FastMCP 自动重建完整 Python 对象
    weather = result.data
    print(f"温度: {weather.temperature}°C,时间: {weather.timestamp}")
    
    # 复杂类型被正确反序列化
    assert isinstance(weather.timestamp, datetime)
    assert isinstance(weather.station_id, UUID)[reference:29]

错误处理:

python 复制代码
from fastmcp.exceptions import ToolError

async with client:
    # 方式一:捕获异常(默认行为)
    try:
        result = await client.call_tool("potentially_failing_tool", {"param": "value"})
    except ToolError as e:
        print(f"工具执行失败: {e}")
    
    # 方式二:手动检查错误标志
    result = await client.call_tool(
        "potentially_failing_tool", 
        {"param": "value"},
        raise_on_error=False
    )
    if result.is_error:
        print(f"失败: {result.content[0].text}")
    else:
        print(f"成功: {result.data}")
3. 读取资源(Resource)
python 复制代码
async with client:
    # 读取单个资源
    resource = await client.read_resource("city://北京/info")
    print(resource.content)
    
    # 列出所有可用资源
    resources = await client.list_resources()
4. 获取提示模板(Prompt)
python 复制代码
async with client:
    # 获取提示模板(可传入参数)
    prompt = await client.get_prompt("data_analyst_prompt", {"data": "销售数据..."})
    print(prompt.messages[0].content)

三、实战:基于 FastMCP + OpenAI SDK 实现极简 Agent

不依赖任何 Agent 框架,直接用 OpenAI 原生 SDK + FastMCP 客户端实现完整工具调用闭环,更直观地体现 MCP 协议的解耦价值。

3.1 核心执行流程

  1. 能力拉取:从 MCP 服务器拉取工具清单,直接映射为模型可识别的 function calling 格式
  2. 模型决策:将用户问题 + 工具描述传入模型,模型自主判断是否调用工具
  3. 工具执行:解析模型返回的工具调用指令,通过 FastMCP 客户端执行对应工具
  4. 结果生成:将工具执行结果回填到对话上下文,再次调用模型生成最终回答

3.2 核心代码

python 复制代码
import asyncio
import json
from openai import AsyncOpenAI
from fastmcp import Client


class MCPAgent:
    def __init__(self, mcp_url: str, api_key: str, model: str = "gpt-4o-mini"):
        self.llm = AsyncOpenAI(api_key=api_key)
        self.model = model
        self.mcp_url = mcp_url
        self.mcp_client = None
        self.tools = []

    async def init(self):
        """连接 MCP 服务器,拉取工具并转换为 OpenAI 格式"""
        self.mcp_client = Client(self.mcp_url)
        await self.mcp_client.__aenter__()
        
        # MCP 的 inputSchema 本身就是标准 JSON Schema,可直接复用
        mcp_tools = await self.mcp_client.list_tools()
        self.tools = [
            {
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema
                }
            }
            for tool in mcp_tools
        ]

    async def run(self, query: str) -> str:
        messages = [
            {"role": "system", "content": "你是智能助手,可调用工具回答问题,回答简洁准确。"},
            {"role": "user", "content": query}
        ]

        # 最多 3 轮工具调用,防止死循环
        for _ in range(3):
            resp = await self.llm.chat.completions.create(
                model=self.model,
                messages=messages,
                tools=self.tools,
                tool_choice="auto"
            )
            msg = resp.choices[0].message
            if not msg.tool_calls:
                return msg.content

            # 模型回复加入上下文
            messages.append(msg.model_dump())

            # 批量执行工具调用
            for tc in msg.tool_calls:
                name = tc.function.name
                args = json.loads(tc.function.arguments)
                # 通过 MCP 协议调用工具
                result = await self.mcp_client.call_tool(name, args)
                result_text = "\n".join([item.text for item in result.content])
                # 结果回填上下文
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "name": name,
                    "content": result_text
                })

        return "已达到最大调用次数,请求未完成"

    async def close(self):
        if self.mcp_client:
            await self.mcp_client.__aexit__(None, None, None)


async def main():
    agent = MCPAgent(
        mcp_url="http://localhost:8000/mcp",
        api_key="替换为你的 OpenAI API Key"
    )
    await agent.init()

    try:
        print("=== 测试1:单工具调用 ===")
        print(await agent.run("北京今天天气怎么样,加上预报"))

        print("\n=== 测试2:多工具并行调用 ===")
        print(await agent.run("上海和广州的天气分别是什么?"))
    finally:
        await agent.close()


if __name__ == "__main__":
    asyncio.run(main())