这次我们直接上手,用 Go 语言从零开始,完整走一遍 MCP Server 的构建流程。之前用 Node 或 Python 的现成框架(比如 FastMCP)写过 MCP Server,框架的好处是方便,但会隐藏大量细节。而这些细节,恰恰是理解 MCP 整个调用流程的关键。所以,干脆用我最喜欢的 Go 语言,从头到尾把 MCP Server 的全过程跑一遍。看完之后,你可能会对 MCP 有一种从源头上理解的豁然开朗感。
不过,在动手之前,有两个基础概念必须先搞清楚:CS 架构和 JSON-RPC。
两个核心概念:CS 架构与 JSON-RPC
客户端-服务器架构(Client-Server Architecture,简称 CS 架构)是一个网络应用程序的计算模型,把任务或工作负载分配给服务提供者(服务器)和服务请求者(客户端)之间。
你可以想象一下日常用微信的场景:微信就是客户端,你看到的文章是由微信服务端发过来的。如果手机开了飞行模式,再刷新页面就看不到了,那是因为断开了服务端的响应。就是这么简单直接。
JSON-RPC 是一个基于 JSON 的轻量级远程过程调用协议。它让客户端能通过网络调用服务器上的方法或函数,就像调用本地函数一样简单。
所谓协议,就是大家约定俗成的规则。比如寄快递,必须填地址、姓名和手机号,快递员才能派送。JSON-RPC 也有它固定的格式,后面演示代码里会看到。
第一行代码:搭建最简 Server
先写一个最简单的 Server,功能很简单——只是把从标准输入接收到的内容打印出来。
package main
import (
"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 客户端,查看调用日志。

日志内容展开后是一个标准的 JSON-RPC 请求,包含以下字段:
"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。重新加载客户端后,发现客户端又发来了三种类型的请求:
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/listtools/listprompts/list
我们目前先处理 tools/list——也就是客户端想知道 Server 有哪些工具可用。参照官网格式,给它加上具体的方法名、参数和描述信息,最后打包成响应返回给客户端。
重新加载桌面端,会发现发送按钮下方多了一个小榔头图标,标记数量为 1,说明有一个可用的 MCP 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 sa ved"
}
},
"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": "一只可爱的猫咪在阳光下",
"destination": "'"$HOME/Downloads/test_cat_image.webp"'"
}
}
}' | ./bin/bytenote-go
这个脚本就是模拟客户端的原始请求结构,将请求发给我们的 Server 服务端。
回顾一下整个调用流程,其实大致就是这几步:
- 初始化:客户端和服务器之间的初始握手
- 工具发现:客户端请求可用工具列表
- 工具调用:客户端调用图像生成工具并获取结果
通过上面的演示就会明白:所谓的“模型上下文协议”(MCP),本质上就是由 AI 驱动的远程函数调用——不过是新瓶装旧酒罢了。
