使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

之前都是用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

长这样:

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

重启Claude Desktop客户端,查看调用日志:

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

内容展开后是一个标准的jsonrpc请求,字段分别是

  • "method"请求方法
  • "params"请求的参数
  • "jsonrpc"版本信息
  • "id"这次请求的id

{  "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,再看日志,定义完这些 我们重新加载客户端,发现客户端又给我们发送了三种类型的请求,分别是:

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

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}如通过日志我们可务端和客户端对了上。在对上了接头的暗号后,客户端就开始跟我们要三样东西,对应了三个方法:
  • resources/list
  • tools/list
  • prompts/list

我们目前先只管tools/list,也就是客户端想了解一下Server有哪些工具列表。

还是参照一下官网给的格式,给它加上具体的方法名以及方法参数和与之对应的描述信息。最后拼接成响应返回给客户端,让客户端清楚地了工具列表:

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

重新加载桌面端,会发现发送按钮下面有了一个小榔头的图标,标记数量为1,有一个可用的m p c tool。

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

点开后,发现就是我们注册的那一个,说明已经注册成功了,里面也有之前定义的完整信息。

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

然后象正常使用 MCP的那样要求“画一个大象”,客户端AI整理好信息后就按服务端的要求拼接好了参数。

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

再次查看调用日志:

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

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所以就不放具体的代码了。

使用Go从 0 开发一个 MCP Server,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服务端,具体效果长这样:

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

再次回顾一下整个的调用流程

使用Go从 0 开发一个 MCP Server,MCP 协议全过程调用的解析

其实大致就是这几步:

初始化:客户端和服务器之间的初始握手
工具发现:客户端请求可用工具列表
工具调用:客户端调用图像生成工具并获取结果

通过上面的演示就会发现:所谓的”模型上下文“MCP本质上就是由AI驱动的远程函数调用,不过是新瓶装了旧酒罢了。

前沿技术新闻资讯模型微调

蚂蚁集团:大模型推理显存优化的深度探索与实践

2026-4-13 19:56:40

前沿技术新闻资讯智能硬件

在昇腾 910B 上部署轻量级和跨平台大模型 Agent

2026-4-13 20:14:34

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
购物车
优惠劵
搜索