前阵子 Agent 这词满天飞,配套总提一个词:MCP。我本职是写 PHP 的,一开始懒得看文档,后来实在绕不开,干脆自己用 Go 写一个小 server、再用 Python 写一份做对比,才真正弄懂这玩意儿到底解决了啥。
说白了,MCP 就是给大模型接外部工具的"标准插头"。以前你想让 AI 查数据库、读文件,得自己写一堆胶水代码;MCP 把"工具怎么描述、怎么调用"这套东西统一了,谁都能照着协议挂一个。官方常把它比喻成"AI 的 USB-C 口"——插上了,模型就能用你的数据和能力。
据 2026 年官方文档,MCP 当前协议版本是 2025-11-25,所有消息都基于 JSON-RPC 2.0。它定义了工具(tools)、资源(resources)、提示词(prompts)、采样(sampling)等几类能力。客户端和服务器之间靠两种传输方式通信:一种是 stdio(标准输入输出),客户端把 server 当子进程拉起来,JSON-RPC 消息走 stdin/stdout,按行分隔、不能夹换行;另一种是 HTTP + SSE。文档里还特意提醒:stdio 模式下 server 往 stdout 写的每条消息都必须是合法 MCP 报文,不能顺手塞点调试日志进去,不然客户端就解析崩了。握手阶段客户端先发 initialize、再拉 tools/list,调用时走 tools/call,这套生命周期是固定的。
我们后端是 Go 和 PHP 混着用,我就用 Go 起了一个进程,走默认的 stdio 传输,暴露一个工具给客户端调用。这里用的是社区里较常用的 mark3labs/mcp-go 这个库(go get github.com/mark3labs/mcp-go),它把 JSON-RPC 和生命周期管理都封装好了,我只需关心业务逻辑。
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shirou/gopsutil/v3/load" "github.com/shirou/gopsutil/v3/mem" // 起一个 MCP server,名字和版本随便填 s := server.NewMCPServer("server-watch", "1.0.0", server.WithToolCapabilities(true)) // 定义一个工具,description 是模型判断"要不要调你"的依据 tool := mcp.NewTool("server_load", mcp.WithDescription("当有人问线上机器卡不卡、压力大不大时调用,返回当前 CPU 负载和内存占用"), s.AddTool(tool, serverLoadHandler) // 走 stdio:客户端拉起本进程,通过 stdin/stdout 通信 if err := server.ServeStdio(s); err != nil { fmt.Printf("server error: %v\n", err)func serverLoadHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { cpu, _ := load.Avg() // 1 分钟平均负载 ram, _ := mem.VirtualMemory() // 内存占用百分比 text := fmt.Sprintf("CPU 1分钟负载=%.2f,内存已用=%.1f%%", cpu.Load1, ram.UsedPercent) return mcp.NewToolResultText(text), nil编译出来是个单文件二进制,丢给支持 MCP 的客户端(比如 Claude Desktop、Cursor)配一下启动命令就能用。Go 的 goroutine 模型对并发工具调用也友好,单进程扛多个会话压力不大。
Python 这边我用了 FastMCP。这个库挺有意思:它 1.0 版在 2024 年被并进了官方 MCP Python SDK,现在的 2.x 由 Prefect 团队维护,是 Python 里搭 MCP server 的常见选择之一。写法比 Go 更轻——一个装饰器就搞定。
from fastmcp import FastMCPmcp = FastMCP("server-watch")def server_load() -> str: """当有人问线上机器卡不卡、压力大不大时调用,返回当前 CPU 负载和内存占用。""" cpu = psutil.getloadavg()[0] ram = psutil.virtual_memory().percent return f"CPU 1分钟负载={cpu:.2f},内存已用={ram:.1f}%"if __name__ == "__main__":两种写法对比很明显:Go 胜在单文件部署、类型安全、生产环境稳;Python 胜在几行就能跑通,适合快速验证想法。对我们这种 PHP 栈的小团队,挂个内部工具成本一下低了——哪天真要上线,把 Python 原型照着 Go 版重写一遍就行。
在支持 MCP 的客户端里填上这个 server 的启动命令(比如 go run main.go 或 python server.py),客户端启动后自动发起 initialize 握手、拉到 server_load 这个工具列表。之后我问"线上机器现在压力大吗",客户端把问题加工具列表发给模型,模型决定调 server_load,拿到数值再组织成回答。整个过程我一行胶水代码都没写。
踩过挺坑的一处是工具描述。description 这一句不是写给人看的,是写给模型看的——它全靠这段描述决定"什么时候该调你"。我初版只写了句"获取负载",结果它经常乱调;改成"当有人问服务器卡不卡、压力大不大时调用",命中率立马高了不少。描述里尽量带上触发场景,比光写功能名管用。
- stdio 模式下别往 stdout 乱写日志,需要调试就写到 stderr,否则客户端解析会挂。
- 工具返回的字段挑关键的回,别把一整坨系统信息塞进去,模型消化不了还费 token。
- 同一份工具逻辑,Go 和 Python 都能挂,选你团队顺手的语言就行,协议本身是语言无关的。
以前接一个新数据源,我得给每个 AI 客户端单独适配;现在按 MCP 写一次,谁支持谁就能用。对我们这种 PHP 栈的小团队,挂个内部工具成本一下低了。它没那么玄,就是个把"模型"和"我的系统"连起来的标准接口。
你用过 MCP 吗?拿 Go 还是 Python 写的,评论区聊聊你踩过的坑。