MCP 入门 —— (JSON-RPC 2.0 协议)

简介

JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议**

​ 在AI技术飞速演进的今天,底层通信协议的选择对系统效率和互操作性至关重要。新兴的AI协议如模型上下文协议(MCP)[1]和 Agent2Agent(A2A)[2]协议,都使用了 JSON-RPC 2.0 协议

协议

1. JSON-RPC 2.0:起源与核心规范

​ JSON-RPC协议的诞生,源于对早期RPC协议(如XML-RPC[4]、SOAP[5])复杂性的反思,旨在提供一种更轻量、更简洁的远程过程调用机制。其2.0版本规范(基于2009年草案,正式发布于2010年左右)更是将这一理念发扬光大。其核心设计哲学正如规范开篇所言:“It is designed to be simple!”

核心原则

我们先来看一下JSON-RPC协议设计的几个核心原则。

  • Stateless (无状态): 每次请求都是独立的,服务器不保存客户端状态。
  • Light-weight (轻量级): 协议开销小,消息体紧凑。
  • JSON Data Format (JSON数据格式): 使用广泛流行、易于解析和人类可读的JSON(RFC 4627[6]) 作为数据交换格式。
  • Transport Agnostic (传输无关): 协议本身不限定网络传输方式,可在HTTP、WebSocket[7]、TCP、甚至进程内等多种环境使用。

请求对象

一个 JSON-RPC 2.0 请求数据是一个单一的 JSON 对象,可以包含以下成员:

  • jsonrpc:字符串,指定 JSON-RPC 的版本号,对于 2.0 规范来说,这个值必须是 2.0。

  • method:字符串,指定要调用的远程方法的名称。

  • params:结构化值,可以是数组或者对象,传递给远程方法的参数。如果方法不需要参数可以省略。

  • id:唯一标识符,可以是字符串或数字,用于关联请求和响应,服务端必须返回相同的值。如果请求是一个通知类型,则此参数可以被省略

一个标准的 JSON-RPC 2.0 请求示例如下:

1
2
3
4
5
6
{
		"jsonrpc": "2.0",
		"method": "subtract",
		"params": [42, 23],
		"id": 1
}

响应对象

一个 JSON-RPC 2.0 响应数据也是一个单一的 JSON 对象,可以包含以下成员:

  • jsonrpc:字符串,指定 JSON-RPC 的版本号,对于 2.0 规范来说,这个值必须是2.0。
  • result:当请求成功时,包含由远程方法返回的结果。如果请求失败,则不包含此成员。
  • error:当请求失败时,包含一个错误对象。如果请求成功,则不包含此成员。
  • id:与请求中的 id 相同,用于识别哪个请求对应的响应。

错误对象包括以下成员:

  • code:整数,用于说明错误类型。JSON-RPC 2.0 定义了一组标准的错误码。
  • message:字符串,提供关于错误的简短描述。
  • data:可选,可以包含额外的错误信息,比如堆栈信息等。

一个成功的响应示例如下:

1
2
3
4
5
{
		"jsonrpc": "2.0",
		"result": 19,
		"id": 1
}

而一个异常响应示例如下:

1
2
3
4
5
6
7
8
{
		"jsonrpc": "2.0",
		"error": {
				"code": -32601,
				"message": "Method not found"
		},
		"id": 1
}

通知

​ 通知是一种特殊类型的请求,没有 id 成员,因此不会得到响应。这样客户端可以向服务器发送事件或命令而无需等待回复。一个通知示例如下:

1
2
3
4
5
{
		"jsonrpc": "2.0",
		"method": "updateStatus",
		"params": ["online"]
}

批量请求

​ JSON-RPC 2.0 支持批量请求,即可以在单个请求中发送多个 JSON-RPC 调用。每个调用都是独立的 JSON-RPC 请求对象,被放在一个数组中。服务器处理这些请求后返回一个数组,其中每个元素对应于一个调用。需要注意的是,如果其中一个调用是通知,则不会有对应的响应项。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[
  {
    "jsonrpc": "2.0",
    "method": "sum",
    "params": [1, 3, 5, 7, 9],
    "id": "1"
  },
  {
    "jsonrpc": "2.0",
    "method": "notify_hello",
    "params": ["Alice"]
  },
  {
    "jsonrpc": "2.0",
    "method": "subtract",
    "params": [42, 23],
    "id": "2"
  }
]

错误码

​ JSON-RPC 2.0 规范中定义了标准的错误码,但开发者也可以根据自己的业务逻辑添加自定义的错误码。这些自定义错误代码应该在-32000到-32099之间,以避免与标准错误码冲突。通过使用自定义错误码,可以为客户端提供更加具体的错误信息。

​ 假如这样一个场景,应用程序需要处理用户认证失败的情况,可以定义一个特定的错误码,比如 -32001,并为这个错误码配一个说明,如 Authentication failed。示例如下:

1
2
3
4
5
6
7
8
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Authentication failed"
  },
  "id": "1"
}

