当前位置:首页 > 编程开发 > 正文

MCP Server 开发实战:从零用 Python 给自己写一个 AI 工具,附完整代码与避坑清单

约 7891 字 · 预计阅读 20 分钟
一句话总结:这篇用一个下午能跑完的实战项目,带你从零写出第一个 MCP Server——从协议背景、十行代码的最小实现,到带参数校验的实用工具、Inspector 调试和远程部署,新手最常踩的坑一次讲完。

给 AI 接工具这件事,现在已经不算新鲜了。真正值得说的是背后那套协议:MCP(Model Context Protocol,模型上下文协议)由 Anthropic 在 2024 年 11 月 25 日发布,到 2026 年 10 月已经成了行业事实标准,2025 年 12 月还被捐给了 Linux 基金会旗下的 Agentic AI Foundation。官方口径很夸张:Tier 1 SDK 每月下载量接近五亿次,TypeScript 和 Python 两个 SDK 累计下载都破了十亿(数据来源:MCP 官方博客 2026-07-28 发布文)。

但网上的教程,大多到 Hello WoRLd 就停了。你跟着敲一遍,看到一个 greet 工具能跑,然后呢?真想给自己写一个能用的工具时,问题才刚开始:参数怎么设计模型才不会填错?调试靠什么?本地跑通了怎么给别人用?这篇就把后面这段路走完。读完你会得到一个可以直接插进 Claude Desktop、Cursor 或者你自己的 Agent 里跑的 MCP Server,以及一份避坑清单。

前置要求不高:会一点 Python(3.10 以上),不需要先懂协议。文中的代码基于官方 Python SDK(pip 包名 mcp),协议规范版本以 2026-07-28 为准,这也是写这篇时的当前版本。

先花五分钟:MCP 到底在解决什么问题

MCP 要治的病叫「M×N 问题」。假设有 M 个 AI 客户端、N 个外部工具,两边对接就要写 M×N 套胶水代码;GitHub 出一个新接口,所有客户端都得跟着改一遍。MCP 的做法是把接口统一成一套标准:工具方按协议实现一次 Server,所有支持 MCP 的客户端即插即用。

协议里只有三个角色,搞清楚这两个词后面就好懂了:

  • Host:跑模型的应用本身,比如 Claude Desktop、Cursor、你自己的 Agent。
  • Client:Host 内部的连接器,一个 Client 对一个 Server,负责说 MCP 这套「话」。
  • Server:你写的进程,对外暴露能力。

Server 能暴露三种原语:Tools(模型可调用的函数,有副作用,干活的主力)、Resources(只读数据,比如文件内容)、Prompts(预置的提示词模板)。个人开发者写自己的工具,九成场景只碰 Tools,这篇也只讲它。

传输层两条路:stdio(Server 作为本地子进程启动,走标准输入输出,适合个人本地用)和 Streamable HTTP(Server 是独立 HTTP 服务,适合远程共享)。底层都是 JSON-RPC 2.0。

图 1:MCP 的三方架构。Host 通过内置 Client 与 Server 通信,所有消息走 JSON-RPC 2.0;本地开发用 stdio,远程共享用 Streamable HTTP。

有个变化得提一句:2026-07-28 这版规范把协议改成了无状态。以前的 initialize 握手和 Mcp-Session-Id 会话头都取消了,每个请求自己带上协议版本和客户端信息。对开发者的实际影响就一条:远程 Server 不用再配粘性会话和共享会话存储,直接架在普通负载均衡后面就行。

十分钟:写出第一个 MCP Server

装环境

一行的事:

pip install "mcp[cli]"

Python 3.10+,就这一个依赖。装 [cli] 是为了用上 mcp dev 这个调试命令,省得后面单独装 Inspector。

十行代码的最小实现

新建 server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("hello-mcp")

@mcp.tool()
def greet(name: str) -> str:
    """跟指定名字的人打个招呼。"""
    return f"你好,{name}!MCP Server 跑通了。"

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

这就能跑。逐行说下它替你干了什么:

  • FastMCP 是官方 SDK 的高层封装,schema 生成、生命周期、事件循环全包了。新项目直接用 FastMCP,比自己拼底层 Server 类省掉八成的样板代码。
  • @mcp.tool() 把函数注册成工具。类型注解会自动变成 input schema——name: str 就是告诉模型「这个参数填字符串」。
  • docstring 会变成工具描述,模型选不选你的工具,主要就看这段字。所以别写废话,写清楚这个工具干什么、什么时候该用。
  • mcp.run(transport="stdio") 启动 stdio 传输,也是默认值。

本地验证与接入客户端

终端里跑 python server.py,进程挂住不退出、没有任何输出,就是正常的(stdio 协议下 stdout 是信道,不能乱打印,这点后面坑那一节还会说)。

