Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions internal/jsonrpc2/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,20 @@ func EncodeIndent(msg Message, prefix, indent string) ([]byte, error) {
return bytes.TrimRight(buf.Bytes(), "\n"), nil
}

// wireDecode is the decode form of [wireCombined]. Method is a [json.RawMessage]
// so we can tell whether the "method" key was present on the wire, including
// when its value is the empty string (see go-sdk#976).
type wireDecode struct {
VersionTag string `json:"jsonrpc"`
ID any `json:"id,omitempty"`
Method json.RawMessage `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *WireError `json:"error,omitempty"`
}

func DecodeMessage(data []byte) (Message, error) {
msg := wireCombined{}
msg := wireDecode{}
if err := internaljson.Unmarshal(data, &msg); err != nil {
return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err)
}
Expand All @@ -183,15 +195,19 @@ func DecodeMessage(data []byte) (Message, error) {
if err != nil {
return nil, err
}
if msg.Method != "" {
// has a method, must be a call
if len(msg.Method) > 0 {
// The "method" key was present. Decode its value (including "").
var method string
if err := internaljson.Unmarshal(msg.Method, &method); err != nil {
return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err)
}
return &Request{
Method: msg.Method,
Method: method,
ID: id,
Params: msg.Params,
}, nil
}
// no method, should be a response
// no method key, should be a response
if !id.IsValid() {
return nil, ErrInvalidRequest
}
Expand Down
45 changes: 45 additions & 0 deletions internal/jsonrpc2/wire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,51 @@ func newResponse(id any, result any, rerr error) jsonrpc2.Message {
return msg
}

// go-sdk#976: empty method must decode as a request (encode omits empty method).
func TestDecodeEmptyMethodRequest(t *testing.T) {
encoded := []byte(`{"jsonrpc":"2.0","id":5,"method":"","params":{}}`)
msg, err := jsonrpc2.DecodeMessage(encoded)
if err != nil {
t.Fatal(err)
}
req, ok := msg.(*jsonrpc2.Request)
if !ok {
t.Fatalf("message type = %T, want *jsonrpc2.Request", msg)
}
if req.Method != "" {
t.Errorf("Method = %q, want empty string", req.Method)
}
if req.ID != jsonrpc2.Int64ID(5) {
t.Errorf("ID = %v, want 5", req.ID.Raw())
}
if !req.IsCall() {
t.Error("empty method with id=5 should be a call")
}
}

func TestDecodeResponseUnchanged(t *testing.T) {
encoded := []byte(`{"jsonrpc":"2.0","id":2,"result":{}}`)
msg, err := jsonrpc2.DecodeMessage(encoded)
if err != nil {
t.Fatal(err)
}
if _, ok := msg.(*jsonrpc2.Response); !ok {
t.Fatalf("message type = %T, want *jsonrpc2.Response", msg)
}
}

// Messages with an id but no "method" key are responses, not malformed requests.
func TestDecodeIDOnlyMessageIsResponse(t *testing.T) {
encoded := []byte(`{"jsonrpc":"2.0","id":5}`)
msg, err := jsonrpc2.DecodeMessage(encoded)
if err != nil {
t.Fatal(err)
}
if _, ok := msg.(*jsonrpc2.Response); !ok {
t.Fatalf("message type = %T, want *jsonrpc2.Response", msg)
}
}

func checkJSON(t *testing.T, got, want []byte) {
// compare the compact form, to allow for formatting differences
g := &bytes.Buffer{}
Expand Down
23 changes: 23 additions & 0 deletions mcp/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,26 @@ func TestIOConnRead(t *testing.T) {
})
}
}

// go-sdk#976: stdio must surface empty-method calls as requests, not responses.
func TestIOConnRead_EmptyMethod(t *testing.T) {
tr := newIOConn(rwc{
rc: io.NopCloser(strings.NewReader(`{"jsonrpc":"2.0","id":5,"method":"","params":{}}`)),
})
t.Cleanup(func() { tr.Close() })

msg, err := tr.Read(context.Background())
if err != nil {
t.Fatalf("ioConn.Read() error = %v", err)
}
req, ok := msg.(*jsonrpc.Request)
if !ok {
t.Fatalf("message type = %T, want *jsonrpc.Request", msg)
}
if req.Method != "" {
t.Errorf("Method = %q, want empty string", req.Method)
}
if req.ID != jsonrpc2.Int64ID(5) {
t.Errorf("ID = %v, want 5", req.ID.Raw())
}
}