此外,还可以在 error 的 data 参数中包含更多的错误细节,比如错误发生的具体位置或建议的解决方法。

JSON-RPC 2.0 定义的标准错误码如下(自定义错误吗不要使用如下几个):

  • 32700: 解析错误,服务器收到无效的 JSON。

  • 32600: 无效请求,发送的 JSON 不是有效的请求对象。

  • 32601: 方法未找到,方法不存在或无效。

  • 32602: 无效参数,提供的参数无效。

  • 32603: 内部错误,JSON-RPC 内部错误。

2.GO实现案例——基于 http

client 端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
	"context"
	"fmt"

	"github.com/ybbus/jsonrpc/v3"
)

type Person struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	fmt.Println("Starting JSON-RPC client...")
	rpcClient := jsonrpc.NewClient("http://localhost:8080/rpc")

	res, err := rpcClient.Call(context.Background(), "echo", "JSON-RPC!")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Response:{jsonrpc:", res.JSONRPC, ", id:", res.ID, ", result:", res.Result, ", error:", res.Error, "}")

	fmt.Println("==========================")

	response, err := rpcClient.CallBatchRaw(context.Background(), []*jsonrpc.RPCRequest{
		{
			Method:  "echo",
			ID:      1,
			Params:  []interface{}{"JSON-RPC1!"},
			JSONRPC: "2.0",
		},
		{
			Method:  "echo",
			ID:      2,
			Params:  []interface{}{"JSON-RPC2!"},
			JSONRPC: "2.0",
		},
		{
			Method:  "echo",
			ID:      3,
			Params:  []interface{}{"JSON-RPC3!"},
			JSONRPC: "2.0",
		},
	})
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	for _, res := range response {
		fmt.Println("Response:{jsonrpc:", res.JSONRPC, ", id:", res.ID, ", result:", res.Result, ", error:", res.Error, "}")
	}
}

server 端

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"
)

// JSON-RPC 请求结构
type JSONRPCRequest struct {
	JSONRPC string          `json:"jsonrpc"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params"`
	ID      interface{}     `json:"id"`
}

// JSON-RPC 响应结构
type JSONRPCResponse struct {
	JSONRPC string      `json:"jsonrpc"`
	Result  interface{} `json:"result,omitempty"`
	Error   *RPCError   `json:"error,omitempty"`
	ID      interface{} `json:"id"`
}

// 错误结构
type RPCError struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"` // 添加可选的错误数据字段
}

// 标准错误代码常量
const (
	ParseErrorCode     = -32700
	InvalidRequestCode = -32600
	MethodNotFoundCode = -32601
	InvalidParamsCode  = -32602
	InternalErrorCode  = -32603
)

// 处理 JSON-RPC 方法
func handleRPCMethod(method string, params json.RawMessage) (interface{}, *RPCError) {
	switch method {
	case "add":
		var numbers []int
		if err := json.Unmarshal(params, &numbers); err != nil {
			return nil, &RPCError{Code: InvalidParamsCode, Message: "Invalid params"}
		}
		if len(numbers) != 2 {
			return nil, &RPCError{Code: InvalidParamsCode, Message: "Expected two parameters"}
		}
		return numbers[0] + numbers[1], nil
	case "echo":
		var message []string
		if err := json.Unmarshal(params, &message); err != nil {
			return nil, &RPCError{Code: InvalidParamsCode, Message: "Invalid params"}
		}

		if len(message) == 0 {
			return nil, &RPCError{Code: InvalidParamsCode, Message: "Expected at least one parameter"}
		}
		return fmt.Sprintf("hello %v", message[0]), nil
	case "time":
		// 添加一个新方法,返回当前服务器时间
		return time.Now().Format(time.RFC3339), nil
	default:
		return nil, &RPCError{Code: MethodNotFoundCode, Message: "Method not found"}
	}
}

// 处理单个JSON-RPC请求
func processRequest(req JSONRPCRequest) JSONRPCResponse {
	// 日志记录请求
	log.Printf("Processing request: method=%s, id=%v", req.Method, req.ID)

	if req.JSONRPC != "2.0" {
		return JSONRPCResponse{
			JSONRPC: "2.0",
			Error:   &RPCError{Code: InvalidRequestCode, Message: "Invalid Request: expected jsonrpc=2.0"},
			ID:      req.ID,
		}
	}

	result, rpcError := handleRPCMethod(req.Method, req.Params)
	// 日志记录响应
	if rpcError != nil {
		log.Printf("Request error: method=%s, id=%v, error=%s", req.Method, req.ID, rpcError.Message)
	}

	return JSONRPCResponse{
		JSONRPC: "2.0",
		Result:  result,
		Error:   rpcError,
		ID:      req.ID,
	}
}

// 请求日志中间件
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf("Received %s request from %s", r.Method, r.RemoteAddr)
		next(w, r)
		log.Printf("Completed in %v", time.Since(start))
	}
}

