首页 / 文章

用 Python 构建你的第一个 MCP 服务器:工具、资源、提示语与检查器

使用 FastMCP 在 Python 中构建 MCP 服务器:一个提供工具、资源与提示的简易辅助工具,支持标准输入/输出与 HTTP 接口,具备调试功能,且一个配置即可服务 3 个客户端。

发布者:REACTAPP.TOP阅读约 9 分钟2056 词

来源: What Is MCP? Build a Custom MCP Server in Python

想象有三个几乎相同的 Python 文件,分别命名为 Claude、Cursor 和 ChatGPT,它们都调用 Slack API,解析响应并将其重新格式化以适配特定的 AI 客户端。现在再想象用一个所有这些客户端都能直接读取的单一文件来替代这三个文件。当 Slack 更改某个字段时,只需修改一次即可。那个单一文件其实就是 MCP 服务器,而构建这样一个服务器大约只需要一个下午的时间。

几句话讲清楚 MCP

模型上下文协议简单到只需三条消息就能描述清楚。客户端连接到服务器并询问它能做什么。服务器会回复模型可以调用的工具、主机可以获取的数据以及用户可以选择的提示词。随后客户端会带着参数调用某个工具,服务器执行该工具并给出回复。在网络传输中,就只有这些内容。

MCP值得使用的关键并非消息格式,而是共同的协议。Claude Code、Cursor、VS Code的Copilot等工具都遵循相同的交互规范,因此现在编写的Python文件日后可以添加到这些工具中的任意一个上。

服务器可提供以下三种功能之一:

  • 工具是模型可通过名称调用的函数,例如用于搜索数据库、推送分支或汇总日志的函数。
  • 资源是只读资料,比如配置文件、数据结构或最近的提交记录,主机在需要时会获取这些资料。
  • 提示词是用户可选择的可重复使用模板,它们会在Claude Code中以斜杠命令的形式出现,或在Claude Desktop中以菜单项的形式呈现。

实际上,大多数服务器都会提供工具,而资源与提示语则处于次要位置。如果只能学习这三者中的一项,那就选择学习工具。消息的传输方式以及认证机制的重要性会在之后才会显现。目前请记住这一点:一个服务器,多个客户端,需要暴露三样内容。

为何集成方案曾大量涌现

在MCP出现之前,要将AI工具与后端连接起来,就需要编写适配器,使后端的接口格式与客户端所需的工具调用格式相匹配。有的客户端要求使用Anthropic的工具架构,有的则采用自己的格式,还有的是需要OpenAI的函数调用结构。底层的Slack搜索功能保持不变,但围绕它却出现了三层不同的封装层。

这种架构的复杂性会迅速上升。三个客户端加上两个后端意味着需要维护六个适配器。再增加一个客户端就变成八个,第三个后端则使总数达到十二个,每个组件都有各自的认证机制、重试策略及特殊要求。MCP将这种复杂结构拆分开来,客户端与后端不再直接相互通信,二者都通过协议进行交互,因此引入新的AI客户端只需添加一条配置项,而无需进行新的集成工作。

该设计刻意保持了不对称性:客户端决定一切——决定加载哪些服务器、何时调用工具以及如何展示结果;服务器的职责则相对简单,只需声明自身的工具并在被调用时作出响应。正是这种不对称性使得该协议保持简洁,便于各大客户端快速实现。

一个文件内的站立会议辅助工具

站立会议辅助工具是一种经典的内部工具,工程师们不断对其进行重写。它会读取本地的 git 日志,总结开发人员昨天的工作内容。在这里,它将以一个 Python 文件的形式同时充当工具、资源以及提示功能。

你需要 FastMCP 包,它是许多 MCP 服务器所依赖的高级 Python 层。

pip install fastmcp

创建 standup_server.py 文件。首先导入相关库并定义该工具。

# standup_server.py
import subprocess
from typing import TypedDict

from fastmcp import FastMCP

mcp = FastMCP("standup-helper")


class StandupSummary(TypedDict):
    branch: str
    since: str
    commit_count: int
    commits: list[str]


