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/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 f175e597..1cf1202f 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -18,3 +18,22 @@ type ( // Response is a JSON-RPC 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) +}