MCP(Model Context Protocol)解决的是同一个老问题:每个模型、每个工具都各写一套对接。它把「模型调用外部能力」这件事统一成一个协议,让一个 server 写一次就能被 Claude、ChatGPT 这类客户端复用,官方叫它 AI 应用的 USB-C 接口。这篇从写一个能跑的 server 开始。
三类能力
一个 MCP server 对外暴露的东西分三种:tools(工具,能被模型调用的函数)、resources(资源,能被客户端读取的数据,类似文件或 API 响应)、prompts(提示模板,预设的交互流程)。工具和资源最容易混淆:工具是模型主动触发、产生动作的,比如「查天气」「发消息」;资源是模型被动读取的上下文,比如「这份文档的内容」「这张表的数据」。prompts 则是把一段固定交互封装成模板,用户一个斜杠命令就能套用。绝大多数人第一版只会用到 tools,resources 和 prompts 是后面才补的。
最小 server
用 Python 的 FastMCP 写一个 server 最短只要十几行。FastMCP 是官方 SDK 里的高层封装,@mcp.tool() 把一个普通函数注册成工具。
# weather_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
def get_weather(city: str) -> str:
'''查询指定城市的天气。'''
return f"{city} 今天晴,25°C"
if __name__ == "__main__":
mcp.run()
函数名和 docstring 都会被当成工具的描述传给模型,模型靠它们决定什么时候调、传什么参数。所以 docstring 别写「内部用」,要写清楚参数和返回,这是工具能不能被正确触发的关键。Node 侧同样有官方 SDK,脚手架一条命令就能起:
npm install @modelcontextprotocol/sdk
npx create-mcp-server my-server
两种传输
MCP 目前主流的传输是 stdio 和 HTTP。stdio 适合本地:客户端把 server 当一个子进程拉起来,通过标准输入输出交换 JSON 消息。HTTP(Streamable HTTP)适合远程部署,server 跑在一个 URL 后面,多个客户端都能连,还支持无状态模式。本地工具型 server 默认 stdio 就够,等要多人共享或放云端了再切 HTTP。一个容易混淆的点:MCP 和模型自带的 function calling 不是一回事。function calling 是单个模型供应商的工具调用格式,MCP 是跨供应商的协议层,你的 MCP 工具最终也会被映射成各家模型的工具调用,但接入层是统一的,分清这两层能少走不少弯路,也更容易跟别人讲清楚 MCP 到底解决了什么。
接进客户端
写好的 server 要在客户端里注册。以 Claude Desktop 为例,配一段 JSON 把它挂上去:
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/path/to/weather_server.py"]
}
}
}
重启客户端后,模型就能看到并调用 get_weather 了。这一步也是 MCP 相比各家私有插件协议最大的区别:同一份 server 配置,换个支持 MCP 的客户端照样能连。
resources 与 prompts
resources 是给模型喂只读上下文用的,典型如「公司文档」「数据库记录」,客户端可以订阅并按标识读取;prompts 是把一段固定交互封装成模板。二者在最小可用阶段可以先不做,等工具多了、上下文要复用的时候再补。加 resources 时注意:资源要能被稳定地按标识读取,别让模型自己去猜 URL 或表名,那等于把脏活又丢回给模型。
容易踩的坑
第一,工具描述写太糙。模型能不能正确传参,很大程度上取决于 docstring 清不清楚,含糊的描述会直接导致传错参。第二,把敏感操作暴露成工具但没加权限控制,模型被诱导就可能执行不该执行的调用,工具内部要自己做校验,别指望协议层替你拦。第三,stdio 和 HTTP 的调试方式不一样,stdio 的日志要写到 stderr,否则会污染消息流,客户端解析直接炸。这三点是新手跑通后又回炉重造的高发原因。第四点是版本兼容:MCP 协议还在快速演进,SDK 和服务端的版本要对应,升级 SDK 前先看迁移说明,别在生产环境直接跳大版本。