The jsonrpc package exposes part of a JSON-RPC v2 implementation for MCP transport authors. It provides low-level message encoding/decoding and types for implementing custom transports.
import "github.com/modelcontextprotocol/go-sdk/jsonrpc"type Message = jsonrpc2.MessageA JSON-RPC message (request, response, or notification).
type Request = jsonrpc2.RequestA JSON-RPC request message.
type Response = jsonrpc2.ResponseA JSON-RPC response message.
type ID = jsonrpc2.IDA JSON-RPC request identifier (can be string, number, or null).
func EncodeMessage(msg Message) ([]byte, error)Serializes a JSON-RPC message to wire format (JSON bytes).
Parameters:
msg: Message to encode (Request, Response, or Notification)Returns:
[]byte: JSON-encoded messageerror: Encoding errorExample:
// Create a request
req := jsonrpc2.Request{
ID: jsonrpc2.NewStringID("1"),
Method: "tools/list",
Params: json.RawMessage(`{}`),
}
// Encode to JSON
data, err := jsonrpc.EncodeMessage(req)
if err != nil {
log.Fatal(err)
}
// data contains: {"jsonrpc":"2.0","id":"1","method":"tools/list","params":{}}func DecodeMessage(data []byte) (Message, error)Deserializes JSON-RPC wire format data into a Message.
Parameters:
data: JSON-encoded message bytesReturns:
Message: Decoded message (Request, Response, or Notification)error: Decoding errorExample:
data := []byte(`{"jsonrpc":"2.0","id":"1","method":"tools/list","params":{}}`)
msg, err := jsonrpc.DecodeMessage(data)
if err != nil {
log.Fatal(err)
}
switch m := msg.(type) {
case *jsonrpc2.Request:
fmt.Printf("Request: %s\n", m.Method)
case *jsonrpc2.Response:
fmt.Printf("Response ID: %v\n", m.ID)
}func MakeID(v any) (ID, error)Coerces a Go value to a JSON-RPC ID.
Parameters:
v: Value to convert (should be nil, float64, or string)Returns:
ID: JSON-RPC identifiererror: Conversion error if value is invalid typeExample:
// String ID
id1, _ := jsonrpc.MakeID("request-123")
// Numeric ID
id2, _ := jsonrpc.MakeID(float64(42))
// Null ID (for notifications)
id3, _ := jsonrpc.MakeID(nil)The jsonrpc package is primarily used when implementing custom transports. Here's a complete example:
package main
import (
"context"
"encoding/json"
"net"
"sync"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// Custom transport over TCP
type TCPTransport struct {
address string
}
func (t *TCPTransport) Connect(ctx context.Context) (mcp.Connection, error) {
conn, err := net.Dial("tcp", t.address)
if err != nil {
return nil, err
}
return &tcpConnection{
conn: conn,
decoder: json.NewDecoder(conn),
encoder: json.NewEncoder(conn),
}, nil
}
// Connection implementation
type tcpConnection struct {
conn net.Conn
decoder *json.Decoder
encoder *json.Encoder
mu sync.Mutex
counter int
}
func (c *tcpConnection) Read(ctx context.Context) (jsonrpc.Message, error) {
var raw json.RawMessage
if err := c.decoder.Decode(&raw); err != nil {
return nil, err
}
return jsonrpc.DecodeMessage(raw)
}
func (c *tcpConnection) Write(ctx context.Context, msg jsonrpc.Message) error {
c.mu.Lock()
defer c.mu.Unlock()
data, err := jsonrpc.EncodeMessage(msg)
if err != nil {
return err
}
return c.encoder.Encode(json.RawMessage(data))
}
func (c *tcpConnection) Close() error {
return c.conn.Close()
}
func (c *tcpConnection) SessionID() string {
return c.conn.RemoteAddr().String()
}import (
"context"
"github.com/gorilla/websocket"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type WebSocketTransport struct {
url string
}
func (t *WebSocketTransport) Connect(ctx context.Context) (mcp.Connection, error) {
conn, _, err := websocket.DefaultDialer.DialContext(ctx, t.url, nil)
if err != nil {
return nil, err
}
return &wsConnection{conn: conn}, nil
}
type wsConnection struct {
conn *websocket.Conn
mu sync.Mutex
}
func (c *wsConnection) Read(ctx context.Context) (jsonrpc.Message, error) {
_, data, err := c.conn.ReadMessage()
if err != nil {
return nil, err
}
return jsonrpc.DecodeMessage(data)
}
func (c *wsConnection) Write(ctx context.Context, msg jsonrpc.Message) error {
c.mu.Lock()
defer c.mu.Unlock()
data, err := jsonrpc.EncodeMessage(msg)
if err != nil {
return err
}
return c.conn.WriteMessage(websocket.TextMessage, data)
}
func (c *wsConnection) Close() error {
return c.conn.Close()
}
func (c *wsConnection) SessionID() string {
return c.conn.RemoteAddr().String()
}import (
"bufio"
"context"
"encoding/binary"
"io"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type FramedTransport struct {
conn io.ReadWriteCloser
}
func (t *FramedTransport) Connect(ctx context.Context) (mcp.Connection, error) {
return &framedConnection{
reader: bufio.NewReader(t.conn),
writer: bufio.NewWriter(t.conn),
closer: t.conn,
}, nil
}
type framedConnection struct {
reader *bufio.Reader
writer *bufio.Writer
closer io.Closer
mu sync.Mutex
}
func (c *framedConnection) Read(ctx context.Context) (jsonrpc.Message, error) {
// Read 4-byte length prefix
var length uint32
if err := binary.Read(c.reader, binary.BigEndian, &length); err != nil {
return nil, err
}
// Read message data
data := make([]byte, length)
if _, err := io.ReadFull(c.reader, data); err != nil {
return nil, err
}
return jsonrpc.DecodeMessage(data)
}
func (c *framedConnection) Write(ctx context.Context, msg jsonrpc.Message) error {
c.mu.Lock()
defer c.mu.Unlock()
// Encode message
data, err := jsonrpc.EncodeMessage(msg)
if err != nil {
return err
}
// Write length prefix
length := uint32(len(data))
if err := binary.Write(c.writer, binary.BigEndian, length); err != nil {
return err
}
// Write message data
if _, err := c.writer.Write(data); err != nil {
return err
}
return c.writer.Flush()
}
func (c *framedConnection) Close() error {
return c.closer.Close()
}
func (c *framedConnection) SessionID() string {
return "framed-session"
}func inspectMessage(data []byte) error {
msg, err := jsonrpc.DecodeMessage(data)
if err != nil {
return err
}
switch m := msg.(type) {
case *jsonrpc2.Request:
fmt.Printf("Request ID: %v\n", m.ID)
fmt.Printf("Method: %s\n", m.Method)
fmt.Printf("Params: %s\n", string(m.Params))
case *jsonrpc2.Response:
fmt.Printf("Response ID: %v\n", m.ID)
if m.Error != nil {
fmt.Printf("Error: %v\n", m.Error)
} else {
fmt.Printf("Result: %s\n", string(m.Result))
}
}
return nil
}type LoggingConnection struct {
conn mcp.Connection
logger *log.Logger
}
func (c *LoggingConnection) Read(ctx context.Context) (jsonrpc.Message, error) {
msg, err := c.conn.Read(ctx)
if err != nil {
c.logger.Printf("Read error: %v", err)
return nil, err
}
// Log the message
data, _ := jsonrpc.EncodeMessage(msg)
c.logger.Printf("→ %s", string(data))
return msg, nil
}
func (c *LoggingConnection) Write(ctx context.Context, msg jsonrpc.Message) error {
// Log the message
data, _ := jsonrpc.EncodeMessage(msg)
c.logger.Printf("← %s", string(data))
return c.conn.Write(ctx, msg)
}
func (c *LoggingConnection) Close() error {
return c.conn.Close()
}
func (c *LoggingConnection) SessionID() string {
return c.conn.SessionID()
}type MessageRouter struct {
handlers map[string]func(jsonrpc.Message) (jsonrpc.Message, error)
}
func NewMessageRouter() *MessageRouter {
return &MessageRouter{
handlers: make(map[string]func(jsonrpc.Message) (jsonrpc.Message, error)),
}
}
func (r *MessageRouter) Handle(method string, handler func(jsonrpc.Message) (jsonrpc.Message, error)) {
r.handlers[method] = handler
}
func (r *MessageRouter) Route(msg jsonrpc.Message) (jsonrpc.Message, error) {
req, ok := msg.(*jsonrpc2.Request)
if !ok {
return nil, fmt.Errorf("not a request")
}
handler, ok := r.handlers[req.Method]
if !ok {
return nil, fmt.Errorf("unknown method: %s", req.Method)
}
return handler(msg)
}// JSON-RPC error codes
const (
ParseError = -32700
InvalidRequest = -32600
MethodNotFound = -32601
InvalidParams = -32602
InternalError = -32603
)
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
func (e *JSONRPCError) Error() string {
return fmt.Sprintf("JSON-RPC error %d: %s", e.Code, e.Message)
}
// Create error response
func errorResponse(id jsonrpc.ID, code int, message string) jsonrpc.Response {
return jsonrpc2.Response{
ID: id,
Error: &jsonrpc2.Error{
Code: code,
Message: message,
},
}
}package main
import (
"context"
"log"
"net"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
// Server: Listen on TCP
listener, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatal(err)
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Accept error: %v", err)
continue
}
// Handle each connection
go handleConnection(conn)
}
}()
// Client: Connect via custom TCP transport
client := mcp.NewClient(
&mcp.Implementation{Name: "tcp-client", Version: "1.0.0"},
nil,
)
transport := &TCPTransport{address: "localhost:9000"}
session, err := client.Connect(context.Background(), transport, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Use session normally
tools, err := session.ListTools(context.Background(), &mcp.ListToolsParams{})
if err != nil {
log.Fatal(err)
}
log.Printf("Found %d tools", len(tools.Tools))
}
func handleConnection(conn net.Conn) {
defer conn.Close()
server := mcp.NewServer(
&mcp.Implementation{Name: "tcp-server", Version: "1.0.0"},
nil,
)
transport := &TCPTransport{conn: conn}
if err := server.Run(context.Background(), transport); err != nil {
log.Printf("Server error: %v", err)
}
}