用 FastMCP 写一个真能跑的 MCP Server,从安装到接入 Claude Code 全过程

MCP 协议火了半年多,各种 MCP Server 满天飞。但我翻了一圈,发现大部分教程停在"概念介绍"阶段——讲完协议规范就没了,读者看完还是不知道怎么动手。

这篇文章换个思路:直接写代码。从装包到写第一个 tool,再到本地跑通、接入 Claude Code,最后加点实际功能进去。全程用 FastMCP,目前 GitHub 24000+ star,官方 MCP Python SDK 的底层就是它。

为什么选 FastMCP

MCP 协议本身定义了一套 JSON-RPC 通信规范,手写的话要处理 transport 层、schema 生成、参数校验这些。FastMCP 把这些全包了,你只需要写 Python 函数,加个装饰器就行。

几个数据:

  • PyPI 日均下载量过百万
  • 据官方统计,70% 的 MCP Server(包括非 Python 语言的)底层用了 FastMCP
  • 支持 stdio 和 HTTP 两种传输方式,本地和远程都能用

不需要学新的 DSL,不需要写 YAML 配置,就是写 Python 函数。

安装

推荐用 uv,比 pip 快不少:

uv pip install fastmcp

装完验证一下:

fastmcp version

如果输出版本号(我这里是 2.13.x),就没问题。也可以用 pip:

pip install fastmcp

第一个 Server:5 行代码

新建 my_server.py

from fastmcp import FastMCP

mcp = FastMCP("我的工具箱")

@mcp.tool
def greet(name: str) -> str:
    """跟某人打个招呼"""
    return f"你好,{name}!"

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

跑起来:

python my_server.py

默认走 stdio 传输,这个模式下 Server 通过标准输入输出跟客户端通信。你在终端里看不到什么输出,这是正常的——它在等客户端连过来。

想用 HTTP 方式跑?改最后一行:

if __name__ == "__main__":
    mcp.run(transport="http", port=8000)

这样 Server 会监听 8000 端口,浏览器访问 http://localhost:8000/mcp 能看到响应。

加点有用的功能

打招呼太简单了。来写几个实际能用的 tool。

查文件大小

import os

@mcp.tool
def file_size(path: str) -> str:
    """查看指定文件的大小"""
    if not os.path.exists(path):
        return f"文件不存在:{path}"
    size = os.path.getsize(path)
    if size < 1024:
        return f"{path} 大小:{size} 字节"
    elif size < 1024 * 1024:
        return f"{path} 大小:{size / 1024:.1f} KB"
    else:
        return f"{path} 大小:{size / 1024 / 1024:.1f} MB"

查天气(调 wttr.in)

import urllib.request
import json

@mcp.tool
def weather(city: str) -> str:
    """查某个城市的当前天气"""
    url = f"https://wttr.in/{city}?format=j1"
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:
            data = json.loads(resp.read())
        current = data["current_condition"][0]
        temp = current["temp_C"]
        desc = current["weatherDesc"][0]["value"]
        humidity = current["humidity"]
        return f"{city} 当前天气:{desc},温度 {temp}°C,湿度 {humidity}%"
    except Exception as e:
        return f"查询失败:{str(e)}"

简单计算器

@mcp.tool
def calc(expression: str) -> str:
    """计算数学表达式,比如 '2 + 3 * 4'"""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "表达式包含不允许的字符"
    try:
        result = eval(expression)
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算出错:{str(e)}"

注意这个 calc 用了 eval,生产环境别这么干,用 ast.literal_eval 或者 sympy 更安全。这里是演示用。

加 Resource:暴露数据给 LLM

tool 是让 LLM 调用的,resource 是让 LLM 读取的。比如你想把一份配置暴露出去:

@mcp.resource("config://app")
def app_config() -> dict:
    return {
        "version": "1.2.0",
        "debug": False,
        "max_retries": 3,
        "api_endpoint": "https://api.example.com"
    }

LLM 可以读这个 resource 来了解你的应用配置,再决定怎么使用 tool。

接入 Claude Code

重点来了。写完 Server 得有个客户端用才行。拿 Claude Code 举例。

stdio 方式(本地直连)

在你的项目目录下创建 .mcp.json

{
  "mcpServers": {
    "my-tools": {
      "command": "python",
      "args": ["my_server.py"],
      "cwd": "/你的项目路径"
    }
  }
}

重启 Claude Code,它会自动识别并连接。你可以在对话里说"帮我查一下 /tmp/test.txt 的文件大小",Claude Code 会自动调用你写的 file_size tool。

HTTP 方式(远程连接)

先把 Server 用 HTTP 方式跑起来:

python my_server.py
# 或者用 CLI:
fastmcp run my_server.py:mcp --transport http --port 8000

.mcp.json 改成:

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

HTTP 方式的好处是 Server 可以跑在远程机器上,多个客户端共享一个 Server。

