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.
187 lines
4.4 KiB
Go
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)
|
|
}
|