MCP.so
登录
返回博客

Python MCP Server:用官方 SDK v2 构建 MCP 服务器(2026)

2026年10月15日MCP.so TeamMCP.so Team

用官方 mcp SDK v2 构建 Python MCP Server:uv 项目搭建、工具、资源、提示词、类型化参数、异步、Streamable HTTP、pytest 测试以及 uvx 打包发布。

要构建一个 Python MCP Server,先用 uv add "mcp[cli]" 安装官方 SDK,创建一个 MCPServer,再用 @mcp.tool()、@mcp.resource() 和 @mcp.prompt() 装饰普通的 Python 函数即可。类型注解就是输入 schema。mcp.run() 默认走 stdio,mcp.run(transport="streamable-http") 则提供 HTTP 服务。你可以用 Client(mcp) 在内存中测试它,最后打包发布,让用户通过 uvx 运行。

下文所有内容都在 mcp 2.3.0(2026 年 10 月的最新稳定版)上实际运行过。它实现了 2026-07-28 协议修订版,同时仍能服务旧版客户端。如果你看过以前的教程,最明显的变化是:v1 里的 FastMCP 类现在叫 MCPServer。

官方 SDK 还是 FastMCP:选哪个?

有两个 Python 库使用相似的装饰器,网上大多数文章都把它们混为一谈:

官方 SDK(mcp)独立版 FastMCP(fastmcp)
维护方Model Context Protocol 项目Prefect
服务器类from mcp.server import MCPServerfrom fastmcp import FastMCP
工具装饰器@mcp.tool()@mcp.tool
HTTP 传输名称"streamable-http""http"(或 "streamable-http")
当前版本2.3.04.1.0(基于 mcp 2.x)
额外能力核心协议、mcp CLI认证提供方、服务器组合、Apps、自带 CLI

混淆源于历史。FastMCP 1.0 在 2024 年被合并进官方 SDK,成为 mcp.server.fastmcp.FastMCP。独立项目之后继续单独发展;而在 SDK v2 中,内置类被重命名为 MCPServer,所以 from mcp.server.fastmcp import FastMCP 现在会抛出 ModuleNotFoundError。本指南使用官方 SDK。如果你选择独立版 FastMCP,思路是相通的,但请以它自己的文档为准。

第 1 步:用 uv 搭建项目

你需要 Python 3.11+(以 SDK 文档为准)和 uv。

uv init --package pypi-info
cd pypi-info
uv add "mcp[cli]" httpx

--package 会生成 src/pypi_info/ 目录结构以及一个 [project.scripts] 条目,后面用 uvx 时需要它。cli 扩展会额外安装 mcp 命令(mcp dev、mcp run、mcp install)。示例服务器调用的是 PyPI JSON API,因此不需要 API key。

第 2 步:编写服务器

创建 src/pypi_info/server.py:

from typing import Annotated

import httpx
from pydantic import BaseModel, Field

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

mcp = MCPServer("pypi-info")


class PackageInfo(BaseModel):
    name: str
    version: str
    summary: str | None
    requires_python: str | None
    recent_versions: list[str] = []


@mcp.tool()
async def get_package(
    name: Annotated[str, Field(description="Exact PyPI project name, e.g. 'httpx'")],
    include_versions: Annotated[int, Field(ge=0, le=20, description="How many recent releases to list")] = 0,
) -> PackageInfo:
    """Get the latest version, summary and Python requirement of a PyPI package. Use before pinning a dependency."""
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(f"https://pypi.org/pypi/{name}/json")
    if r.status_code == 404:
        raise ToolError(f"No PyPI project named {name!r}. Check the spelling.")
    r.raise_for_status()
    data = r.json()
    info = data["info"]
    return PackageInfo(
        name=info["name"],
        version=info["version"],
        summary=info["summary"],
        requires_python=info["requires_python"],
        recent_versions=list(data["releases"])[-include_versions:] if include_versions else [],
    )


