Skip to content

Commit fe208c1

Browse files
authored
jsonrpc: expose encoding and decoding functions (#114)
Expose `jsonrpc2.MakeID`, `jsonrpc2.EncodeMessage` and `jsonrpc2.DecodeMessage` functions to allow implementing custom `mcp.Transport`. Add an example that demonstrates a custom transport implementation. Fixes #110.
1 parent aa9d4b2 commit fe208c1

File tree

3 files changed

+129
-1
lines changed

3 files changed

+129
-1
lines changed

examples/custom-transport/main.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"bufio"
9+
"context"
10+
"errors"
11+
"io"
12+
"log"
13+
"os"
14+
15+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
16+
"github.com/modelcontextprotocol/go-sdk/mcp"
17+
)
18+
19+
// IOTransport is a simplified implementation of a transport that communicates using
20+
// newline-delimited JSON over an io.Reader and io.Writer. It is similar to ioTransport
21+
// in transport.go and serves as a demonstration of how to implement a custom transport.
22+
type IOTransport struct {
23+
r *bufio.Reader
24+
w io.Writer
25+
}
26+
27+
// NewIOTransport creates a new IOTransport with the given io.Reader and io.Writer.
28+
func NewIOTransport(r io.Reader, w io.Writer) *IOTransport {
29+
return &IOTransport{
30+
r: bufio.NewReader(r),
31+
w: w,
32+
}
33+
}
34+
35+
// ioConn is a connection that uses newlines to delimit messages. It implements [mcp.Connection].
36+
type ioConn struct {
37+
r *bufio.Reader
38+
w io.Writer
39+
}
40+
41+
// Connect implements [mcp.Transport.Connect] by creating a new ioConn.
42+
func (t *IOTransport) Connect(ctx context.Context) (mcp.Connection, error) {
43+
return &ioConn{
44+
r: t.r,
45+
w: t.w,
46+
}, nil
47+
}
48+
49+
// Read implements [mcp.Connection.Read], assuming messages are newline-delimited JSON.
50+
func (t *ioConn) Read(context.Context) (jsonrpc.Message, error) {
51+
data, err := t.r.ReadBytes('\n')
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
return jsonrpc.DecodeMessage(data[:len(data)-1])
57+
}
58+
59+
// Write implements [mcp.Connection.Write], appending a newline delimiter after the message.
60+
func (t *ioConn) Write(_ context.Context, msg jsonrpc.Message) error {
61+
data, err := jsonrpc.EncodeMessage(msg)
62+
if err != nil {
63+
return err
64+
}
65+
66+
_, err1 := t.w.Write(data)
67+
_, err2 := t.w.Write([]byte{'\n'})
68+
return errors.Join(err1, err2)
69+
}
70+
71+
// Close implements [mcp.Connection.Close]. Since this is a simplified example, it is a no-op.
72+
func (t *ioConn) Close() error {
73+
return nil
74+
}
75+
76+
// SessionID implements [mcp.Connection.SessionID]. Since this is a simplified example,
77+
// it returns an empty session ID.
78+
func (t *ioConn) SessionID() string {
79+
return ""
80+
}
81+
82+
// HiArgs is the argument type for the SayHi tool.
83+
type HiArgs struct {
84+
Name string `json:"name" mcp:"the name to say hi to"`
85+
}
86+
87+
// SayHi is a tool handler that responds with a greeting.
88+
func SayHi(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[HiArgs]) (*mcp.CallToolResultFor[struct{}], error) {
89+
return &mcp.CallToolResultFor[struct{}]{
90+
Content: []mcp.Content{
91+
&mcp.TextContent{Text: "Hi " + params.Arguments.Name},
92+
},
93+
}, nil
94+
}
95+
96+
func main() {
97+
server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil)
98+
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
99+
100+
// Run the server with a custom IOTransport using stdio as the io.Reader and io.Writer.
101+
transport := &IOTransport{
102+
r: bufio.NewReader(os.Stdin),
103+
w: os.Stdout,
104+
}
105+
err := server.Run(context.Background(), transport)
106+
if err != nil {
107+
log.Println("[ERROR]: Failed to run server:", err)
108+
}
109+
}

internal/jsonrpc2/messages.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type ID struct {
1919
// MakeID coerces the given Go value to an ID. The value is assumed to be the
2020
// default JSON marshaling of a Request identifier -- nil, float64, or string.
2121
//
22-
// Returns an error if the value type was a valid Request ID type.
22+
// Returns an error if the value type was not a valid Request ID type.
2323
//
2424
// TODO: ID can't be a json.Marshaler/Unmarshaler, because we want to omitzero.
2525
// Simplify this package by making ID json serializable once we can rely on

jsonrpc/jsonrpc.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,22 @@ type (
1818
// Response is a JSON-RPC response.
1919
Response = jsonrpc2.Response
2020
)
21+
22+
// MakeID coerces the given Go value to an ID. The value is assumed to be the
23+
// default JSON marshaling of a Request identifier -- nil, float64, or string.
24+
//
25+
// Returns an error if the value type was not a valid Request ID type.
26+
func MakeID(v any) (ID, error) {
27+
return jsonrpc2.MakeID(v)
28+
}
29+
30+
// EncodeMessage serializes a JSON-RPC message to its wire format.
31+
func EncodeMessage(msg Message) ([]byte, error) {
32+
return jsonrpc2.EncodeMessage(msg)
33+
}
34+
35+
// DecodeMessage deserializes JSON-RPC wire format data into a Message.
36+
// It returns either a Request or Response based on the message content.
37+
func DecodeMessage(data []byte) (Message, error) {
38+
return jsonrpc2.DecodeMessage(data)
39+
}

0 commit comments

Comments
 (0)