支持私有化部署
AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


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

发布日期:2025-03-20 13:14:01 浏览次数: 1989 作者:字节笔记本
推荐语

深入探索MCP协议,从零开始用Go语言构建MCP Server,理解CS架构和JSON-RPC协议。

核心内容:
1. 为何选择Go语言从零构建MCP Server
2. CS架构和JSON-RPC协议的基本概念解析
3. 编写MCP Server的第一步:创建简单Server并测试

杨芳贤
53A创始人/腾讯云(TVP)最具价值专家

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

长这样:

e8f23b4b-3524-48f4-b66c-0bf553f65675.png

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

399b66ce-4757-4473-8ad9-48c0889c4067.png

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

55e7e61a-2d11-447f-8124-b00196f0f8ab.png

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有哪些工具列表。

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

ef873069-7b24-4975-b849-bdfd50633927.png

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

93cd9806-b558-45cf-87aa-1bcb4c5d44c4.png

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

335f1d91-29ef-4a3d-80c7-4a6d2a44071f.png

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

628212f5-8c2d-4b37-a130-eb2beed68a34.png

再次查看调用日志:

deaa4980-04be-4e0d-985c-752c680e6c1e.png

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

b7554d69-c640-4704-a36b-99d2406cec88.png

搞完这些其实整个的流程就通了,我们现在可以来模拟一下客户端的请求做一下测试,脚本如下:

# 测试图像生成请求
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服务端,具体效果长这样:

Mar-20-2025 09-13-29.gif

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

ba321104-e222-4d78-b8f3-6e9e38898f21.png

其实大致就是这几步:

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

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


53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询