@mcp.tool()
def summarize_standup(
    branch: str = "main",
    since: str = "yesterday",
) -> StandupSummary:
    """Summarize recent git activity for a standup.

    Reads the local git log on the given branch since the
    given time window. Returns commit count and one-line
    subjects for each commit. Used by AI clients via MCP.
    """
    try:
        result = subprocess.run(
            [
                "git", "log",
                f"--since={since}",
                "--pretty=format:%h %s",
                branch,
            ],
            capture_output=True,
            text=True,
            timeout=5,
            check=True,
        )
    except (subprocess.CalledProcessError,
            subprocess.TimeoutExpired) as exc:
        return {
            "branch": branch,
            "since": since,
            "commit_count": 0,
            "commits": [f"git error: {exc}"],
        }

    lines = [
        line for line in result.stdout.splitlines() if line
    ]
    return {
        "branch": branch,
        "since": since,
        "commit_count": len(lines),
        "commits": lines,
    }


# resources and prompts come next

在幕后,该装饰器承担四项功能:读取类型提示以生成 JSON 模式,使用文档字符串作为展示给模型的描述,在握手过程中将该工具列在服务器的功能列表中,以及将传入的 JSON-RPC 调用路由到相应函数。

请注意其中设置了严格五秒超时的子进程调用。如果 git 命令卡住,等待工具响应的主应用程序也会随之挂起,因此每个外部调用都需要设置超时。错误处理分支返回的词典结构与成功处理分支相同,这样模型总能获得可预测的数据格式。更复杂的工具还可以通过再添加一个子进程调用并将该输出添加到词典中,来调用 GitHub CLI 处理开放的拉取请求。

接下来是资源信息和提示信息,它们共同构成了完整的文件内容。

# standup_server.py (continued)

@mcp.resource("recent_commits://main")
def recent_commits_main() -> str:
    """Last 10 commits on the main branch, plain text.

    Resources are pulled by the host opportunistically.
    They are not invoked by the model the way tools are.
    """
    result = subprocess.run(
        [
            "git", "log",
            "-n", "10",
            "--pretty=format:%h %ad %s",
            "--date=short",
            "main",
        ],
        capture_output=True,
        text=True,
        timeout=5,
    )
    return result.stdout or "(no commits found)"


@mcp.prompt("standup_template")
def standup_template(focus: str = "shipping work") -> str:
    """Reusable standup question exposed as a prompt
    template. Surfaces as a slash command in clients that
    expose prompts (e.g. /standup_template in Claude Code).
    """
    return (
        f"Summarize what I worked on yesterday, focusing on "
        f"{focus}. Use the summarize_standup tool to get the "
        f"git log, then write a one-paragraph standup note."
    )


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

资源 URI 只是一段字符串,其格式并无严格规范,但通常为协议名加上路径;当需要参数时,FastMCP 支持 URI 模板。提示词则是一个返回格式化文本的函数,客户端会将其作为用户可调用的模板呈现,而 Claude Code 会将其作为斜杠命令提供。提示词绝不会被自动注入,始终由用户自行选择。

最后一行用于启动服务器,由于 FastMCP 已处理好了标准输入输出相关功能,因此无需指定端口、Web 框架或事件循环。

传输方式与身份验证

传输方式决定了 JSON-RPC 消息在客户端与服务器之间的传输方式。

对于本地服务器,可使用stdio。主机会将你的脚本作为子进程启动。Stdio可以绕过整个网络堆栈:无需端口、无需TLS,也无需身份验证,因为子进程会继承主机的信任边界。

当服务器需要在多次请求之间保持状态或可从多台机器访问时,应使用Streamable HTTP。远程服务器面临网络环境,因此需要严格的身份验证。

# bottom of standup_server.py

if __name__ == "__main__":
    # Default transport is stdio. The host (Claude Code,
    # Cursor, Claude Desktop, etc.) launches this script
    # as a subprocess and talks to it over stdin/stdout.
    # No port, no TLS, no auth. The trust boundary is
    # whoever launched the host.
    mcp.run()

    # To expose the same server over the network instead,
    # use Streamable HTTP. SSE was deprecated in the
    # March 2025 spec update. Do not use it for new code.
    #
    # Production HTTP also needs an auth layer in front.
    # OAuth 2.1 with Dynamic Client Registration is the
    # current pattern. See Week 22 for the full flow.
    #
    # mcp.run(
    #     transport="streamable-http",
    #     host="0.0.0.0",
    #     port=8000,
    # )

不带参数调用 run 即表示使用 stdio,注释部分展示了 HTTP 升级的过程。此版本仍采用 stdio,这对大多数内部开发工具而言是正确的选择。仅读取本地文件或本地仓库的工具根本不应通过网络接口来访问。

本地开发流程

