Update the jsonrpc2 MCP framer to support both incoming and outgoing batches. In order to achieve this, we must correlate the framed Reader and Writer, which is not explicitly supported by the jsonrpc2 API, but does work. A note is left to revisit the Framer interface. Change-Id: If060cb5822da067833db20d58f5f112a0528da91 Reviewed-on: https://go-review.googlesource.com/c/tools/+/667295 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
// Copyright 2018 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 jsonrpc2
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Reader abstracts the transport mechanics from the JSON RPC protocol.
|
|
// A Conn reads messages from the reader it was provided on construction,
|
|
// and assumes that each call to Read fully transfers a single message,
|
|
// or returns an error.
|
|
// A reader is not safe for concurrent use, it is expected it will be used by
|
|
// a single Conn in a safe manner.
|
|
type Reader interface {
|
|
// Read gets the next message from the stream.
|
|
Read(context.Context) (Message, int64, error)
|
|
}
|
|
|
|
// Writer abstracts the transport mechanics from the JSON RPC protocol.
|
|
// A Conn writes messages using the writer it was provided on construction,
|
|
// and assumes that each call to Write fully transfers a single message,
|
|
// or returns an error.
|
|
// A writer is not safe for concurrent use, it is expected it will be used by
|
|
// a single Conn in a safe manner.
|
|
type Writer interface {
|
|
// Write sends a message to the stream.
|
|
Write(context.Context, Message) (int64, error)
|
|
}
|
|
|
|
// Framer wraps low level byte readers and writers into jsonrpc2 message
|
|
// readers and writers.
|
|
// It is responsible for the framing and encoding of messages into wire form.
|
|
//
|
|
// TODO(rfindley): rethink the framer interface, as with JSONRPC2 batching
|
|
// there is a need for Reader and Writer to be correlated, and while the
|
|
// implementation of framing here allows that, it is not made explicit by the
|
|
// interface.
|
|
//
|
|
// Perhaps a better interface would be
|
|
//
|
|
// Frame(io.ReadWriteCloser) (Reader, Writer).
|
|
type Framer interface {
|
|
// Reader wraps a byte reader into a message reader.
|
|
Reader(io.Reader) Reader
|
|
// Writer wraps a byte writer into a message writer.
|
|
Writer(io.Writer) Writer
|
|
}
|
|
|
|
// RawFramer returns a new Framer.
|
|
// The messages are sent with no wrapping, and rely on json decode consistency
|
|
// to determine message boundaries.
|
|
func RawFramer() Framer { return rawFramer{} }
|
|
|
|
type rawFramer struct{}
|
|
type rawReader struct{ in *json.Decoder }
|
|
type rawWriter struct{ out io.Writer }
|
|
|
|
func (rawFramer) Reader(rw io.Reader) Reader {
|
|
return &rawReader{in: json.NewDecoder(rw)}
|
|
}
|
|
|
|
func (rawFramer) Writer(rw io.Writer) Writer {
|
|
return &rawWriter{out: rw}
|
|
}
|
|
|
|
func (r *rawReader) Read(ctx context.Context) (Message, int64, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, 0, ctx.Err()
|
|
default:
|
|
}
|
|
var raw json.RawMessage
|
|
if err := r.in.Decode(&raw); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
msg, err := DecodeMessage(raw)
|
|
return msg, int64(len(raw)), err
|
|
}
|
|
|
|
func (w *rawWriter) Write(ctx context.Context, msg Message) (int64, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
default:
|
|
}
|
|
data, err := EncodeMessage(msg)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("marshaling message: %v", err)
|
|
}
|
|
n, err := w.out.Write(data)
|
|
return int64(n), err
|
|
}
|
|
|
|
// HeaderFramer returns a new Framer.
|
|
// The messages are sent with HTTP content length and MIME type headers.
|
|
// This is the format used by LSP and others.
|
|
func HeaderFramer() Framer { return headerFramer{} }
|
|
|
|
type headerFramer struct{}
|
|
type headerReader struct{ in *bufio.Reader }
|
|
type headerWriter struct{ out io.Writer }
|
|
|
|
func (headerFramer) Reader(rw io.Reader) Reader {
|
|
return &headerReader{in: bufio.NewReader(rw)}
|
|
}
|
|
|
|
func (headerFramer) Writer(rw io.Writer) Writer {
|
|
return &headerWriter{out: rw}
|
|
}
|
|
|
|
func (r *headerReader) Read(ctx context.Context) (Message, int64, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, 0, ctx.Err()
|
|
default:
|
|
}
|
|
var total, length int64
|
|
// read the header, stop on the first empty line
|
|
for {
|
|
line, err := r.in.ReadString('\n')
|
|
total += int64(len(line))
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
if total == 0 {
|
|
return nil, 0, io.EOF
|
|
}
|
|
err = io.ErrUnexpectedEOF
|
|
}
|
|
return nil, total, fmt.Errorf("failed reading header line: %w", err)
|
|
}
|
|
line = strings.TrimSpace(line)
|
|
// check we have a header line
|
|
if line == "" {
|
|
break
|
|
}
|
|
colon := strings.IndexRune(line, ':')
|
|
if colon < 0 {
|
|
return nil, total, fmt.Errorf("invalid header line %q", line)
|
|
}
|
|
name, value := line[:colon], strings.TrimSpace(line[colon+1:])
|
|
switch name {
|
|
case "Content-Length":
|
|
if length, err = strconv.ParseInt(value, 10, 32); err != nil {
|
|
return nil, total, fmt.Errorf("failed parsing Content-Length: %v", value)
|
|
}
|
|
if length <= 0 {
|
|
return nil, total, fmt.Errorf("invalid Content-Length: %v", length)
|
|
}
|
|
default:
|
|
// ignoring unknown headers
|
|
}
|
|
}
|
|
if length == 0 {
|
|
return nil, total, fmt.Errorf("missing Content-Length header")
|
|
}
|
|
data := make([]byte, length)
|
|
n, err := io.ReadFull(r.in, data)
|
|
total += int64(n)
|
|
if err != nil {
|
|
return nil, total, err
|
|
}
|
|
msg, err := DecodeMessage(data)
|
|
return msg, total, err
|
|
}
|
|
|
|
func (w *headerWriter) Write(ctx context.Context, msg Message) (int64, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
default:
|
|
}
|
|
data, err := EncodeMessage(msg)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("marshaling message: %v", err)
|
|
}
|
|
n, err := fmt.Fprintf(w.out, "Content-Length: %v\r\n\r\n", len(data))
|
|
total := int64(n)
|
|
if err == nil {
|
|
n, err = w.out.Write(data)
|
|
total += int64(n)
|
|
}
|
|
return total, err
|
|
}
|