112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
// Package jsonrpc implements a minimal JSON-RPC 2.0 client over a subprocess's
|
|
// stdio (US-014/#116). It is the shared transport foundation reused by the MCP
|
|
// client (#130/#131), the plugin system (#132/#133) and process-isolated
|
|
// sub-agents (#135): each spawns an external executable and speaks line-delimited
|
|
// JSON-RPC 2.0 over the child's stdin/stdout.
|
|
//
|
|
// The wire format follows the spec: every message carries "jsonrpc":"2.0". A
|
|
// request has an id and expects a matching response; a notification omits the id
|
|
// and expects none. Requests and responses are correlated by id, so concurrent
|
|
// requests from different goroutines are safe — each waits only on its own reply.
|
|
//
|
|
// This file defines the message envelope and (de)serialization; transport.go
|
|
// implements the subprocess client.
|
|
package jsonrpc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// Version is the only JSON-RPC protocol version this package speaks.
|
|
const Version = "2.0"
|
|
|
|
// ID is a JSON-RPC request identifier. The spec allows a string or a number;
|
|
// this client only ever generates numeric ids, but ID round-trips whatever a
|
|
// peer sends back so response correlation still works against servers that echo
|
|
// string ids.
|
|
type ID struct {
|
|
num int64
|
|
str string
|
|
isStr bool
|
|
}
|
|
|
|
// NumID returns a numeric request id.
|
|
func NumID(n int64) ID { return ID{num: n} }
|
|
|
|
// String renders the id for use as a map key when correlating responses.
|
|
func (id ID) String() string {
|
|
if id.isStr {
|
|
return "s:" + id.str
|
|
}
|
|
return fmt.Sprintf("n:%d", id.num)
|
|
}
|
|
|
|
// MarshalJSON emits the id as its underlying JSON scalar (number or string).
|
|
func (id ID) MarshalJSON() ([]byte, error) {
|
|
if id.isStr {
|
|
return json.Marshal(id.str)
|
|
}
|
|
return json.Marshal(id.num)
|
|
}
|
|
|
|
// UnmarshalJSON accepts either a JSON number or string id.
|
|
func (id *ID) UnmarshalJSON(data []byte) error {
|
|
var n int64
|
|
if err := json.Unmarshal(data, &n); err == nil {
|
|
id.num, id.isStr, id.str = n, false, ""
|
|
return nil
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err == nil {
|
|
id.str, id.isStr, id.num = s, true, 0
|
|
return nil
|
|
}
|
|
return fmt.Errorf("jsonrpc: id is neither number nor string: %s", data)
|
|
}
|
|
|
|
// Request is an outgoing JSON-RPC request or notification. When ID is nil the
|
|
// message is a notification (no response expected).
|
|
type Request struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID *ID `json:"id,omitempty"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
}
|
|
|
|
// Response is an incoming JSON-RPC response. Exactly one of Result / Error is
|
|
// set on a well-formed reply.
|
|
type Response struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID *ID `json:"id,omitempty"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Error *Error `json:"error,omitempty"`
|
|
}
|
|
|
|
// Error is a JSON-RPC error object.
|
|
type Error struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
}
|
|
|
|
// Error implements the error interface so a peer error can be returned directly.
|
|
func (e *Error) Error() string {
|
|
return fmt.Sprintf("jsonrpc: server error %d: %s", e.Code, e.Message)
|
|
}
|
|
|
|
// newRequest builds a request (id != nil) or notification (id == nil) with the
|
|
// given params marshaled to JSON. A nil params value is omitted from the wire
|
|
// message.
|
|
func newRequest(id *ID, method string, params any) (*Request, error) {
|
|
req := &Request{JSONRPC: Version, ID: id, Method: method}
|
|
if params != nil {
|
|
raw, err := json.Marshal(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("jsonrpc: marshal params for %q: %w", method, err)
|
|
}
|
|
req.Params = raw
|
|
}
|
|
return req, nil
|
|
}
|