在 Cursor 里接入

Cursor 的配置方式差不多。打开 Settings → MCP,添加一个 Server:

{
  "my-tools": {
    "command": "python",
    "args": ["/绝对路径/my_server.py"]
  }
}

用 Client 测试

不想开 Claude Code 也行,FastMCP 自带 Client:

import asyncio
from fastmcp import Client

client = Client("http://localhost:8000/mcp")

async def test():
    async with client:
        # 列出所有 tool
        tools = await client.list_tools()
        print(f"可用工具:{[t.name for t in tools]}")

        # 调用 weather tool
        result = await client.call_tool("weather", {"city": "Beijing"})
        print(result)

        # 调用 calc tool
        result = await client.call_tool("calc", {"expression": "3.14 * 5 * 5"})
        print(result)

asyncio.run(test())

跑一下,能看到工具列表和调用结果,说明 Server 没问题。

踩坑记录

实测过程中碰到几个问题,列一下:

1. stdio 模式下 print 会干扰通信

Server 的 stdio 传输用的是标准输入输出,如果你在代码里 print() 调试信息,会被当成协议消息解析,然后报错。解决办法:用 logging 写到 stderr,或者换 HTTP 传输开发。

import sys
print("调试信息", file=sys.stderr)  # 写到 stderr,不影响 stdio 通信

2. tool 函数的 docstring 很重要

LLM 根据 docstring 来决定什么时候调用哪个 tool。写太模糊了 LLM 会乱调,写太具体了又不够灵活。我的经验是用一句话说清楚这个 tool 干什么,参数名取有意义的英文名。

3. 返回值尽量是字符串

虽然 FastMCP 支持返回 dict、list 等类型,但返回字符串最稳。因为 MCP 协议最终传输的是文本,中间有个序列化过程,字符串最不容易出问题。复杂数据可以用 json.dumps() 转一下。

4. HTTP 模式的端口冲突

8000 端口经常被占用。指定端口前先查一下:

lsof -i :8000

被占了就换个端口,9100、9200 这种不太常用的。

完整代码

把前面的代码整合一下,一个带 4 个 tool + 1 个 resource 的 Server:

# my_server.py
import os
import json
import sys
import urllib.request
from fastmcp import FastMCP

mcp = FastMCP("我的工具箱")

@mcp.tool
def greet(name: str) -> str:
    """跟某人打个招呼"""
    return f"你好,{name}!"

@mcp.tool
def file_size(path: str) -> str:
    """查看指定文件的大小"""
    if not os.path.exists(path):
        return f"文件不存在:{path}"
    size = os.path.getsize(path)
    if size < 1024:
        return f"{path} 大小:{size} 字节"
    elif size < 1024 * 1024:
        return f"{path} 大小:{size / 1024:.1f} KB"
    else:
        return f"{path} 大小:{size / 1024 / 1024:.1f} MB"

@mcp.tool
def weather(city: str) -> str:
    """查某个城市的当前天气"""
    url = f"https://wttr.in/{city}?format=j1"
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:
            data = json.loads(resp.read())
        current = data["current_condition"][0]
        temp = current["temp_C"]
        desc = current["weatherDesc"][0]["value"]
        humidity = current["humidity"]
        return f"{city} 当前天气:{desc},温度 {temp}°C,湿度 {humidity}%"
    except Exception as e:
        return f"查询失败:{str(e)}"

@mcp.tool
def calc(expression: str) -> str:
    """计算数学表达式,比如 '2 + 3 * 4'"""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "表达式包含不允许的字符"
    try:
        result = eval(expression)
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算出错:{str(e)}"

@mcp.resource("config://app")
def app_config() -> dict:
    return {
        "version": "1.2.0",
        "debug": False,
        "max_retries": 3,
    }

if __name__ == "__main__":
    transport = sys.argv[1] if len(sys.argv) > 1 else "stdio"
    if transport == "http":
        mcp.run(transport="http", port=9100)
    else:
        mcp.run()

用法:

# 本地 stdio 模式
python my_server.py

# HTTP 模式
python my_server.py http

下一步可以做什么

Server 跑起来之后,可以往上叠功能:

  • 接数据库:写个 tool 查 SQLite 或 PostgreSQL
  • 接内部 API:把公司内部的 REST API 包一层,通过 MCP 暴露给 LLM
  • 加鉴权:FastMCP 的 auth 参数支持 OAuth 和 Token 验证,HTTP 模式下可以限制谁能调用
  • 部署到 Prefect Horizon:免费托管,推上 GitHub 就能用

FastMCP 的文档在 gofastmcp.com,比较详细,遇到问题先翻文档。

这篇的代码我都在 macOS + Python 3.11 + FastMCP 2.13 上跑过,Linux 和 Windows 理论上没区别。要是碰到问题,大概率是 Python 版本的事——FastMCP 要求 3.10 以上。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