微信扫码
添加专属顾问
我要投稿
深入探索MCP协议,从零开始用Go语言构建MCP Server,理解CS架构和JSON-RPC协议。 核心内容: 1. 为何选择Go语言从零构建MCP Server 2. CS架构和JSON-RPC协议的基本概念解析 3. 编写MCP Server的第一步:创建简单Server并测试
之前都是用node或者python现成的框架比如fastmcp 来写mcpserver,使用框架的好处是方便,但是会隐藏掉大量的细节。
细节的缺失会影响我们对于MCP整个调用流程的理解,所以今天就直接使用我最喜欢的Go语言从0开始完完整整地把MCP Server的全过程走一遍,相信会看完之后会对MCP有一种从源上理解的豁然开朗的感觉。
不过在这里之前我们还需要了解一下最基础的两个概念,CS架构和 JSON-RPC
客户端-服务器架构(Client-Server Architecture,简称CS架构)是一种网络应用程序的计算模型,将任务或工作负载分配到服务提供者(服务器)和服务请求者(客户端)之间。
客户端-服务器架构其实就是你现在使用微信的方式,微信就是客户端,你看到的文章就是由微信服务端发过来的。如果你手机开了飞行模型,再次刷新页面就看不到了,那是因为断开了服务端的响应。
JSON-RPC是一种基于JSON的轻量级远程过程调用(Remote Procedure Call)协议。它允许客户端通过网络调用服务器上的方法或函数,就像调用本地函数一样简单。
所谓的协议就是大家约定俗成的规则,比如快递信息,要寄快递必须填地址姓名和手机号,这样才能指派到具体的人派发邮件到你。JSON-RPC也有自己的固定格式(下面的演示代码会看到)。
第一行代码,我们先写一个最简单的Server,内容很简单,只是把接受的标准流也就是输入内容给打印出来。
package mainimport ( "encoding/json""log""os")func main() { decoder := json.NewDecoder(os.Stdin) // 设置日志输出到stderr log.SetOutput(os.Stderr) log.SetFlags(log.LstdFlags | log.Lmicroseconds) log.Printf("Starting minimal MCP server ...") var req map[string]interface{} if err := decoder.Decode(&req); err != nil { log.Printf("Error decoding request: %v", err) return } log.Printf("Received request: %v", req) }
然后在Claude Desktop中配置好这个最简的Server,地址在:
/Users/user/Library/Application Support/Claude/claude_desktop_config.json
长这样:
重启Claude Desktop客户端,查看调用日志:
内容展开后是一个标准的jsonrpc请求,字段分别是
{ "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "claude-ai", "version": "0.1.0" } }, "jsonrpc": "2.0", "id": 0 }
通过日志我们可以看到,MCP协议的第一步是客户端发起的,向我们写的服务端发起了请求:一次初始化握手请求。
请求内容里面是一个标识为"initialize"的方法请求。那么我们就参照官网的格式标准试着回复一下吧。
method, hasMethod := req["method"].(string) id, hasId := req["id"] var response map[string]interface{} if hasMethod && hasId && method == "initialize" { response = map[string]interface{}{ "jsonrpc": "2.0", "id":id, "result": map[string]interface{}{ "protocolVersion": "2024-11-05", "serverInfo": map[string]interface{}{ "name":"mcp-go", "version": "0.0.0", }, "capabilities": map[string]interface{}{ "tools": map[string]interface{}{}, }, }, } log.Printf("Sending response: %v", response) err := encoder.Encode(response) if err != nil { log.Printf("Error encoding response: %v", err) } }
回复的内容就是在capabilities
里面定义一个暂时为空的tools,再看日志,定义完这些 我们重新加载客户端,发现客户端又给我们发送了三种类型的请求,分别是:
2025-03-20T00:12:46.661Z [bytenote-go] [info] Message from server: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"mcp-go-01","version":"0.1.0"}}} 2025-03-20T00:12:46.662Z [bytenote-go] [info] Message from client: {"method":"notifications/initialized","jsonrpc":"2.0"} 2025-03-20T00:12:46.669Z [bytenote-go] [info] Message from client: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":1} 2025-03-20T00:12:46.669Z [bytenote-go] [info] Message from client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":2}如通过日志我们可务端和客户端对了上。在对上了接头的暗号后,客户端就开始跟我们要三样东西,对应了三个方法:
我们目前先只管tools/list
,也就是客户端想了解一下Server有哪些工具列表。
还是参照一下官网给的格式,给它加上具体的方法名以及方法参数和与之对应的描述信息。最后拼接成响应返回给客户端,让客户端清楚地了工具列表:
重新加载桌面端,会发现发送按钮下面有了一个小榔头的图标,标记数量为1,有一个可用的m p c tool。
点开后,发现就是我们注册的那一个,说明已经注册成功了,里面也有之前定义的完整信息。
然后象正常使用 MCP的那样要求“画一个大象”,客户端AI整理好信息后就按服务端的要求拼接好了参数。
再次查看调用日志:
2025-03-20T00:54:15.397Z [bytenote-go] [info] Message from client: {"method":"tools/call","params":{"name":"generate-image","arguments":{"prompt":"A majestic elephant standing in a natural grassland habitat, with its trunk raised slightly. Detailed texture showing the wrinkled skin. Realistic lighting with soft shadows.","destination":"elephant_image.png"}},"jsonrpc":"2.0","id":67}
这回,客户端又给我们的服务发来了一个叫tools/call
请求,原来这个才是真正的调用,那就实现一下吧,还是老三样:方法名,方法参数,描述。
不同的是这次是取客户端传递过来的参数,当然方法的最后还得根据原来的请求ID正常原路返回回去:
// 添加到main.go的switch语句中 case "tools/list": toolSchema := json.RawMessage(`{ "type": "object", "properties": { "prompt": { "type": "string", "description": "Description of the image to generate" }, "destination": { "type": "string", "description": "Path where the generated image should be saved" } }, "required": ["prompt", "destination"] }`) response = JSONRPCResponse{ JSONRPC: "2.0", ID:request.ID, Result: ListToolsResult{ Tools: []Tool{ { Name:"generate-image", Description: "Generate an image using a text prompt", InputSchema: toolSchema, }, }, }, }
接下来就是Tool里面具体做的内容了,本次演示我们请求一下siliconflow图像生成的接口,因为主要讲MCP所以就不放具体的代码了。
搞完这些其实整个的流程就通了,我们现在可以来模拟一下客户端的请求做一下测试,脚本如下:
# 测试图像生成请求 echo -e "\n发送图像生成请求..." echo '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "generate-image", "arguments": { "prompt": "一只可爱的猫咪在阳光下", "width": 1024, "height": 1024, "guidance_scale": 7.5, "steps": 20, "negative_prompt": "模糊,扭曲,低质量", "destination": "'"$HOME/Downloads/test_cat_image.webp"'" } } }' | ./bin/bytenote-go
这个脚本就是把客户端最原始的请求结构模拟给到我们的Server服务端,具体效果长这样:
再次回顾一下整个的调用流程
其实大致就是这几步:
初始化:客户端和服务器之间的初始握手
工具发现:客户端请求可用工具列表
工具调用:客户端调用图像生成工具并获取结果
通过上面的演示就会发现:所谓的”模型上下文“MCP本质上就是由AI驱动的远程函数调用,不过是新瓶装了旧酒罢了。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-04-15
谷歌推出AI编程Firebase Studio,基于浏览器,云端构建全栈应用,无需本地
2025-04-15
专访Answer.AI创始人周立:AI时代,学什么在未来是有用的?
2025-04-14
Atypica.AI,第一个高完成度用户洞察 agent
2025-04-14
高效AI开发指南:上下文管理全解,以Cline为例
2025-04-14
使用Inspector调试MCP服务
2025-04-14
通过抓取数百个新闻来源,利用人工智能分析新闻,并提供简洁、个性化的每日简报,帮助用户从新闻噪音中筛选出有价值的信息。
2025-04-14
即梦AI字体我有点玩明白了,用这套Prompt提效50%
2025-04-14
用这个方案,一键模仿70%的海报和投流素材图,而且几乎0成本
2025-03-06
2024-09-04
2025-01-25
2024-09-26
2024-10-30
2024-09-03
2024-12-11
2024-12-25
2024-10-30
2025-02-18