Skip to content

fix: type mismatch for request/response ID #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 16, 2025
Merged
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
2 changes: 1 addition & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (c *Client) sendRequest(

request := transport.JSONRPCRequest{
JSONRPC: mcp.JSONRPC_VERSION,
ID: id,
ID: mcp.NewRequestId(id),
Method: method,
Params: params,
}
Expand Down
10 changes: 5 additions & 5 deletions client/transport/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ type Interface interface {
}

type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID int64 `json:"id"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
JSONRPC string `json:"jsonrpc"`
ID mcp.RequestId `json:"id"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
}

type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID *int64 `json:"id"`
ID mcp.RequestId `json:"id"`
Result json.RawMessage `json:"result"`
Error *struct {
Code int `json:"code"`
Expand Down
24 changes: 15 additions & 9 deletions client/transport/sse.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type SSE struct {
baseURL *url.URL
endpoint *url.URL
httpClient *http.Client
responses map[int64]chan *JSONRPCResponse
responses map[string]chan *JSONRPCResponse
mu sync.RWMutex
onNotification func(mcp.JSONRPCNotification)
notifyMu sync.RWMutex
Expand Down Expand Up @@ -62,7 +62,7 @@ func NewSSE(baseURL string, options ...ClientOption) (*SSE, error) {
smc := &SSE{
baseURL: parsedURL,
httpClient: &http.Client{},
responses: make(map[int64]chan *JSONRPCResponse),
responses: make(map[string]chan *JSONRPCResponse),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always use string version of the id for lookups to ensure consistency.

endpointChan: make(chan struct{}),
headers: make(map[string]string),
}
Expand Down Expand Up @@ -200,7 +200,7 @@ func (c *SSE) handleSSEEvent(event, data string) {
}

// Handle notification
if baseMessage.ID == nil {
if baseMessage.ID.IsNil() {
var notification mcp.JSONRPCNotification
if err := json.Unmarshal([]byte(data), &notification); err != nil {
return
Expand All @@ -213,14 +213,17 @@ func (c *SSE) handleSSEEvent(event, data string) {
return
}

// Create string key for map lookup
idKey := baseMessage.ID.String()

c.mu.RLock()
ch, ok := c.responses[*baseMessage.ID]
ch, exists := c.responses[idKey]
c.mu.RUnlock()

if ok {
if exists {
ch <- &baseMessage
c.mu.Lock()
delete(c.responses, *baseMessage.ID)
delete(c.responses, idKey)
c.mu.Unlock()
}
}
Expand Down Expand Up @@ -267,14 +270,17 @@ func (c *SSE) SendRequest(
req.Header.Set(k, v)
}

// Create string key for map lookup
idKey := request.ID.String()

// Register response channel
responseChan := make(chan *JSONRPCResponse, 1)
c.mu.Lock()
c.responses[request.ID] = responseChan
c.responses[idKey] = responseChan
c.mu.Unlock()
deleteResponseChan := func() {
c.mu.Lock()
delete(c.responses, request.ID)
delete(c.responses, idKey)
c.mu.Unlock()
}

Expand Down Expand Up @@ -327,7 +333,7 @@ func (c *SSE) Close() error {
for _, ch := range c.responses {
close(ch)
}
c.responses = make(map[int64]chan *JSONRPCResponse)
c.responses = make(map[string]chan *JSONRPCResponse)
c.mu.Unlock()

return nil
Expand Down
51 changes: 35 additions & 16 deletions client/transport/sse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func TestSSE(t *testing.T) {

request := JSONRPCRequest{
JSONRPC: "2.0",
ID: 1,
ID: mcp.NewRequestId(int64(1)),
Method: "debug/echo",
Params: params,
}
Expand All @@ -174,7 +174,7 @@ func TestSSE(t *testing.T) {
// Parse the result to verify echo
var result struct {
JSONRPC string `json:"jsonrpc"`
ID int64 `json:"id"`
ID mcp.RequestId `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
Expand All @@ -187,8 +187,11 @@ func TestSSE(t *testing.T) {
if result.JSONRPC != "2.0" {
t.Errorf("Expected JSONRPC value '2.0', got '%s'", result.JSONRPC)
}
if result.ID != 1 {
t.Errorf("Expected ID 1, got %d", result.ID)
idValue, ok := result.ID.Value().(int64)
if !ok {
t.Errorf("Expected ID to be int64, got %T", result.ID.Value())
Comment on lines +190 to +192
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check the type.

} else if idValue != 1 {
t.Errorf("Expected ID 1, got %d", idValue)
}
if result.Method != "debug/echo" {
t.Errorf("Expected method 'debug/echo', got '%s'", result.Method)
Expand All @@ -211,7 +214,7 @@ func TestSSE(t *testing.T) {
// Prepare a request
request := JSONRPCRequest{
JSONRPC: "2.0",
ID: 3,
ID: mcp.NewRequestId(int64(3)),
Method: "debug/echo",
}

Expand Down Expand Up @@ -292,7 +295,7 @@ func TestSSE(t *testing.T) {
// Each request has a unique ID and payload
request := JSONRPCRequest{
JSONRPC: "2.0",
ID: int64(100 + idx),
ID: mcp.NewRequestId(int64(100 + idx)),
Method: "debug/echo",
Params: map[string]any{
"requestIndex": idx,
Expand All @@ -317,15 +320,25 @@ func TestSSE(t *testing.T) {
continue
}

if responses[i] == nil || responses[i].ID == nil || *responses[i].ID != int64(100+i) {
t.Errorf("Request %d: Expected ID %d, got %v", i, 100+i, responses[i])
if responses[i] == nil {
t.Errorf("Request %d: Response is nil", i)
continue
}

expectedId := int64(100 + i)
idValue, ok := responses[i].ID.Value().(int64)
if !ok {
t.Errorf("Request %d: Expected ID to be int64, got %T", i, responses[i].ID.Value())
continue
} else if idValue != expectedId {
t.Errorf("Request %d: Expected ID %d, got %d", i, expectedId, idValue)
continue
}

// Parse the result to verify echo
var result struct {
JSONRPC string `json:"jsonrpc"`
ID int64 `json:"id"`
ID mcp.RequestId `json:"id"`
Method string `json:"method"`
Params map[string]any `json:"params"`
}
Expand All @@ -336,8 +349,11 @@ func TestSSE(t *testing.T) {
}

// Verify data matches what was sent
if result.ID != int64(100+i) {
t.Errorf("Request %d: Expected echoed ID %d, got %d", i, 100+i, result.ID)
idValue, ok = result.ID.Value().(int64)
if !ok {
t.Errorf("Request %d: Expected ID to be int64, got %T", i, result.ID.Value())
} else if idValue != int64(100+i) {
t.Errorf("Request %d: Expected echoed ID %d, got %d", i, 100+i, idValue)
}

if result.Method != "debug/echo" {
Expand All @@ -356,7 +372,7 @@ func TestSSE(t *testing.T) {
// Prepare a request
request := JSONRPCRequest{
JSONRPC: "2.0",
ID: 100,
ID: mcp.NewRequestId(int64(100)),
Method: "debug/echo_error_string",
}

Expand All @@ -378,8 +394,11 @@ func TestSSE(t *testing.T) {
if responseError.Method != "debug/echo_error_string" {
t.Errorf("Expected method 'debug/echo_error_string', got '%s'", responseError.Method)
}
if responseError.ID != 100 {
t.Errorf("Expected ID 100, got %d", responseError.ID)
idValue, ok := responseError.ID.Value().(int64)
if !ok {
t.Errorf("Expected ID to be int64, got %T", responseError.ID.Value())
} else if idValue != 100 {
t.Errorf("Expected ID 100, got %d", idValue)
}
if responseError.JSONRPC != "2.0" {
t.Errorf("Expected JSONRPC '2.0', got '%s'", responseError.JSONRPC)
Expand Down Expand Up @@ -453,7 +472,7 @@ func TestSSEErrors(t *testing.T) {
// Prepare a request
request := JSONRPCRequest{
JSONRPC: "2.0",
ID: 99,
ID: mcp.NewRequestId(int64(99)),
Method: "ping",
}

Expand Down Expand Up @@ -492,7 +511,7 @@ func TestSSEErrors(t *testing.T) {
// Try to send a request after close
request := JSONRPCRequest{
JSONRPC: "2.0",
ID: 1,
ID: mcp.NewRequestId(int64(1)),
Method: "ping",
}

Expand Down
24 changes: 15 additions & 9 deletions client/transport/stdio.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Stdio struct {
stdin io.WriteCloser
stdout *bufio.Reader
stderr io.ReadCloser
responses map[int64]chan *JSONRPCResponse
responses map[string]chan *JSONRPCResponse
mu sync.RWMutex
done chan struct{}
onNotification func(mcp.JSONRPCNotification)
Expand All @@ -42,7 +42,7 @@ func NewIO(input io.Reader, output io.WriteCloser, logging io.ReadCloser) *Stdio
stdout: bufio.NewReader(input),
stderr: logging,

responses: make(map[int64]chan *JSONRPCResponse),
responses: make(map[string]chan *JSONRPCResponse),
done: make(chan struct{}),
}
}
Expand All @@ -61,7 +61,7 @@ func NewStdio(
args: args,
env: env,

responses: make(map[int64]chan *JSONRPCResponse),
responses: make(map[string]chan *JSONRPCResponse),
done: make(chan struct{}),
}

Expand Down Expand Up @@ -181,7 +181,7 @@ func (c *Stdio) readResponses() {
}

// Handle notification
if baseMessage.ID == nil {
if baseMessage.ID.IsNil() {
var notification mcp.JSONRPCNotification
if err := json.Unmarshal([]byte(line), &notification); err != nil {
continue
Expand All @@ -194,14 +194,17 @@ func (c *Stdio) readResponses() {
continue
}

// Create string key for map lookup
idKey := baseMessage.ID.String()

c.mu.RLock()
ch, ok := c.responses[*baseMessage.ID]
ch, exists := c.responses[idKey]
c.mu.RUnlock()

if ok {
if exists {
ch <- &baseMessage
c.mu.Lock()
delete(c.responses, *baseMessage.ID)
delete(c.responses, idKey)
c.mu.Unlock()
}
}
Expand All @@ -227,14 +230,17 @@ func (c *Stdio) SendRequest(
}
requestBytes = append(requestBytes, '\n')

// Create string key for map lookup
idKey := request.ID.String()

// Register response channel
responseChan := make(chan *JSONRPCResponse, 1)
c.mu.Lock()
c.responses[request.ID] = responseChan
c.responses[idKey] = responseChan
c.mu.Unlock()
deleteResponseChan := func() {
c.mu.Lock()
delete(c.responses, request.ID)
delete(c.responses, idKey)
c.mu.Unlock()
}

Expand Down
Loading