一句话总结:这篇用一个下午能跑完的实战项目,带你从零写出第一个 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 级别的:
- 必须校验 Origin 头。不校验会被 DNS rebinding 攻击打——恶意网页诱导你的浏览器访问本地 MCP 服务。SDK 默认处理了,自己包 HTTP 层的话别忘了。
- 本地跑只绑 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。
- 装环境
- 十行代码的最小实现
- 本地验证与接入客户端


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