fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+660
@@ -0,0 +1,660 @@
|
||||
// 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 mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
|
||||
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
|
||||
"github.com/modelcontextprotocol/go-sdk/internal/xcontext"
|
||||
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||
)
|
||||
|
||||
// ErrConnectionClosed is returned when sending a message to a connection that
|
||||
// is closed or in the process of closing.
|
||||
var ErrConnectionClosed = errors.New("connection closed")
|
||||
|
||||
// ErrSessionMissing is returned when the session is known to not be present on
|
||||
// the server.
|
||||
var ErrSessionMissing = errors.New("session not found")
|
||||
|
||||
// A Transport is used to create a bidirectional connection between MCP client
|
||||
// and server.
|
||||
//
|
||||
// Transports should be used for at most one call to [Server.Connect] or
|
||||
// [Client.Connect].
|
||||
type Transport interface {
|
||||
// Connect returns the logical JSON-RPC connection..
|
||||
//
|
||||
// It is called exactly once by [Server.Connect] or [Client.Connect].
|
||||
Connect(ctx context.Context) (Connection, error)
|
||||
}
|
||||
|
||||
// A Connection is a logical bidirectional JSON-RPC connection.
|
||||
type Connection interface {
|
||||
// Read reads the next message to process off the connection.
|
||||
//
|
||||
// Connections must allow Read to be called concurrently with Close. In
|
||||
// particular, calling Close should unblock a Read waiting for input.
|
||||
Read(context.Context) (jsonrpc.Message, error)
|
||||
|
||||
// Write writes a new message to the connection.
|
||||
//
|
||||
// Write may be called concurrently, as calls or responses may occur
|
||||
// concurrently in user code.
|
||||
Write(context.Context, jsonrpc.Message) error
|
||||
|
||||
// Close closes the connection. It is implicitly called whenever a Read or
|
||||
// Write fails.
|
||||
//
|
||||
// Close may be called multiple times, potentially concurrently.
|
||||
Close() error
|
||||
|
||||
// TODO(#148): remove SessionID from this interface.
|
||||
SessionID() string
|
||||
}
|
||||
|
||||
// A ClientConnection is a [Connection] that is specific to the MCP client.
|
||||
//
|
||||
// If client connections implement this interface, they may receive information
|
||||
// about changes to the client session.
|
||||
//
|
||||
// TODO: should this interface be exported?
|
||||
type clientConnection interface {
|
||||
Connection
|
||||
|
||||
// sessionUpdated is called whenever the client session state changes.
|
||||
sessionUpdated(clientSessionState)
|
||||
}
|
||||
|
||||
// A serverConnection is a Connection that is specific to the MCP server.
|
||||
//
|
||||
// If server connections implement this interface, they receive information
|
||||
// about changes to the server session.
|
||||
//
|
||||
// TODO: should this interface be exported?
|
||||
type serverConnection interface {
|
||||
Connection
|
||||
sessionUpdated(ServerSessionState)
|
||||
}
|
||||
|
||||
// A StdioTransport is a [Transport] that communicates over stdin/stdout using
|
||||
// newline-delimited JSON.
|
||||
type StdioTransport struct{}
|
||||
|
||||
// Connect implements the [Transport] interface.
|
||||
func (*StdioTransport) Connect(context.Context) (Connection, error) {
|
||||
return newIOConn(rwc{os.Stdin, nopCloserWriter{os.Stdout}}), nil
|
||||
}
|
||||
|
||||
// nopCloserWriter is an io.WriteCloser with a trivial Close method.
|
||||
type nopCloserWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloserWriter) Close() error { return nil }
|
||||
|
||||
// An IOTransport is a [Transport] that communicates over separate
|
||||
// io.ReadCloser and io.WriteCloser using newline-delimited JSON.
|
||||
type IOTransport struct {
|
||||
Reader io.ReadCloser
|
||||
Writer io.WriteCloser
|
||||
}
|
||||
|
||||
// Connect implements the [Transport] interface.
|
||||
func (t *IOTransport) Connect(context.Context) (Connection, error) {
|
||||
return newIOConn(rwc{t.Reader, t.Writer}), nil
|
||||
}
|
||||
|
||||
// An InMemoryTransport is a [Transport] that communicates over an in-memory
|
||||
// network connection, using newline-delimited JSON.
|
||||
//
|
||||
// InMemoryTransports should be constructed using [NewInMemoryTransports],
|
||||
// which returns two transports connected to each other.
|
||||
type InMemoryTransport struct {
|
||||
rwc io.ReadWriteCloser
|
||||
}
|
||||
|
||||
// Connect implements the [Transport] interface.
|
||||
func (t *InMemoryTransport) Connect(context.Context) (Connection, error) {
|
||||
return newIOConn(t.rwc), nil
|
||||
}
|
||||
|
||||
// NewInMemoryTransports returns two [InMemoryTransport] objects that connect
|
||||
// to each other.
|
||||
//
|
||||
// The resulting transports are symmetrical: use either to connect to a server,
|
||||
// and then the other to connect to a client. Servers must be connected before
|
||||
// clients, as the client initializes the MCP session during connection.
|
||||
func NewInMemoryTransports() (*InMemoryTransport, *InMemoryTransport) {
|
||||
c1, c2 := net.Pipe()
|
||||
return &InMemoryTransport{c1}, &InMemoryTransport{c2}
|
||||
}
|
||||
|
||||
type binder[T handler, State any] interface {
|
||||
// TODO(rfindley): the bind API has gotten too complicated. Simplify.
|
||||
bind(Connection, *jsonrpc2.Connection, State, func()) T
|
||||
disconnect(T)
|
||||
}
|
||||
|
||||
type handler interface {
|
||||
handle(ctx context.Context, req *jsonrpc.Request) (any, error)
|
||||
}
|
||||
|
||||
func connect[H handler, State any](ctx context.Context, t Transport, b binder[H, State], s State, onClose func()) (H, error) {
|
||||
var zero H
|
||||
mcpConn, err := t.Connect(ctx)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
// If logging is configured, write message logs.
|
||||
reader, writer := jsonrpc2.Reader(mcpConn), jsonrpc2.Writer(mcpConn)
|
||||
var (
|
||||
h H
|
||||
preempter canceller
|
||||
)
|
||||
bind := func(conn *jsonrpc2.Connection) jsonrpc2.Handler {
|
||||
h = b.bind(mcpConn, conn, s, onClose)
|
||||
preempter.conn = conn
|
||||
return jsonrpc2.HandlerFunc(h.handle)
|
||||
}
|
||||
_ = jsonrpc2.NewConnection(ctx, jsonrpc2.ConnectionConfig{
|
||||
Reader: reader,
|
||||
Writer: writer,
|
||||
Closer: mcpConn,
|
||||
Bind: bind,
|
||||
Preempter: &preempter,
|
||||
OnDone: func() {
|
||||
b.disconnect(h)
|
||||
},
|
||||
OnInternalError: func(err error) { log.Printf("jsonrpc2 error: %v", err) },
|
||||
})
|
||||
assert(preempter.conn != nil, "unbound preempter")
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// A canceller is a jsonrpc2.Preempter that cancels in-flight requests on MCP
|
||||
// cancelled notifications.
|
||||
type canceller struct {
|
||||
conn *jsonrpc2.Connection
|
||||
}
|
||||
|
||||
// Preempt implements [jsonrpc2.Preempter].
|
||||
func (c *canceller) Preempt(ctx context.Context, req *jsonrpc.Request) (result any, err error) {
|
||||
if req.Method == notificationCancelled {
|
||||
var params CancelledParams
|
||||
if err := internaljson.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := jsonrpc2.MakeID(params.RequestID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go c.conn.Cancel(id)
|
||||
}
|
||||
return nil, jsonrpc2.ErrNotHandled
|
||||
}
|
||||
|
||||
// call executes and awaits a jsonrpc2 call on the given connection,
|
||||
// translating errors into the mcp domain.
|
||||
func call(ctx context.Context, conn *jsonrpc2.Connection, method string, params Params, result Result) error {
|
||||
// The "%w"s in this function expose jsonrpc.Error as part of the API.
|
||||
call := conn.Call(ctx, method, params)
|
||||
err := call.Await(ctx, result)
|
||||
switch {
|
||||
case errors.Is(err, jsonrpc2.ErrClientClosing), errors.Is(err, jsonrpc2.ErrServerClosing):
|
||||
return fmt.Errorf("%w: calling %q: %v", ErrConnectionClosed, method, err)
|
||||
case ctx.Err() != nil:
|
||||
// Notify the peer of cancellation.
|
||||
err := conn.Notify(xcontext.Detach(ctx), notificationCancelled, &CancelledParams{
|
||||
Reason: ctx.Err().Error(),
|
||||
RequestID: call.ID().Raw(),
|
||||
})
|
||||
// By default, the jsonrpc2 library waits for graceful shutdown when the
|
||||
// connection is closed, meaning it expects all outgoing and incoming
|
||||
// requests to complete. However, for MCP this expectation is unrealistic,
|
||||
// and can lead to hanging shutdown. For example, if a streamable client is
|
||||
// killed, the server will not be able to detect this event, except via
|
||||
// keepalive pings (if they are configured), and so outgoing calls may hang
|
||||
// indefinitely.
|
||||
//
|
||||
// Therefore, we choose to eagerly retire calls, removing them from the
|
||||
// outgoingCalls map, when the caller context is cancelled: if the caller
|
||||
// will never receive the response, there's no need to track it.
|
||||
conn.Retire(call, ctx.Err())
|
||||
return errors.Join(ctx.Err(), err)
|
||||
case err != nil:
|
||||
return fmt.Errorf("calling %q: %w", method, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A LoggingTransport is a [Transport] that delegates to another transport,
|
||||
// writing RPC logs to an io.Writer.
|
||||
type LoggingTransport struct {
|
||||
Transport Transport
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
// Connect connects the underlying transport, returning a [Connection] that writes
|
||||
// logs to the configured destination.
|
||||
func (t *LoggingTransport) Connect(ctx context.Context) (Connection, error) {
|
||||
delegate, err := t.Transport.Connect(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &loggingConn{delegate: delegate, w: t.Writer}, nil
|
||||
}
|
||||
|
||||
type loggingConn struct {
|
||||
delegate Connection
|
||||
|
||||
mu sync.Mutex
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (c *loggingConn) SessionID() string { return c.delegate.SessionID() }
|
||||
|
||||
// Read is a stream middleware that logs incoming messages.
|
||||
func (s *loggingConn) Read(ctx context.Context) (jsonrpc.Message, error) {
|
||||
msg, err := s.delegate.Read(ctx)
|
||||
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
fmt.Fprintf(s.w, "read error: %v\n", err)
|
||||
s.mu.Unlock()
|
||||
} else {
|
||||
data, err := jsonrpc2.EncodeMessage(msg)
|
||||
s.mu.Lock()
|
||||
if err != nil {
|
||||
fmt.Fprintf(s.w, "LoggingTransport: failed to marshal: %v", err)
|
||||
}
|
||||
fmt.Fprintf(s.w, "read: %s\n", string(data))
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// Write is a stream middleware that logs outgoing messages.
|
||||
func (s *loggingConn) Write(ctx context.Context, msg jsonrpc.Message) error {
|
||||
err := s.delegate.Write(ctx, msg)
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
fmt.Fprintf(s.w, "write error: %v\n", err)
|
||||
s.mu.Unlock()
|
||||
} else {
|
||||
data, err := jsonrpc2.EncodeMessage(msg)
|
||||
s.mu.Lock()
|
||||
if err != nil {
|
||||
fmt.Fprintf(s.w, "LoggingTransport: failed to marshal: %v", err)
|
||||
}
|
||||
fmt.Fprintf(s.w, "write: %s\n", string(data))
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *loggingConn) Close() error {
|
||||
return s.delegate.Close()
|
||||
}
|
||||
|
||||
// A rwc binds an io.ReadCloser and io.WriteCloser together to create an
|
||||
// io.ReadWriteCloser.
|
||||
type rwc struct {
|
||||
rc io.ReadCloser
|
||||
wc io.WriteCloser
|
||||
}
|
||||
|
||||
func (r rwc) Read(p []byte) (n int, err error) {
|
||||
return r.rc.Read(p)
|
||||
}
|
||||
|
||||
func (r rwc) Write(p []byte) (n int, err error) {
|
||||
return r.wc.Write(p)
|
||||
}
|
||||
|
||||
func (r rwc) Close() error {
|
||||
rcErr := r.rc.Close()
|
||||
|
||||
var wcErr error
|
||||
if r.wc != nil { // we only allow a nil writer in unit tests
|
||||
wcErr = r.wc.Close()
|
||||
}
|
||||
|
||||
return errors.Join(rcErr, wcErr)
|
||||
}
|
||||
|
||||
// An ioConn is a transport that delimits messages with newlines across
|
||||
// a bidirectional stream, and supports jsonrpc.2 message batching.
|
||||
//
|
||||
// See https://github.com/ndjson/ndjson-spec for discussion of newline
|
||||
// delimited JSON.
|
||||
//
|
||||
// See [msgBatch] for more discussion of message batching.
|
||||
type ioConn struct {
|
||||
protocolVersion string // negotiated version, set during session initialization.
|
||||
|
||||
writeMu sync.Mutex // guards Write, which must be concurrency safe.
|
||||
rwc io.ReadWriteCloser // the underlying stream
|
||||
|
||||
// incoming receives messages from the read loop started in [newIOConn].
|
||||
incoming <-chan msgOrErr
|
||||
|
||||
// If outgoiBatch has a positive capacity, it will be used to batch requests
|
||||
// and notifications before sending.
|
||||
outgoingBatch []jsonrpc.Message
|
||||
|
||||
// Unread messages in the last batch. Since reads are serialized, there is no
|
||||
// need to guard here.
|
||||
queue []jsonrpc.Message
|
||||
|
||||
// batches correlate incoming requests to the batch in which they arrived.
|
||||
// Since writes may be concurrent to reads, we need to guard this with a mutex.
|
||||
batchMu sync.Mutex
|
||||
batches map[jsonrpc2.ID]*msgBatch // lazily allocated
|
||||
|
||||
closeOnce sync.Once
|
||||
closed chan struct{}
|
||||
closeErr error
|
||||
}
|
||||
|
||||
type msgOrErr struct {
|
||||
msg json.RawMessage
|
||||
err error
|
||||
}
|
||||
|
||||
func newIOConn(rwc io.ReadWriteCloser) *ioConn {
|
||||
var (
|
||||
incoming = make(chan msgOrErr)
|
||||
closed = make(chan struct{})
|
||||
)
|
||||
// Start a goroutine for reads, so that we can select on the incoming channel
|
||||
// in [ioConn.Read] and unblock the read as soon as Close is called (see #224).
|
||||
//
|
||||
// This leaks a goroutine if rwc.Read does not unblock after it is closed,
|
||||
// but that is unavoidable since AFAIK there is no (easy and portable) way to
|
||||
// guarantee that reads of stdin are unblocked when closed.
|
||||
go func() {
|
||||
dec := json.NewDecoder(rwc)
|
||||
for {
|
||||
var raw json.RawMessage
|
||||
err := dec.Decode(&raw)
|
||||
// If decoding was successful, check for trailing data at the end of the stream.
|
||||
if err == nil {
|
||||
// Read the next byte to check if there is trailing data.
|
||||
var tr [1]byte
|
||||
if n, readErr := dec.Buffered().Read(tr[:]); n > 0 {
|
||||
// If read byte is not a newline, it is an error.
|
||||
// Support both Unix (\n) and Windows (\r\n) line endings.
|
||||
if tr[0] != '\n' && tr[0] != '\r' {
|
||||
err = fmt.Errorf("invalid trailing data at the end of stream")
|
||||
}
|
||||
} else if readErr != nil && readErr != io.EOF {
|
||||
err = readErr
|
||||
}
|
||||
}
|
||||
select {
|
||||
case incoming <- msgOrErr{msg: raw, err: err}:
|
||||
case <-closed:
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return &ioConn{
|
||||
rwc: rwc,
|
||||
incoming: incoming,
|
||||
closed: closed,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ioConn) SessionID() string { return "" }
|
||||
|
||||
func (c *ioConn) sessionUpdated(state ServerSessionState) {
|
||||
protocolVersion := ""
|
||||
if state.InitializeParams != nil {
|
||||
protocolVersion = state.InitializeParams.ProtocolVersion
|
||||
}
|
||||
if protocolVersion == "" {
|
||||
protocolVersion = protocolVersion20250326
|
||||
}
|
||||
c.protocolVersion = negotiatedVersion(protocolVersion)
|
||||
}
|
||||
|
||||
// addBatch records a msgBatch for an incoming batch payload.
|
||||
// It returns an error if batch is malformed, containing previously seen IDs.
|
||||
//
|
||||
// See [msgBatch] for more.
|
||||
func (t *ioConn) addBatch(batch *msgBatch) error {
|
||||
t.batchMu.Lock()
|
||||
defer t.batchMu.Unlock()
|
||||
for id := range batch.unresolved {
|
||||
if _, ok := t.batches[id]; ok {
|
||||
return fmt.Errorf("%w: batch contains previously seen request %v", jsonrpc2.ErrInvalidRequest, id.Raw())
|
||||
}
|
||||
}
|
||||
for id := range batch.unresolved {
|
||||
if t.batches == nil {
|
||||
t.batches = make(map[jsonrpc2.ID]*msgBatch)
|
||||
}
|
||||
t.batches[id] = batch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateBatch records a response in the message batch tracking the
|
||||
// corresponding incoming call, if any.
|
||||
//
|
||||
// The second result reports whether resp was part of a batch. If this is true,
|
||||
// the first result is nil if the batch is still incomplete, or the full set of
|
||||
// batch responses if resp completed the batch.
|
||||
func (t *ioConn) updateBatch(resp *jsonrpc.Response) ([]*jsonrpc.Response, bool) {
|
||||
t.batchMu.Lock()
|
||||
defer t.batchMu.Unlock()
|
||||
|
||||
if batch, ok := t.batches[resp.ID]; ok {
|
||||
idx, ok := batch.unresolved[resp.ID]
|
||||
if !ok {
|
||||
panic("internal error: inconsistent batches")
|
||||
}
|
||||
batch.responses[idx] = resp
|
||||
delete(batch.unresolved, resp.ID)
|
||||
delete(t.batches, resp.ID)
|
||||
if len(batch.unresolved) == 0 {
|
||||
return batch.responses, true
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// A msgBatch records information about an incoming batch of jsonrpc.2 calls.
|
||||
//
|
||||
// The jsonrpc.2 spec (https://www.jsonrpc.org/specification#batch) says:
|
||||
//
|
||||
// "The Server should respond with an Array containing the corresponding
|
||||
// Response objects, after all of the batch Request objects have been
|
||||
// processed. A Response object SHOULD exist for each Request object, except
|
||||
// that there SHOULD NOT be any Response objects for notifications. The Server
|
||||
// MAY process a batch rpc call as a set of concurrent tasks, processing them
|
||||
// in any order and with any width of parallelism."
|
||||
//
|
||||
// Therefore, a msgBatch keeps track of outstanding calls and their responses.
|
||||
// When there are no unresolved calls, the response payload is sent.
|
||||
type msgBatch struct {
|
||||
unresolved map[jsonrpc2.ID]int
|
||||
responses []*jsonrpc.Response
|
||||
}
|
||||
|
||||
func (t *ioConn) Read(ctx context.Context) (jsonrpc.Message, error) {
|
||||
// As a matter of principle, enforce that reads on a closed context return an
|
||||
// error.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
if len(t.queue) > 0 {
|
||||
next := t.queue[0]
|
||||
t.queue = t.queue[1:]
|
||||
return next, nil
|
||||
}
|
||||
|
||||
var raw json.RawMessage
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
|
||||
case v := <-t.incoming:
|
||||
if v.err != nil {
|
||||
return nil, v.err
|
||||
}
|
||||
raw = v.msg
|
||||
|
||||
case <-t.closed:
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
msgs, batch, err := readBatch(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if batch && t.protocolVersion >= protocolVersion20250618 {
|
||||
return nil, fmt.Errorf("JSON-RPC batching is not supported in %s and later (request version: %s)", protocolVersion20250618, t.protocolVersion)
|
||||
}
|
||||
|
||||
t.queue = msgs[1:]
|
||||
|
||||
if batch {
|
||||
var respBatch *msgBatch // track incoming requests in the batch
|
||||
for _, msg := range msgs {
|
||||
if req, ok := msg.(*jsonrpc.Request); ok {
|
||||
if respBatch == nil {
|
||||
respBatch = &msgBatch{
|
||||
unresolved: make(map[jsonrpc2.ID]int),
|
||||
}
|
||||
}
|
||||
if _, ok := respBatch.unresolved[req.ID]; ok {
|
||||
return nil, fmt.Errorf("duplicate message ID %q", req.ID)
|
||||
}
|
||||
respBatch.unresolved[req.ID] = len(respBatch.responses)
|
||||
respBatch.responses = append(respBatch.responses, nil)
|
||||
}
|
||||
}
|
||||
if respBatch != nil {
|
||||
// The batch contains one or more incoming requests to track.
|
||||
if err := t.addBatch(respBatch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return msgs[0], err
|
||||
}
|
||||
|
||||
// readBatch reads batch data, which may be either a single JSON-RPC message,
|
||||
// or an array of JSON-RPC messages.
|
||||
func readBatch(data []byte) (msgs []jsonrpc.Message, isBatch bool, _ error) {
|
||||
// Try to read an array of messages first.
|
||||
var rawBatch []json.RawMessage
|
||||
if err := internaljson.Unmarshal(data, &rawBatch); err == nil {
|
||||
if len(rawBatch) == 0 {
|
||||
return nil, true, fmt.Errorf("empty batch")
|
||||
}
|
||||
for _, raw := range rawBatch {
|
||||
msg, err := jsonrpc2.DecodeMessage(raw)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
return msgs, true, nil
|
||||
}
|
||||
// Try again with a single message.
|
||||
msg, err := jsonrpc2.DecodeMessage(data)
|
||||
return []jsonrpc.Message{msg}, false, err
|
||||
}
|
||||
|
||||
func (t *ioConn) Write(ctx context.Context, msg jsonrpc.Message) error {
|
||||
// As in [ioConn.Read], enforce that Writes on a closed context are an error.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
t.writeMu.Lock()
|
||||
defer t.writeMu.Unlock()
|
||||
|
||||
// Batching support: if msg is a Response, it may have completed a batch, so
|
||||
// check that first. Otherwise, it is a request or notification, and we may
|
||||
// want to collect it into a batch before sending, if we're configured to use
|
||||
// outgoing batches.
|
||||
if resp, ok := msg.(*jsonrpc.Response); ok {
|
||||
if batch, ok := t.updateBatch(resp); ok {
|
||||
if len(batch) > 0 {
|
||||
data, err := marshalMessages(batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
_, err = t.rwc.Write(data)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else if len(t.outgoingBatch) < cap(t.outgoingBatch) {
|
||||
t.outgoingBatch = append(t.outgoingBatch, msg)
|
||||
if len(t.outgoingBatch) == cap(t.outgoingBatch) {
|
||||
data, err := marshalMessages(t.outgoingBatch)
|
||||
t.outgoingBatch = t.outgoingBatch[:0]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
_, err = t.rwc.Write(data)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
data, err := jsonrpc2.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling message: %v", err)
|
||||
}
|
||||
data = append(data, '\n') // newline delimited
|
||||
_, err = t.rwc.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *ioConn) Close() error {
|
||||
t.closeOnce.Do(func() {
|
||||
t.closeErr = t.rwc.Close()
|
||||
close(t.closed)
|
||||
})
|
||||
return t.closeErr
|
||||
}
|
||||
|
||||
func marshalMessages[T jsonrpc.Message](msgs []T) ([]byte, error) {
|
||||
var rawMsgs []json.RawMessage
|
||||
for _, msg := range msgs {
|
||||
raw, err := jsonrpc2.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encoding batch message: %w", err)
|
||||
}
|
||||
rawMsgs = append(rawMsgs, raw)
|
||||
}
|
||||
return json.Marshal(rawMsgs)
|
||||
}
|
||||
Reference in New Issue
Block a user