调试用 MCP Inspector,一行命令起浏览器调试台:

npx @modelcontextprotocol/inspector python server.py

打开它打印的地址(新版默认带 session Token),切到 Tools 面板就能看到 greet,填参数点 Run Tool,JSON 响应直接出来。

确认没问题,接进客户端。Claude Desktop 的配置文件(Windows 在 %APPDATA%\Claude\claude_desktop_config.json)里加一段:

{
  "mcpServers": {
    "hello-mcp": {
      "command": "python",
      "args": ["C:/absolute/path/to/server.py"]
    }
  }
}

Cursor 用的是 ~/.cursor/mcp.json,格式一样。重启客户端,问它「跟张三打个招呼」,工具被调到,全链路就通了。

写个真能用的:把常用查询包成工具

Hello World 只能证明环境通了。下面这个例子来自真实场景——我自己的博客后台。需求很具体:想让自己电脑上的 AI 客户端直接回答「最近发了哪些 GEO 相关的文章」「ID 是 27 的那篇什么时候发的」,而不是我手动翻后台。

先定工具,定工具就是定模型的能力边界,我的原则是三条:名字和描述写成人话;参数能少则少,能带默认值就带默认值;失败不抛异常,返回可读的错误文本——模型拿到错误文本还能自己纠正,拿到协议层异常整个对话就断了。

import sys
import logging
from mcp.server.fastmcp import FastMCP

# 日志一律走 stderr,stdout 是 JSON-RPC 的信道
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logger = logging.getLogger("article-mcp")

mcp = FastMCP("article-mcp")

# 真实场景换成读数据库或调接口,这里用列表模拟
ARTICLES = [
    {"id": 27, "title": "GEO 和 SEO 到底差在哪", "category": "品牌优化", "date": "2026-10-15"},
    {"id": 28, "title": "AI 搜索到底爱引用哪些网站", "category": "品牌优化", "date": "2026-10-16"},
    {"id": 35, "title": "AI 总说错你的品牌信息:实体一致性实操", "category": "品牌优化", "date": "2026-10-17"},
]

@mcp.tool()
def list_articles(category: str = "") -> list[dict]:
    """列出博客文章,可按分类筛选。category 传「品牌优化」等分类名,留空返回全部。"""
    rows = ARTICLES
    if category:
        rows = [a for a in rows if a["category"] == category]
    logger.info("list_articles 被调用,category=%s", category or "全部")
    return rows

@mcp.tool()
def get_article(article_id: int) -> dict:
    """按 ID 查一篇文章的详情,返回标题、分类和发布日期。"""
    for a in ARTICLES:
        if a["id"] == article_id:
            return a
    return {"error": f"没有找到 ID 为 {article_id} 的文章,先用 list_articles 看看有哪些"}

两个工具,一个列表一个详情,覆盖了「先找后看」这个最常用的查询路径。注意几个细节:

返回 list[dict] 和 dict,FastMCP 会自动生成结构化输出,客户端拿到的是带 structuredContent 的结果,比纯文本好处理。返回类型支持 Pydantic 模型、TypedDict、dataclass 这些,复杂对象直接上 Pydantic。

get_article 找不到时返回的字典里带了下一步建议(「先用 list_articles 看看」),这是刻意写的——模型读到这句,大概率会自己再去调一次 list_articles,相当于你用错误信息引导了它的下一步动作。

命令行下不打开浏览器也能验证:

npx @modelcontextprotocol/inspector --cli python server.py --method tools/list
npx @modelcontextprotocol/inspector --cli python server.py --method tools/call --tool-name get_article --tool-arg article_id=27

第二条能正确返回 ID 27 那篇文章,这个 Server 就算完成了。它现在在你机器上跑,插到任何 MCP 客户端里都能用。

从本地方便到远程服务:Streamable HTTP

自己用,stdio 够了。但要分享给同事、或者让你的 Server 同时服务多个客户端,就得走 Streamable HTTP。改一行启动方式:

mcp.run(transport="streamable-http")  # 默认监听 127.0.0.1:8000/mcp

客户端配置从「command + args」换成「url + headers」:

{
  "mcpServers": {
    "article-mcp": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}

部署远程时有两条硬规矩,都是规范里 MUST 级别的:

  1. 必须校验 Origin 头。不校验会被 DNS rebinding 攻击打——恶意网页诱导你的浏览器访问本地 MCP 服务。SDK 默认处理了,自己包 HTTP 层的话别忘了。
  2. 本地跑只绑 127.0.0.1,别绑 0.0.0.0。同一条规范里还有一句 SHOULD:给所有连接加认证。远程暴露的 Server 裸奔,等于把工具权限送给全网。

好在 2026-07-28 之后无状态化了,横向扩容就是多起几个进程挂负载均衡,不用再管会话共享。这对个人开发者是个实在的利好——以前想把自己的 Server 放到云上给团队用,光会话存储就够折腾一晚上。

上面两条安全规矩我是按规范文档转述的,字段和封装细节以你手里 SDK 的版本为准,这点我没逐个 SDK 核对过,自己包 HTTP 层的话建议把官方那页文档翻出来对着看。

六个坑,每个都见过有人踩

图 2:工具不出现或调用失败时的定位流程。先用 Inspector 验证协议链路,再决定查客户端还是查 Server 代码。

坑 症状 解法
print 打到 stdout客户端显示零工具,或连上就断日志全走 stderr,协议下 stdout 是信道
配置写相对路径Server 静默启动失败args 里一律绝对路径,客户端按自己的工作目录拉进程
缺类型注解模型拼不出合法参数,工具等于不存在每个参数都加类型注解,docstring 写清用途
SDK 版本漂移抄的老教程 import 路径失效以自己安装版本的官方 quickstart 为准
改完不重启客户端新工具不出现配置只在启动时读,改完完整重启客户端
不处理异常一次崩溃,整条连接的工具全掉校验输入,错误以文本返回,别 raise

第一个坑最隐蔽。你 print("debug") 调试得挺好,客户端里工具列表就是空的——因为你的 print 混进了 JSON-RPC 消息流,协议直接解析失败。用 logging 配到 stderr 就没事,这也是官方示例全走 stderr 的原因。

FAQ

Q1:MCP Server 只能用 Python 写吗?

不是。TypeScript、Go、C# 都是 Tier 1 SDK,生态里 TS 占比也不小。只是 Python 上手最快,FastMCP 十行出活,个人工具首选。写底层服务、对性能敏感再考虑 Go。

Q2:FastMCP 和底层 Server 类怎么选?

新项目无脑 FastMCP,schema 生成、进度通知、结构化输出都白送。什么时候下沉到底层 Server 类?要自定义传输、要改握手行为、或者协议新特性 FastMCP 还没跟上——比如你急着用 2026-07-28 里的某个扩展。

Q3:一个 Server 放多少工具合适?

GitHub 官方 MCP 放了上百个,但那是团队维护的。个人工具我的经验是控制在十个以内,而且描述别重叠——两个工具描述雷同,模型选错的概率直线上升。工具多了客户端 context 也吃不消,Claude Code 后来搞的 Tool Search 就是为了治这个病。

Q4:MCP 和 Function Calling 是一回事吗?

不是一回事,但层不同。Function Calling 是模型本身的能力——模型决定调哪个函数、参数填什么;MCP 是这套调用的标准化传输层,规定了工具怎么发现、怎么调用、结果怎么回传。可以理解为 Function Calling 管「模型想调」,MCP 管「调到的路怎么修」。这块后面单独开一篇讲 Function Calling 实战,这里先埋个引子。

Q5:2026-07-28 规范变了,以前的代码要重写吗?

大部分不用。握手和会话是协议层的事,SDK 在传输层做了兼容。用 stdio 的本地 Server 基本无感;远程 Server 如果自己实现过会话管理,才需要按官方 migration notes 调整。说实话,这版规范的迁移成本比从 2025-03-26 升到 2025-11-25 那会儿小得多,那才真是伤筋动骨。

结语

MCP Server 开发这件事,门槛比宣传的低得多。官方 SDK 把协议细节包干净之后,剩下的就是普通 Python 编程——会写函数就会写工具。真正花时间的从来不是协议,是想清楚「这个工具该干什么、模型怎么才知道什么时候用它」。

我的建议是今晚就动手:挑一个你每天都要手动查的东西(书签、待办、数据库里的某张表),按上面 list_articles 的路子包成两个工具。一小时内能跑通,跑通之后你对「AI 应用开发」的理解会具体很多——这比再刷十篇解读文章都管用。

下一篇聊 MCP 协议深入:Tools、Resources、Prompts 三种原语到底该怎么设计,什么时候该用 Resources 而不是 Tools。

目录导航
  • 先花五分钟:MCP 到底在解决什么问题
  • 十分钟:写出第一个 MCP Server
    • 装环境
    • 十行代码的最小实现
    • 本地验证与接入客户端
  • 写个真能用的:把常用查询包成工具
  • 从本地方便到远程服务:Streamable HTTP
  • 六个坑,每个都见过有人踩
  • FAQ
  • 结语
  • 相关文章:

  • Function Calling 实战:schema 军规、三道闸与并发,200 次注入实测全绿之下还有 16.5% 错调2026-10-18 20:36:18
  • RAG 实战:几百行代码搭一个自己的知识库问答(Python + SQLite,不碰框架)2026-10-18 20:36:18
  • MCP 协议深入:Tools、Resources、Prompts 三种原语怎么设计才不翻车2026-10-18 20:36:18
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。