Skip to content
Open
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 examples/server/auth-middleware/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
)

require (
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
)
Expand Down
1 change: 1 addition & 0 deletions examples/server/auth-middleware/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
Expand Down
2 changes: 1 addition & 1 deletion examples/server/rate-limiting/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
)

require (
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
)

Expand Down
1 change: 1 addition & 0 deletions examples/server/rate-limiting/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
Expand Down
57 changes: 57 additions & 0 deletions mcp/cmd_go125_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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.

//go:build go1.25

package mcp_test

import (
"context"
"errors"
"testing"
"testing/synctest"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

func TestServerRunContextCancel(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

serverTransport, clientTransport := mcp.NewInMemoryTransports()

// run the server and capture the exit error
onServerExit := make(chan error)
go func() {
onServerExit <- server.Run(ctx, serverTransport)
}()

// send a ping to the server to ensure it's running
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
session, err := client.Connect(ctx, clientTransport, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { session.Close() })

if err := session.Ping(context.Background(), nil); err != nil {
t.Fatal(err)
}

// cancel the context to stop the server
cancel()

// wait for the server to exit
// synctest will detect deadlock if server doesn't exit
synctest.Wait()
err = <-onServerExit
if !errors.Is(err, context.Canceled) {
t.Fatalf("server did not exit after context cancellation, got error: %v", err)
}
})
}
42 changes: 0 additions & 42 deletions mcp/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,48 +76,6 @@ func runCancelContextServer() {
}
}

func TestServerRunContextCancel(t *testing.T) {
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

serverTransport, clientTransport := mcp.NewInMemoryTransports()

// run the server and capture the exit error
onServerExit := make(chan error)
go func() {
onServerExit <- server.Run(ctx, serverTransport)
}()

// send a ping to the server to ensure it's running
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
session, err := client.Connect(ctx, clientTransport, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { session.Close() })

if err := session.Ping(context.Background(), nil); err != nil {
t.Fatal(err)
}

// cancel the context to stop the server
cancel()

// wait for the server to exit
// TODO: use synctest when availble
select {
case <-time.After(5 * time.Second):
t.Fatal("server did not exit after context cancellation")
case err := <-onServerExit:
if !errors.Is(err, context.Canceled) {
t.Fatalf("server did not exit after context cancellation, got error: %v", err)
}
}
}

func TestServerInterrupt(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("requires POSIX signals")
Expand Down
71 changes: 71 additions & 0 deletions mcp/elicitation_go125_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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.

//go:build go1.25

package mcp

import (
"context"
"testing"
"testing/synctest"
)

func TestElicitationCompleteNotification(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx := context.Background()

var elicitationCompleteCh = make(chan *ElicitationCompleteParams, 1)

c := NewClient(testImpl, &ClientOptions{
Capabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept"}, nil
},
ElicitationCompleteHandler: func(_ context.Context, req *ElicitationCompleteNotificationRequest) {
elicitationCompleteCh <- req.Params
},
})

cs, ss, cleanup := basicClientServerConnection(t, c, nil, nil)
_ = cs // Dummy usage to avoid "declared and not used" error.
defer cleanup()

// 1. Server initiates a URL elicitation
elicitID := "testElicitationID-123"
resp, err := ss.Elicit(ctx, &ElicitParams{
Mode: "url",
Message: "Please complete this form: ",
URL: "https://example.com/form?id=" + elicitID,
ElicitationID: elicitID,
})
if err != nil {
t.Fatalf("Elicit failed: %v", err)
}
if resp.Action != "accept" {
t.Fatalf("Elicit action is %q, want %q", resp.Action, "accept")
}

// 2. Server sends elicitation complete notification (simulating out-of-band completion)
err = handleNotify(ctx, notificationElicitationComplete, newServerRequest(ss, &ElicitationCompleteParams{
ElicitationID: elicitID,
}))
if err != nil {
t.Fatalf("failed to send elicitation complete notification: %v", err)
}

// 3. Client should receive the notification
synctest.Wait()
gotParams := <-elicitationCompleteCh
if gotParams.ElicitationID != elicitID {
t.Errorf("elicitationComplete notification ID mismatch: got %q, want %q", gotParams.ElicitationID, elicitID)
}
})
}
60 changes: 0 additions & 60 deletions mcp/elicitation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"errors"
"strings"
"testing"
"time"

"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
Expand Down Expand Up @@ -142,62 +141,3 @@ func TestElicitationURLMode(t *testing.T) {
})
}
}

func TestElicitationCompleteNotification(t *testing.T) {
ctx := context.Background()

var elicitationCompleteCh = make(chan *ElicitationCompleteParams, 1)

c := NewClient(testImpl, &ClientOptions{
Capabilities: &ClientCapabilities{
Roots: RootCapabilities{ListChanged: true},
RootsV2: &RootCapabilities{ListChanged: true},
Elicitation: &ElicitationCapabilities{
URL: &URLElicitationCapabilities{},
},
},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept"}, nil
},
ElicitationCompleteHandler: func(_ context.Context, req *ElicitationCompleteNotificationRequest) {
elicitationCompleteCh <- req.Params
},
})

cs, ss, cleanup := basicClientServerConnection(t, c, nil, nil)
_ = cs // Dummy usage to avoid "declared and not used" error.
defer cleanup()

// 1. Server initiates a URL elicitation
elicitID := "testElicitationID-123"
resp, err := ss.Elicit(ctx, &ElicitParams{
Mode: "url",
Message: "Please complete this form: ",
URL: "https://example.com/form?id=" + elicitID,
ElicitationID: elicitID,
})
if err != nil {
t.Fatalf("Elicit failed: %v", err)
}
if resp.Action != "accept" {
t.Fatalf("Elicit action is %q, want %q", resp.Action, "accept")
}

// 2. Server sends elicitation complete notification (simulating out-of-band completion)
err = handleNotify(ctx, notificationElicitationComplete, newServerRequest(ss, &ElicitationCompleteParams{
ElicitationID: elicitID,
}))
if err != nil {
t.Fatalf("failed to send elicitation complete notification: %v", err)
}

// 3. Client should receive the notification
select {
case gotParams := <-elicitationCompleteCh:
if gotParams.ElicitationID != elicitID {
t.Errorf("elicitationComplete notification ID mismatch: got %q, want %q", gotParams.ElicitationID, elicitID)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for elicitation complete notification")
}
}
Loading