@mcp.resource("pypi://about")
def about() -> str:
    """What this server does and where its data comes from."""
    return "Package metadata comes from the public PyPI JSON API."


@mcp.prompt()
def compare(first: str, second: str) -> str:
    """Compare two PyPI packages for a new project."""
    return (
        f"Use the get_package tool on {first} and {second}, then compare them on "
        "maintenance, Python support and scope. Recommend one."
    )


def main() -> None:
    mcp.run()


if __name__ == "__main__":
    main()

SDK 会从这个文件中读取以下信息:

  • 名称和描述来自函数名和 docstring。docstring 是写给模型看的:说明这个工具做什么、什么时候该用。
  • 类型化参数:Annotated[..., Field(...)] 为每个参数添加描述和约束。ge=0, le=20 会变成 schema 里的 minimum/maximum,传入 include_versions=999 的调用会在你的函数运行前就被拒绝。模型看到这个错误后会重试。Literal[...] 可以定义枚举,Pydantic 模型参数可以定义嵌套对象。
  • 结构化输出:返回 Pydantic 模型会为工具生成输出 schema。客户端会同时拿到可读文本(content)和类型化的 structured_content。
  • 异步:涉及 I/O 时用 async def。普通的 def 工具也可以用,SDK 会把它放到工作线程里运行,不会阻塞服务器。
  • 错误:抛出 ToolError,并附上模型能据此采取行动的消息。它会作为 is_error 结果返回,而不是让程序崩溃。

工具由模型控制,资源是由应用控制的数据,提示词是用户挑选的模板。何时用哪个,请看 MCP 资源与工具的区别。

如果某个工具需要汇报进度或读取服务器自己的资源,声明一个 ctx: Context 参数(from mcp.server.mcpserver import Context),然后调用 await ctx.report_progress(...) 或 await ctx.read_resource(...)。这个参数由 SDK 自动填入,模型看不到它。

第 3 步:运行并检查

uv run mcp dev src/pypi_info/server.py

这会在 MCP Inspector 中启动你的服务器(PATH 上需要有 npx)。打开打印出的 URL,在 Tools 标签页调用 get_package,在 Resources 下读取 pypi://about,在 Prompts 下渲染 compare。

在 stdio 模式下,stdout 承载的是协议数据。请使用写入 stderr 的 logging 模块,不要用 print()。

第 4 步:通过 Streamable HTTP 提供服务

本地客户端通过 stdio 启动你的文件。要把服务器提供给手上没有你代码的人,就要通过 URL 提供服务:

uv run mcp run src/pypi_info/server.py --transport streamable-http

也可以在代码里设置:mcp.run(transport="streamable-http", port=8000)。host、port、streamable_http_path(默认 /mcp)和 json_response 等选项要传给 run(),而不是 MCPServer 构造函数。客户端连接 http://127.0.0.1:8000/mcp。

在部署到真实域名之前,有一点必须知道:应用默认开启 DNS rebinding 防护,在你传入允许列表之前只接受 localhost 的 Host 头。没有允许列表时,所有请求都会得到 421 Misdirected Request:

from mcp.server.transport_security import TransportSecuritySettings

security = TransportSecuritySettings(
    allowed_hosts=["mcp.example.com", "mcp.example.com:*"],
    allowed_origins=["https://app.example.com"],
)
app = mcp.streamable_http_app(transport_security=security)
# run with: uvicorn pypi_info.server:app --workers 4

现代(2026-07-28)客户端没有会话,所以任何 worker 都能处理任何请求,无需粘性会话。托管、OAuth 和扩容请参阅如何部署 MCP 服务器。

第 5 步:用 pytest 测试

Client 可以在内存中直接连接服务器对象,无需子进程,也无需端口:

uv add --dev pytest anyio
# tests/test_server.py
import pytest
from mcp import Client

from pypi_info.server import mcp


@pytest.fixture
def anyio_backend():
    return "asyncio"


