mcp: fix conformance tests (#132)

- Use new AddXXX API.

- Update goldens to reflect new capability logic.

Fixes #131.
This commit is contained in:
Jonathan Amsterdam 2025-07-14 12:47:58 -04:00 • committed by GitHub
commit de4b788e59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 70 additions and 75 deletions

View file

@ -23,6 +23,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"golang.org/x/tools/txtar" "golang.org/x/tools/txtar"
) )
@ -46,12 +47,12 @@ var update = flag.Bool("update", false, "if set, update conformance test data")
// with -update to have the test runner update the expected output, which may // with -update to have the test runner update the expected output, which may
// be client or server depending on the perspective of the test. // be client or server depending on the perspective of the test.
type conformanceTest struct { type conformanceTest struct {
name string // test name name string // test name
path string // path to test file path string // path to test file
archive *txtar.Archive // raw archive, for updating archive *txtar.Archive // raw archive, for updating
tools, prompts, resources []string // named features to include tools, prompts, resources []string // named features to include
client []JSONRPCMessage // client messages client []jsonrpc.Message // client messages
server []JSONRPCMessage // server messages server []jsonrpc.Message // server messages
} }
// TODO(rfindley): add client conformance tests. // TODO(rfindley): add client conformance tests.
@ -100,10 +101,36 @@ func TestServerConformance(t *testing.T) {
func runServerTest(t *testing.T, test *conformanceTest) { func runServerTest(t *testing.T, test *conformanceTest) {
ctx := t.Context() ctx := t.Context()
// Construct the server based on features listed in the test. // Construct the server based on features listed in the test.
s := NewServer("testServer", "v1.0.0", nil) s := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
add(tools, s.AddTools, test.tools...) for _, tn := range test.tools {
add(prompts, s.AddPrompts, test.prompts...) switch tn {
add(resources, s.AddResources, test.resources...) case "greet":
AddTool(s, &Tool{
Name: "greet",
Description: "say hi",
}, sayHi)
default:
t.Fatalf("unknown tool %q", tn)
}
}
for _, pn := range test.prompts {
switch pn {
case "code_review":
s.AddPrompt(codeReviewPrompt, codReviewPromptHandler)
default:
t.Fatalf("unknown prompt %q", pn)
}
}
for _, rn := range test.resources {
switch rn {
case "info.txt":
s.AddResource(resource1, readHandler)
case "info":
s.AddResource(resource3, handleEmbeddedResource)
default:
t.Fatalf("unknown resource %q", rn)
}
}
// Connect the server, and connect the client stream, // Connect the server, and connect the client stream,
// but don't connect an actual client. // but don't connect an actual client.
@ -117,24 +144,24 @@ func runServerTest(t *testing.T, test *conformanceTest) {
t.Fatal(err) t.Fatal(err)
} }
writeMsg := func(msg JSONRPCMessage) { writeMsg := func(msg jsonrpc.Message) {
if err := cStream.Write(ctx, msg); err != nil { if err := cStream.Write(ctx, msg); err != nil {
t.Fatalf("Write failed: %v", err) t.Fatalf("Write failed: %v", err)
} }
} }
var ( var (
serverMessages []JSONRPCMessage serverMessages []jsonrpc.Message
outRequests []*JSONRPCRequest outRequests []*jsonrpc.Request
outResponses []*JSONRPCResponse outResponses []*jsonrpc.Response
) )
// Separate client requests and responses; we use them differently. // Separate client requests and responses; we use them differently.
for _, msg := range test.client { for _, msg := range test.client {
switch msg := msg.(type) { switch msg := msg.(type) {
case *JSONRPCRequest: case *jsonrpc.Request:
outRequests = append(outRequests, msg) outRequests = append(outRequests, msg)
case *JSONRPCResponse: case *jsonrpc.Response:
outResponses = append(outResponses, msg) outResponses = append(outResponses, msg)
default: default:
t.Fatalf("bad message type %T", msg) t.Fatalf("bad message type %T", msg)
@ -143,7 +170,7 @@ func runServerTest(t *testing.T, test *conformanceTest) {
// nextResponse handles incoming requests and notifications, and returns the // nextResponse handles incoming requests and notifications, and returns the
// next incoming response. // next incoming response.
nextResponse := func() (*JSONRPCResponse, error, bool) { nextResponse := func() (*jsonrpc.Response, error, bool) {
for { for {
msg, err := cStream.Read(ctx) msg, err := cStream.Read(ctx)
if err != nil { if err != nil {
@ -156,7 +183,7 @@ func runServerTest(t *testing.T, test *conformanceTest) {
return nil, err, false return nil, err, false
} }
serverMessages = append(serverMessages, msg) serverMessages = append(serverMessages, msg)
if req, ok := msg.(*JSONRPCRequest); ok && req.ID.IsValid() { if req, ok := msg.(*jsonrpc.Request); ok && req.ID.IsValid() {
// Pair up the next outgoing response with this request. // Pair up the next outgoing response with this request.
// We assume requests arrive in the same order every time. // We assume requests arrive in the same order every time.
if len(outResponses) == 0 { if len(outResponses) == 0 {
@ -167,7 +194,7 @@ func runServerTest(t *testing.T, test *conformanceTest) {
outResponses = outResponses[1:] outResponses = outResponses[1:]
continue continue
} }
return msg.(*JSONRPCResponse), nil, true return msg.(*jsonrpc.Response), nil, true
} }
} }
@ -191,7 +218,7 @@ func runServerTest(t *testing.T, test *conformanceTest) {
// There might be more notifications or requests, but there shouldn't be more // There might be more notifications or requests, but there shouldn't be more
// responses. // responses.
// Run this in a goroutine so the current thread can wait for it. // Run this in a goroutine so the current thread can wait for it.
var extra *JSONRPCResponse var extra *jsonrpc.Response
go func() { go func() {
extra, err, _ = nextResponse() extra, err, _ = nextResponse()
}() }()
@ -240,8 +267,8 @@ func runServerTest(t *testing.T, test *conformanceTest) {
t.Fatalf("os.WriteFile(%q) failed: %v", test.path, err) t.Fatalf("os.WriteFile(%q) failed: %v", test.path, err)
} }
} else { } else {
// JSONRPCMessages are not comparable, so we instead compare lines of JSON. // jsonrpc.Messages are not comparable, so we instead compare lines of JSON.
transform := cmpopts.AcyclicTransformer("toJSON", func(msg JSONRPCMessage) []string { transform := cmpopts.AcyclicTransformer("toJSON", func(msg jsonrpc.Message) []string {
encoded, err := jsonrpc2.EncodeIndent(msg, "", "\t") encoded, err := jsonrpc2.EncodeIndent(msg, "", "\t")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -271,9 +298,9 @@ func loadConformanceTest(dir, path string) (*conformanceTest, error) {
} }
// decodeMessages loads JSON-RPC messages from the archive file. // decodeMessages loads JSON-RPC messages from the archive file.
decodeMessages := func(data []byte) ([]JSONRPCMessage, error) { decodeMessages := func(data []byte) ([]jsonrpc.Message, error) {
dec := json.NewDecoder(bytes.NewReader(data)) dec := json.NewDecoder(bytes.NewReader(data))
var res []JSONRPCMessage var res []jsonrpc.Message
for dec.More() { for dec.More() {
var raw json.RawMessage var raw json.RawMessage
if err := dec.Decode(&raw); err != nil { if err := dec.Decode(&raw); err != nil {

View file

@ -39,6 +39,21 @@ func sayHi(ctx context.Context, ss *ServerSession, params *CallToolParamsFor[hiP
return &CallToolResultFor[any]{Content: []Content{&TextContent{Text: "hi " + params.Arguments.Name}}}, nil return &CallToolResultFor[any]{Content: []Content{&TextContent{Text: "hi " + params.Arguments.Name}}}, nil
} }
var codeReviewPrompt = &Prompt{
Name: "code_review",
Description: "do a code review",
Arguments: []*PromptArgument{{Name: "Code", Required: true}},
}
func codReviewPromptHandler(_ context.Context, _ *ServerSession, params *GetPromptParams) (*GetPromptResult, error) {
return &GetPromptResult{
Description: "Code review prompt",
Messages: []*PromptMessage{
{Role: "user", Content: &TextContent{Text: "Please review the following code: " + params.Arguments["Code"]}},
},
}, nil
}
func TestEndToEnd(t *testing.T) { func TestEndToEnd(t *testing.T) {
ctx := context.Background() ctx := context.Background()
var ct, st Transport = NewInMemoryTransports() var ct, st Transport = NewInMemoryTransports()
@ -73,18 +88,7 @@ func TestEndToEnd(t *testing.T) {
func(context.Context, *ServerSession, *CallToolParamsFor[map[string]any]) (*CallToolResult, error) { func(context.Context, *ServerSession, *CallToolParamsFor[map[string]any]) (*CallToolResult, error) {
return nil, errTestFailure return nil, errTestFailure
}) })
s.AddPrompt(&Prompt{ s.AddPrompt(codeReviewPrompt, codReviewPromptHandler)
Name: "code_review",
Description: "do a code review",
Arguments: []*PromptArgument{{Name: "Code", Required: true}},
}, func(_ context.Context, _ *ServerSession, params *GetPromptParams) (*GetPromptResult, error) {
return &GetPromptResult{
Description: "Code review prompt",
Messages: []*PromptMessage{
{Role: "user", Content: &TextContent{Text: "Please review the following code: " + params.Arguments["Code"]}},
},
}, nil
})
s.AddPrompt(&Prompt{Name: "fail"}, func(_ context.Context, _ *ServerSession, _ *GetPromptParams) (*GetPromptResult, error) { s.AddPrompt(&Prompt{Name: "fail"}, func(_ context.Context, _ *ServerSession, _ *GetPromptParams) (*GetPromptResult, error) {
return nil, errTestFailure return nil, errTestFailure
}) })

View file

@ -29,12 +29,6 @@ code_review
"logging": {}, "logging": {},
"prompts": { "prompts": {
"listChanged": true "listChanged": true
},
"resources": {
"listChanged": true
},
"tools": {
"listChanged": true
} }
}, },
"protocolVersion": "2024-11-05", "protocolVersion": "2024-11-05",

View file

@ -47,14 +47,8 @@ info.txt
"capabilities": { "capabilities": {
"completions": {}, "completions": {},
"logging": {}, "logging": {},
"prompts": {
"listChanged": true
},
"resources": { "resources": {
"listChanged": true "listChanged": true
},
"tools": {
"listChanged": true
} }
}, },
"protocolVersion": "2024-11-05", "protocolVersion": "2024-11-05",

View file

@ -30,12 +30,6 @@ greet
"capabilities": { "capabilities": {
"completions": {}, "completions": {},
"logging": {}, "logging": {},
"prompts": {
"listChanged": true
},
"resources": {
"listChanged": true
},
"tools": { "tools": {
"listChanged": true "listChanged": true
} }

View file

@ -19,18 +19,9 @@ response with its latest supported version.
"result": { "result": {
"capabilities": { "capabilities": {
"completions": {}, "completions": {},
"logging": {}, "logging": {}
"prompts": {
"listChanged": true
},
"resources": {
"listChanged": true
},
"tools": {
"listChanged": true
}
}, },
"protocolVersion": "2025-03-26", "protocolVersion": "2025-06-18",
"serverInfo": { "serverInfo": {
"name": "testServer", "name": "testServer",
"version": "v1.0.0" "version": "v1.0.0"

View file

@ -19,16 +19,7 @@ support.
"result": { "result": {
"capabilities": { "capabilities": {
"completions": {}, "completions": {},
"logging": {}, "logging": {}
"prompts": {
"listChanged": true
},
"resources": {
"listChanged": true
},
"tools": {
"listChanged": true
}
}, },
"protocolVersion": "2024-11-05", "protocolVersion": "2024-11-05",
"serverInfo": { "serverInfo": {