// JSON-RPC 请求处理函数
func rpcHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	if r.Method != http.MethodPost {
		log.Printf("Invalid HTTP method: %s", r.Method)
		http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
		return
	}

	// 读取请求体
	body, err := io.ReadAll(r.Body)
	if err != nil {
		log.Printf("Error reading request body: %v", err)
		resp := JSONRPCResponse{
			JSONRPC: "2.0",
			Error:   &RPCError{Code: ParseErrorCode, Message: "Parse error: could not read request body"},
			ID:      nil,
		}
		json.NewEncoder(w).Encode(resp)
		return
	}

	// 请求体为空检查
	if len(body) == 0 {
		log.Print("Empty request body")
		resp := JSONRPCResponse{
			JSONRPC: "2.0",
			Error:   &RPCError{Code: InvalidRequestCode, Message: "Invalid Request: empty body"},
			ID:      nil,
		}
		json.NewEncoder(w).Encode(resp)
		return
	}

	// 尝试解析为批量请求
	var batchReq []JSONRPCRequest
	if err := json.Unmarshal(body, &batchReq); err == nil && len(batchReq) > 0 {
		log.Printf("Processing batch request with %d items", len(batchReq))
		// 批量请求处理
		responses := make([]JSONRPCResponse, len(batchReq))
		for i, req := range batchReq {
			responses[i] = processRequest(req)
		}
		json.NewEncoder(w).Encode(responses)
		return
	}

	// 尝试解析为单个请求
	var singleReq JSONRPCRequest
	if err := json.Unmarshal(body, &singleReq); err != nil {
		log.Printf("JSON parse error: %v", err)
		resp := JSONRPCResponse{
			JSONRPC: "2.0",
			Error:   &RPCError{Code: ParseErrorCode, Message: "Parse error: invalid JSON"},
			ID:      nil,
		}
		json.NewEncoder(w).Encode(resp)
		return
	}

	// 处理单个请求
	log.Print("Processing single request")
	resp := processRequest(singleReq)
	json.NewEncoder(w).Encode(resp)
}

func main() {
	// 添加日志配置
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
	log.SetPrefix("JSON-RPC: ")

	// 使用中间件包装处理函数
	http.HandleFunc("/rpc", loggingMiddleware(rpcHandler))

	log.Println("JSON-RPC 服务端已启动,地址:http://localhost:8080/rpc")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatalf("服务器启动失败: %v", err)
	}
}
1
2
3
4
5
6
7
8
outpu:

Starting JSON-RPC client...
Response:{jsonrpc: 2.0 , id: 0 , result: hello JSON-RPC! , error: <nil> }
==========================
Response:{jsonrpc: 2.0 , id: 1 , result: hello JSON-RPC1! , error: <nil> }
Response:{jsonrpc: 2.0 , id: 2 , result: hello JSON-RPC2! , error: <nil> }
Response:{jsonrpc: 2.0 , id: 3 , result: hello JSON-RPC3! , error: <nil> }

3.使用场景

​ JSON-RPC 2.0 的使用场景包括但不限于如下几个:

  • Web 应用程序,客户端与服务器之间的异步通信,例如浏览器与后端服务交互。
  • 微服务之间的通信,通过 JSON-RPC 调用其他微服务的接口。
  • 物联网设备,设备与服务器之间的通信,由于 JSON-RPC 的轻量级特性,非常适合资源受限的设备。
  • 移动应用,移动客户端与服务器之间的交互,减少数据传输量,提高响应速度。
  • 区块链和加密货币,节点之间的通信或客户端与节点的交互,许多区块链系统(如以太坊)使用 JSON-RPC 进行接口调用。
  • 远程过程调用(RPC)服务,替代传统的 SOAP 或 XML-RPC,提供简单的接口调用机制。

文档和版本控制

​ 良好的文档对于任何 API 都至关重要,尤其是像 JSON-RPC 2.0 这样依赖于明确的请求和响应格式的协议。

  • 编写清晰的 API 文档:详细描述每个方法的用途、参数、返回值和可能的错误情况。可以使用工具如 Swagger 或 Postman 来生成交互式的 API 文档。
  • 保持文档更新:随着 API 的发展,确保文档始终保持最新状态,反映最新的变更和改进。
  • 版本控制:为 API 引入版本控制,以便在不影响现有用户的情况下进行更新。可以在 URL 中或通过请求头指定 API 版本。

社区和支持

​ JSON-RPC 2.0 是一个开放的标准,拥有活跃的社区和丰富的资源。参与社区讨论、阅读官方文档和技术博客、关注相关论坛和社交媒体,都可以帮助更快地解决问题,并获取最新的最佳实践。

​ 此外,很多流行的编程语言和框架都有现成的 JSON-RPC 库,可以大大简化开发过程。选择一个成熟且维护良好的库,不仅可以节省时间,还能减少出错的可能性。

4.小结

​ JSON-RPC 2.0作为一种轻量级的 RPC 协议,提供了如标准化的错误处理、批量请求支持和通知机制等功能,具有简单、易用、跨语言等优点,适用于多种分布式系统场景。

参考文章