From 197c21fe30246405d903fda4a9408ee743decc41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vid=20Drobni=C4=8D?= Date: Thu, 10 Jul 2025 08:17:59 +0200 Subject: [PATCH 1/2] jsonrpc: expose encoding and decoding functions 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 --- examples/custom-transport/main.go | 109 ++++++++++++++++++++++++++++++ jsonrpc/jsonrpc.go | 12 ++++ 2 files changed, 121 insertions(+) create mode 100644 examples/custom-transport/main.go diff --git a/examples/custom-transport/main.go b/examples/custom-transport/main.go new file mode 100644 index 00000000..cc4f15f3 --- /dev/null +++ b/examples/custom-transport/main.go @@ -0,0 +1,109 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "context" + "errors" + "io" + "log" + "os" + + "github.com/modelcontextprotocol/go-sdk/jsonrpc" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// IOTransport is a simplified implementation of a transport that communicates using +// newline-delimited JSON over an io.Reader and io.Writer. It is similar to ioTransport +// in transport.go and serves as a demonstration of how to implement a custom transport. +type IOTransport struct { + r *bufio.Reader + w io.Writer +} + +// NewIOTransport creates a new IOTransport with the given io.Reader and io.Writer. +func NewIOTransport(r io.Reader, w io.Writer) *IOTransport { + return &IOTransport{ + r: bufio.NewReader(r), + w: w, + } +} + +// ioConn is a connection that uses newlines to delimit messages. It implements [mcp.Connection]. +type ioConn struct { + r *bufio.Reader + w io.Writer +} + +// Connect implements [mcp.Transport.Connect] by creating a new ioConn. +func (t *IOTransport) Connect(ctx context.Context) (mcp.Connection, error) { + return &ioConn{ + r: t.r, + w: t.w, + }, nil +} + +// Read implements [mcp.Connection.Read], assuming messages are newline-delimited JSON. +func (t *ioConn) Read(context.Context) (jsonrpc.Message, error) { + data, err := t.r.ReadBytes('\n') + if err != nil { + return nil, err + } + + return jsonrpc.DecodeMessage(data[:len(data)-1]) +} + +// Write implements [mcp.Connection.Write], appending a newline delimiter after the message. +func (t *ioConn) Write(_ context.Context, msg jsonrpc.Message) error { + data, err := jsonrpc.EncodeMessage(msg) + if err != nil { + return err + } + + _, err1 := t.w.Write(data) + _, err2 := t.w.Write([]byte{'\n'}) + return errors.Join(err1, err2) +} + +// Close implements [mcp.Connection.Close]. Since this is a simplified example, it is a no-op. +func (t *ioConn) Close() error { + return nil +} + +// SessionID implements [mcp.Connection.SessionID]. Since this is a simplified example, +// it returns an empty session ID. +func (t *ioConn) SessionID() string { + return "" +} + +// HiArgs is the argument type for the SayHi tool. +type HiArgs struct { + Name string `json:"name" mcp:"the name to say hi to"` +} + +// SayHi is a tool handler that responds with a greeting. +func SayHi(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[HiArgs]) (*mcp.CallToolResultFor[struct{}], error) { + return &mcp.CallToolResultFor[struct{}]{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "Hi " + params.Arguments.Name}, + }, + }, nil +} + +func main() { + server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil) + mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi) + + // Run the server with a custom IOTransport using stdio as the io.Reader and io.Writer. + transport := &IOTransport{ + r: bufio.NewReader(os.Stdin), + w: os.Stdout, + } + err := server.Run(context.Background(), transport) + if err != nil { + log.Println("[ERROR]: Failed to run server:", err) + } +} diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index f175e597..c2393a01 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -18,3 +18,15 @@ type ( // Response is a JSON-RPC response. Response = jsonrpc2.Response ) + +func MakeID(v any) (ID, error) { + return jsonrpc2.MakeID(v) +} + +func EncodeMessage(msg Message) ([]byte, error) { + return jsonrpc2.EncodeMessage(msg) +} + +func DecodeMessage(data []byte) (Message, error) { + return jsonrpc2.DecodeMessage(data) +} From 01e7def913ed25ff1f3c73f221d9b2d2d03bbc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vid=20Drobni=C4=8D?= Date: Mon, 14 Jul 2025 07:15:53 +0200 Subject: [PATCH 2/2] jsonrpc: document exposed functions Add documentation to exported `jsonrpc` functions. --- internal/jsonrpc2/messages.go | 2 +- jsonrpc/jsonrpc.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/jsonrpc2/messages.go b/internal/jsonrpc2/messages.go index 03371b91..2de3d4f0 100644 --- a/internal/jsonrpc2/messages.go +++ b/internal/jsonrpc2/messages.go @@ -19,7 +19,7 @@ type ID struct { // MakeID coerces the given Go value to an ID. The value is assumed to be the // default JSON marshaling of a Request identifier -- nil, float64, or string. // -// Returns an error if the value type was a valid Request ID type. +// Returns an error if the value type was not a valid Request ID type. // // TODO: ID can't be a json.Marshaler/Unmarshaler, because we want to omitzero. // Simplify this package by making ID json serializable once we can rely on diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index c2393a01..1cf1202f 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -19,14 +19,21 @@ type ( Response = jsonrpc2.Response ) +// MakeID coerces the given Go value to an ID. The value is assumed to be the +// default JSON marshaling of a Request identifier -- nil, float64, or string. +// +// Returns an error if the value type was not a valid Request ID type. func MakeID(v any) (ID, error) { return jsonrpc2.MakeID(v) } +// EncodeMessage serializes a JSON-RPC message to its wire format. func EncodeMessage(msg Message) ([]byte, error) { return jsonrpc2.EncodeMessage(msg) } +// DecodeMessage deserializes JSON-RPC wire format data into a Message. +// It returns either a Request or Response based on the message content. func DecodeMessage(data []byte) (Message, error) { return jsonrpc2.DecodeMessage(data) }