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.
This commit is contained in:
parent
aa9d4b28be
commit
fe208c144f
3 changed files with 129 additions and 1 deletions
109
examples/custom-transport/main.go
Normal file
109
examples/custom-transport/main.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ type ID struct {
|
||||||
// MakeID coerces the given Go value to an ID. The value is assumed to be the
|
// 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.
|
// 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.
|
// 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
|
// Simplify this package by making ID json serializable once we can rely on
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,22 @@ type (
|
||||||
// Response is a JSON-RPC response.
|
// Response is a JSON-RPC response.
|
||||||
Response = jsonrpc2.Response
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue