Skip to content

Commit acbf379

Browse files
Piyush BagPiyush Bag
authored andcommitted
fix(jsonrpc2): respond to requests with empty method names
Decode empty-method JSON-RPC messages as requests instead of responses so stdio servers return -32601 instead of dropping them. Fixes #976
1 parent dd97816 commit acbf379

3 files changed

Lines changed: 60 additions & 2 deletions

File tree

internal/jsonrpc2/messages.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,10 @@ func DecodeMessage(data []byte) (Message, error) {
183183
if err != nil {
184184
return nil, err
185185
}
186-
if msg.Method != "" {
187-
// has a method, must be a call
186+
if msg.Method != "" || (msg.Result == nil && msg.Error == nil && (msg.Params != nil || id.IsValid())) {
187+
// Requests carry a method (including the empty string, which is invalid
188+
// but must still be dispatched so the peer receives a JSON-RPC error).
189+
// Responses carry result or error without a method.
188190
return &Request{
189191
Method: msg.Method,
190192
ID: id,

internal/jsonrpc2/wire_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,39 @@ func newResponse(id any, result any, rerr error) jsonrpc2.Message {
102102
return msg
103103
}
104104

105+
// go-sdk#976: empty method must decode as a request (encode omits empty method).
106+
func TestDecodeEmptyMethodRequest(t *testing.T) {
107+
encoded := []byte(`{"jsonrpc":"2.0","id":5,"method":"","params":{}}`)
108+
msg, err := jsonrpc2.DecodeMessage(encoded)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
req, ok := msg.(*jsonrpc2.Request)
113+
if !ok {
114+
t.Fatalf("message type = %T, want *jsonrpc2.Request", msg)
115+
}
116+
if req.Method != "" {
117+
t.Errorf("Method = %q, want empty string", req.Method)
118+
}
119+
if req.ID != jsonrpc2.Int64ID(5) {
120+
t.Errorf("ID = %v, want 5", req.ID.Raw())
121+
}
122+
if !req.IsCall() {
123+
t.Error("empty method with id=5 should be a call")
124+
}
125+
}
126+
127+
func TestDecodeResponseUnchanged(t *testing.T) {
128+
encoded := []byte(`{"jsonrpc":"2.0","id":2,"result":{}}`)
129+
msg, err := jsonrpc2.DecodeMessage(encoded)
130+
if err != nil {
131+
t.Fatal(err)
132+
}
133+
if _, ok := msg.(*jsonrpc2.Response); !ok {
134+
t.Fatalf("message type = %T, want *jsonrpc2.Response", msg)
135+
}
136+
}
137+
105138
func checkJSON(t *testing.T, got, want []byte) {
106139
// compare the compact form, to allow for formatting differences
107140
g := &bytes.Buffer{}

mcp/transport_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,26 @@ func TestIOConnRead(t *testing.T) {
124124
})
125125
}
126126
}
127+
128+
// go-sdk#976: stdio must surface empty-method calls as requests, not responses.
129+
func TestIOConnRead_EmptyMethod(t *testing.T) {
130+
tr := newIOConn(rwc{
131+
rc: io.NopCloser(strings.NewReader(`{"jsonrpc":"2.0","id":5,"method":"","params":{}}`)),
132+
})
133+
t.Cleanup(func() { tr.Close() })
134+
135+
msg, err := tr.Read(context.Background())
136+
if err != nil {
137+
t.Fatalf("ioConn.Read() error = %v", err)
138+
}
139+
req, ok := msg.(*jsonrpc.Request)
140+
if !ok {
141+
t.Fatalf("message type = %T, want *jsonrpc.Request", msg)
142+
}
143+
if req.Method != "" {
144+
t.Errorf("Method = %q, want empty string", req.Method)
145+
}
146+
if req.ID != jsonrpc2.Int64ID(5) {
147+
t.Errorf("ID = %v, want 5", req.ID.Raw())
148+
}
149+
}

0 commit comments

Comments
 (0)