@pytest.mark.anyio
async def test_get_package():
    async with Client(mcp, raise_exceptions=True) as client:
        result = await client.call_tool("get_package", {"name": "httpx"})
        assert not result.is_error
        assert result.structured_content["name"] == "httpx"


@pytest.mark.anyio
async def test_unknown_package_is_tool_error():
    async with Client(mcp) as client:
        result = await client.call_tool("get_package", {"name": "no-such-package-xyz-123"})
        assert result.is_error

运行 uv run pytest。raise_exceptions=True 会让你在测试中看到真实的服务端错误,而不是远程调用方收到的通用消息。把同一个 Client 指向 "http://localhost:8000/mcp",就能对 HTTP 服务器做冒烟测试。

第 6 步:连接到客户端

所有本地客户端用的都是同一条启动命令,所以请使用绝对路径:

claude mcp add pypi-info -- uv run --directory /absolute/path/to/pypi-info pypi-info

对于 Claude Desktop,uv run mcp install src/pypi_info/server.py 会替你写好配置条目。加上 -v API_KEY=... 或 -f .env 可以传入环境变量。Cursor 和 VS Code 在各自的 JSON 文件中使用同样的 command 和 args。详见 Claude Desktop、Cursor 和 VS Code 指南。

第 7 步:为 uvx 打包

uv init --package 已经生成了一个控制台脚本,把它指向 main:

[project.scripts]
pypi-info = "pypi_info.server:main"

像用户一样构建并试用 wheel:

uv build
npx @modelcontextprotocol/inspector --cli uvx --from ./dist/pypi_info-0.1.0-py3-none-any.whl pypi-info --method tools/list

用 uv publish 发布。之后任何人都可以运行 uvx pypi-info,客户端配置就变成 "command": "uvx", "args": ["pypi-info"]。要把服务器收录到官方 MCP Registry,在 README 里加一行类似 mcp-name: io.github.your-username/pypi-info 的内容(写在 HTML 注释里也可以),Registry 就是靠它验证 PyPI 包的所有权。然后按照构建 MCP 服务器中的说明运行 mcp-publisher init、login 和 publish。

常见错误

  • 导入 mcp.server.fastmcp:这个路径在 v2 中已被移除。请使用 from mcp.server import MCPServer;如果还没准备好迁移,就固定 mcp<2。
  • 把 port= 传给 MCPServer(...):这会抛出 TypeError。传输选项应该传给 run()。
  • 客户端配置中使用相对路径:客户端会在它自己的工作目录下启动你的服务器。
  • 新建 SSE 服务器:transport="sse" 仍可用于兼容旧客户端,但自 2025-03-26 起已被取代。请基于 Streamable HTTP 构建(原因)。

下一步

常见问题

FastMCP 和官方 MCP Python SDK 是一回事吗?

不是。FastMCP 1.0 在 2024 年被合并进了官方 SDK,但 Prefect 维护的独立 fastmcp 包现在是一个单独的项目。在官方 SDK v2 中,内置类叫 MCPServer,通过 from mcp.server import MCPServer 导入。

如何运行 Python MCP 服务器?

调用 mcp.run() 走 stdio,这是 Claude Code、Cursor 等本地客户端使用的方式;或调用 mcp.run(transport='streamable-http'),在 8000 端口的 /mcp 路径提供 HTTP 服务。开发时可以用 uv run mcp dev server.py 在 MCP Inspector 中打开服务器。

如何分享 Python MCP 服务器,让别人用 uvx 运行?

添加一个指向调用 mcp.run() 的函数的 [project.scripts] 条目,用 uv build 构建,再用 uv publish 发布。之后用户可以用 uvx 加包名启动它,客户端配置中 command 写 uvx,参数写你的包名。

MCP Python SDK 需要哪个 Python 版本?

官方 SDK v2 文档面向 Python 3.11 或更高版本。用 uv add 'mcp[cli]' 或 pip install 'mcp[cli]' 安装,即可同时获得 SDK 和 mcp 命令行工具。