or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

auth.mdindex.mdjsonrpc.mdmcp-capabilities.mdmcp-client.mdmcp-content.mdmcp-protocol.mdmcp-server.mdmcp-transports.mdoauthex.md
tile.json

jsonrpc.mddocs/

JSON-RPC

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 Path

import "github.com/modelcontextprotocol/go-sdk/jsonrpc"

Message Types

Message Interface

type Message = jsonrpc2.Message

A JSON-RPC message (request, response, or notification).

Request

type Request = jsonrpc2.Request

A JSON-RPC request message.

Response

type Response = jsonrpc2.Response

A JSON-RPC response message.

Request ID

type ID = jsonrpc2.ID

A JSON-RPC request identifier (can be string, number, or null).

Functions

Encoding Messages

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 message
  • error: Encoding error

Example:

// 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":{}}

Decoding Messages

func DecodeMessage(data []byte) (Message, error)

Deserializes JSON-RPC wire format data into a Message.

Parameters:

  • data: JSON-encoded message bytes

Returns:

  • Message: Decoded message (Request, Response, or Notification)
  • error: Decoding error

Example:

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)
}

Creating Request IDs

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 identifier
  • error: Conversion error if value is invalid type

Example:

// String ID
id1, _ := jsonrpc.MakeID("request-123")

// Numeric ID
id2, _ := jsonrpc.MakeID(float64(42))

// Null ID (for notifications)
id3, _ := jsonrpc.MakeID(nil)

Custom Transport Implementation

The jsonrpc package is primarily used when implementing custom transports. Here's a complete example:

Basic Custom Transport

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()
}

WebSocket Transport

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()
}

Framed Transport (Length-Prefixed)

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"
}

Message Inspection

Inspecting Messages

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
}

Logging Transport

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()
}

Message Routing

Message Router

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)
}

Error Handling

JSON-RPC Errors

// 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,
		},
	}
}

Best Practices

Transport Implementation

  • Always implement proper locking for Write operations
  • Handle context cancellation in Read/Write
  • Provide meaningful SessionID values
  • Clean up resources in Close
  • Return appropriate errors

Message Handling

  • Validate messages after decoding
  • Check for required fields
  • Handle malformed JSON gracefully
  • Log protocol errors for debugging

Performance

  • Reuse encoders/decoders when possible
  • Buffer I/O operations
  • Consider message size limits
  • Implement timeouts for network operations

Error Handling

  • Distinguish between transport and protocol errors
  • Return JSON-RPC error codes appropriately
  • Provide detailed error messages
  • Don't leak sensitive information in errors

Complete Example

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)
	}
}