go-sdk/mcp/sse_test.go
Christopher Speller 2b07560be0
mcp: fix sseClientConn to use custom HTTP client (#105)
When attempting to create an OAuth authentication on top of the SSE
transport I discovered the sseClientConn was using http.DefaultClient
for requests instead of the custom client provided via
SSEClientTransportOptions.

This change stores the HTTP client in the sseClientConn struct and uses
it for all requests. Also adds test to verify the custom client is
properly used.
2025-07-09 11:54:53 -04:00

187 lines
4.4 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"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestSSEServer(t *testing.T) {
for _, closeServerFirst := range []bool{false, true} {
t.Run(fmt.Sprintf("closeServerFirst=%t", closeServerFirst), func(t *testing.T) {
ctx := context.Background()
server := NewServer("testServer", "v1.0.0", nil)
AddTool(server, &Tool{Name: "greet"}, sayHi)
sseHandler := NewSSEHandler(func(*http.Request) *Server { return server })
conns := make(chan *ServerSession, 1)
sseHandler.onConnection = func(cc *ServerSession) {
select {
case conns <- cc:
default:
}
}
httpServer := httptest.NewServer(sseHandler)
defer httpServer.Close()
var customClientUsed int64
customClient := &http.Client{
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
atomic.AddInt64(&customClientUsed, 1)
return http.DefaultTransport.RoundTrip(req)
}),
}
clientTransport := NewSSEClientTransport(httpServer.URL, &SSEClientTransportOptions{
HTTPClient: customClient,
})
c := NewClient("testClient", "v1.0.0", nil)
cs, err := c.Connect(ctx, clientTransport)
if err != nil {
t.Fatal(err)
}
if err := cs.Ping(ctx, nil); err != nil {
t.Fatal(err)
}
ss := <-conns
gotHi, err := cs.CallTool(ctx, &CallToolParams{
Name: "greet",
Arguments: map[string]any{"Name": "user"},
})
if err != nil {
t.Fatal(err)
}
wantHi := &CallToolResult{
Content: []Content{
&TextContent{Text: "hi user"},
},
}
if diff := cmp.Diff(wantHi, gotHi); diff != "" {
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
}
// Verify that customClient was used
if atomic.LoadInt64(&customClientUsed) == 0 {
t.Error("Expected custom HTTP client to be used, but it wasn't")
}
// Test that closing either end of the connection terminates the other
// end.
if closeServerFirst {
cs.Close()
ss.Wait()
} else {
ss.Close()
cs.Wait()
}
})
}
}
func TestScanEvents(t *testing.T) {
tests := []struct {
name string
input string
want []event
wantErr string
}{
{
name: "simple event",
input: "event: message\nid: 1\ndata: hello\n\n",
want: []event{
{name: "message", id: "1", data: []byte("hello")},
},
},
{
name: "multiple data lines",
input: "data: line 1\ndata: line 2\n\n",
want: []event{
{data: []byte("line 1\nline 2")},
},
},
{
name: "multiple events",
input: "data: first\n\nevent: second\ndata: second\n\n",
want: []event{
{data: []byte("first")},
{name: "second", data: []byte("second")},
},
},
{
name: "no trailing newline",
input: "data: hello",
want: []event{
{data: []byte("hello")},
},
},
{
name: "malformed line",
input: "invalid line\n\n",
wantErr: "malformed line",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := strings.NewReader(tt.input)
var got []event
var err error
for e, err2 := range scanEvents(r) {
if err2 != nil {
err = err2
break
}
got = append(got, e)
}
if tt.wantErr != "" {
if err == nil {
t.Fatalf("scanEvents() got nil error, want error containing %q", tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("scanEvents() error = %q, want containing %q", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("scanEvents() returned unexpected error: %v", err)
}
if len(got) != len(tt.want) {
t.Fatalf("scanEvents() got %d events, want %d", len(got), len(tt.want))
}
for i := range got {
if g, w := got[i].name, tt.want[i].name; g != w {
t.Errorf("event %d: name = %q, want %q", i, g, w)
}
if g, w := got[i].id, tt.want[i].id; g != w {
t.Errorf("event %d: id = %q, want %q", i, g, w)
}
if g, w := string(got[i].data), string(tt.want[i].data); g != w {
t.Errorf("event %d: data = %q, want %q", i, g, w)
}
}
})
}
}
// roundTripperFunc is a helper to create a custom RoundTripper
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}