不要直接将新服务器连接到 AI 客户端来测试它。客户端被设计为具备容错能力,它们会默默忽略模式不匹配和缺失的功能,以确保聊天功能正常运行;而当工具出现故障时,模型只会道歉。

应使用官方的 MCP Inspector,这是一个基于浏览器的开发界面,可通过 stdio 与服务器通信。

npx @modelcontextprotocol/inspector python standup_server.py

该命令会将你的脚本作为子进程启动,并打开一个本地控制面板。你可以查看原始的握手信息,确切了解服务器声明了哪些功能,还能实时运行各种工具和资源。

在这个流程中通常会出现三个问题:

  1. 架构不匹配。函数的返回值与其类型提示不一致,检查工具会立即指出这一问题。
  2. 缺少功能。你调用了服务器从未声明过的功能,此时控制面板会显示协商过程中实际注册的功能。
  3. 输出污染。任何多余的print()语句都会破坏JSON-RPC数据流,因为标准输入输出会携带这些消息。建议使用Python的logging模块将日志输出到标准错误流,因为该协议规定标准错误流用于可读性的诊断信息。

FastMCP 还提供了一种开发模式,当文件发生变化时会重启服务器,这有助于在 Inspector 中进行迭代开发。

一个服务器,多个客户端

现在的设计优势就体现出来了。只有一个 Python 文件,可在 Inspector 中进行验证,而且无需任何针对特定客户端的代码即可将其连接到不同的 AI 客户端。每个客户端都会读取一个配置文件,指定要启动的本地服务器,而 Claude Code、Cursor 和 Claude Desktop 中的 JSON 结构是完全一致的:即一个 mcpServers 对象,其中包含要运行的命令及其参数。

{
  "mcpServers": {
    "standup-helper": {
      "command": "python",
      "args": ["/Users/you/code/standup_server.py"]
    }
  }
}
{
  "mcpServers": {
    "standup-helper": {
      "command": "python",
      "args": ["/Users/you/code/standup_server.py"]
    }
  }
}
{
  "mcpServers": {
    "standup-helper": {
      "command": "python",
      "args": ["/Users/you/code/standup_server.py"]
    }
  }
}

三种方式中的密钥、命令及参数都完全相同,只有每个客户端所使用的配置文件位置不同。

打开任意一个客户端并请求进行站会交流。客户端会启动 Python 进程,通过标准输入输出进行功能协商,向模型展示汇总工具,再将模型的响应转发回你的脚本。Git 会随之运行,字典会返回数据,模型也会每次沿着相同的代码路径来记录站会内容。

ChatGPT 不在列表中,因为它的连接器是通过 Streamable HTTP 与远程服务器通信的。若要将此辅助工具集成到 ChatGPT 中,就需要一个公共 URL 和认证层,这与这种完全本地运行的架构在运行方式上存在很大差异。

何时不应使用 MCP

该协议并非万能工具,有些场景并不适合使用它:

  • 对延迟极为敏感的路径。每次工具调用都会产生一次JSON-RPC往返通信,因此处于紧密内循环中的工具应采用直接SDK集成方式。
  • 自定义交互式输出。如果需要特定图表或自定义表单,那就需要构建主机集成,因为该协议将数据与展示分离。
  • 完全由你掌控的配置。如果由同一团队同时开发AI客户端和后端,直接将它们连接起来通常比在中间插入MCP更易于维护。

小巧的协议,巨大的变革

整个协议由三个原始组件、两种传输方式以及一次JSON-RPC握手构成。正是这种简洁的设计成为了它的优势:它将负责构建智能系统的人与提供上下文信息的人分离开来。编写第一个服务器只需一个下午的时间,而每当有新的AI工具出现时,这一投入都会得到回报。

相关阅读

  • LangGraph中的自愈多智能体团队:架构师、测试员、调试器 — 用Python构建LangGraph状态机,让架构师、开发人员、pytest沙箱、调试器和审查员协同工作并循环迭代,直至测试通过。
  • 使用llama.cpp和OpenCode将Qwen3.8-Flash-Next作为编码智能体在本地运行 — 详细指导如何使用llama.cpp在自己的GPU上运行125B参数量的MoE编码模型,通过类OpenAI的API提供服务,并通过OpenCode对其进行控制。
  • 实用笔记:什么是检索增强生成(RAG)?实用指南 — 为采用该模式的团队提供的实用笔记:什么是检索增强生成(RAG)?实用指南的详细操作步骤,包括相关契约、检查项以及可直接使用的代码片段。