This change updates the MCP client stdio JSON decoder to correctly handle both Unix (`\n`) and Windows (`\r\n`) line endings. Fixes #664 Co-authored-by: qysz <i_zf@qq.com>
126 lines
3.7 KiB
Go
126 lines
3.7 KiB
Go
// 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"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
|
|
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
|
)
|
|
|
|
func TestBatchFraming(t *testing.T) {
|
|
// This test checks that the ndjsonFramer can read and write JSON batches.
|
|
//
|
|
// The framer is configured to write a batch size of 2, and we confirm that
|
|
// nothing is sent over the wire until the second write, at which point both
|
|
// messages become available.
|
|
ctx := context.Background()
|
|
|
|
r, w := io.Pipe()
|
|
tport := newIOConn(rwc{r, w})
|
|
tport.outgoingBatch = make([]jsonrpc.Message, 0, 2)
|
|
t.Cleanup(func() { tport.Close() })
|
|
|
|
// Read the two messages into a channel, for easy testing later.
|
|
read := make(chan jsonrpc.Message)
|
|
go func() {
|
|
for range 2 {
|
|
msg, _ := tport.Read(ctx)
|
|
read <- msg
|
|
}
|
|
}()
|
|
|
|
// The first write should not yet be observed by the reader.
|
|
tport.Write(ctx, &jsonrpc.Request{ID: jsonrpc2.Int64ID(1), Method: "test"})
|
|
select {
|
|
case got := <-read:
|
|
t.Fatalf("after one write, got message %v", got)
|
|
default:
|
|
}
|
|
|
|
// ...but the second write causes both messages to be observed.
|
|
tport.Write(ctx, &jsonrpc.Request{ID: jsonrpc2.Int64ID(2), Method: "test"})
|
|
for _, want := range []int64{1, 2} {
|
|
got := <-read
|
|
if got := got.(*jsonrpc.Request).ID.Raw(); got != want {
|
|
t.Errorf("got message #%d, want #%d", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIOConnRead(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
protocolVersion string
|
|
}{
|
|
{
|
|
name: "valid json input",
|
|
input: `{"jsonrpc":"2.0","id":1,"method":"test","params":{}}`,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "newline at the end of first valid json input",
|
|
input: `{"jsonrpc":"2.0","id":1,"method":"test","params":{}}
|
|
`,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "bad data at the end of first valid json input",
|
|
input: `{"jsonrpc":"2.0","id":1,"method":"test","params":{}},`,
|
|
want: "invalid trailing data at the end of stream",
|
|
},
|
|
{
|
|
name: "batching unknown protocol",
|
|
input: `[{"jsonrpc":"2.0","id":1,"method":"test1"},{"jsonrpc":"2.0","id":2,"method":"test2"}]`,
|
|
want: "",
|
|
protocolVersion: "",
|
|
},
|
|
{
|
|
name: "windows newline at the end of first valid json input",
|
|
input: "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"test\",\"params\":{}}\r\n",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "batching old protocol",
|
|
input: `[{"jsonrpc":"2.0","id":1,"method":"test1"},{"jsonrpc":"2.0","id":2,"method":"test2"}]`,
|
|
want: "",
|
|
protocolVersion: protocolVersion20241105,
|
|
},
|
|
{
|
|
name: "batching new protocol",
|
|
input: `[{"jsonrpc":"2.0","id":1,"method":"test1"},{"jsonrpc":"2.0","id":2,"method":"test2"}]`,
|
|
want: "JSON-RPC batching is not supported in 2025-06-18 and later (request version: 2025-06-18)",
|
|
protocolVersion: protocolVersion20250618,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tr := newIOConn(rwc{
|
|
rc: io.NopCloser(strings.NewReader(tt.input)),
|
|
})
|
|
t.Cleanup(func() { tr.Close() })
|
|
if tt.protocolVersion != "" {
|
|
tr.sessionUpdated(ServerSessionState{
|
|
InitializeParams: &InitializeParams{
|
|
ProtocolVersion: tt.protocolVersion,
|
|
},
|
|
})
|
|
}
|
|
_, err := tr.Read(context.Background())
|
|
if err == nil && tt.want != "" {
|
|
t.Errorf("ioConn.Read() got nil error but wanted %v", tt.want)
|
|
}
|
|
if err != nil && err.Error() != tt.want {
|
|
t.Errorf("ioConn.Read() = %v, want %v", err.Error(), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|