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/cmpopts"
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"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
// be client or server depending on the perspective of the test.
type conformanceTest struct {
name string // test name
path string // path to test file
archive *txtar.Archive // raw archive, for updating
tools, prompts, resources []string // named features to include
client []JSONRPCMessage // client messages
server []JSONRPCMessage // server messages
name string // test name
path string // path to test file
archive *txtar.Archive // raw archive, for updating
tools, prompts, resources []string // named features to include
client []jsonrpc.Message // client messages
server []jsonrpc.Message // server messages
}
// TODO(rfindley): add client conformance tests.
@ -100,10 +101,36 @@ func TestServerConformance(t *testing.T) {
func runServerTest(t *testing.T, test *conformanceTest) {
ctx := t.Context()
// Construct the server based on features listed in the test.
s := NewServer("testServer", "v1.0.0", nil)
add(tools, s.AddTools, test.tools...)
add(prompts, s.AddPrompts, test.prompts...)
add(resources, s.AddResources, test.resources...)
s := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
for _, tn := range test.tools {
switch tn {
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,
// but don't connect an actual client.
@ -117,24 +144,24 @@ func runServerTest(t *testing.T, test *conformanceTest) {
t.Fatal(err)
}
writeMsg := func(msg JSONRPCMessage) {
writeMsg := func(msg jsonrpc.Message) {
if err := cStream.Write(ctx, msg); err != nil {
t.Fatalf("Write failed: %v", err)
}
}
var (
serverMessages []JSONRPCMessage
outRequests []*JSONRPCRequest
outResponses []*JSONRPCResponse
serverMessages []jsonrpc.Message
outRequests []*jsonrpc.Request
outResponses []*jsonrpc.Response
)
// Separate client requests and responses; we use them differently.
for _, msg := range test.client {
switch msg := msg.(type) {
case *JSONRPCRequest:
case *jsonrpc.Request:
outRequests = append(outRequests, msg)
case *JSONRPCResponse:
case *jsonrpc.Response:
outResponses = append(outResponses, msg)
default:
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
// next incoming response.
nextResponse := func() (*JSONRPCResponse, error, bool) {
nextResponse := func() (*jsonrpc.Response, error, bool) {
for {
msg, err := cStream.Read(ctx)
if err != nil {
@ -156,7 +183,7 @@ func runServerTest(t *testing.T, test *conformanceTest) {
return nil, err, false
}
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.
// We assume requests arrive in the same order every time.
if len(outResponses) == 0 {
@ -167,7 +194,7 @@ func runServerTest(t *testing.T, test *conformanceTest) {
outResponses = outResponses[1:]
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
// responses.
// Run this in a goroutine so the current thread can wait for it.
var extra *JSONRPCResponse
var extra *jsonrpc.Response
go func() {
extra, err, _ = nextResponse()
}()
@ -240,8 +267,8 @@ func runServerTest(t *testing.T, test *conformanceTest) {
t.Fatalf("os.WriteFile(%q) failed: %v", test.path, err)
}
} else {
// JSONRPCMessages are not comparable, so we instead compare lines of JSON.
transform := cmpopts.AcyclicTransformer("toJSON", func(msg JSONRPCMessage) []string {
// jsonrpc.Messages are not comparable, so we instead compare lines of JSON.
transform := cmpopts.AcyclicTransformer("toJSON", func(msg jsonrpc.Message) []string {
encoded, err := jsonrpc2.EncodeIndent(msg, "", "\t")
if err != nil {
t.Fatal(err)
@ -271,9 +298,9 @@ func loadConformanceTest(dir, path string) (*conformanceTest, error) {
}
// 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))
var res []JSONRPCMessage
var res []jsonrpc.Message
for dec.More() {
var raw json.RawMessage
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
}
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) {
ctx := context.Background()
var ct, st Transport = NewInMemoryTransports()
@ -73,18 +88,7 @@ func TestEndToEnd(t *testing.T) {
func(context.Context, *ServerSession, *CallToolParamsFor[map[string]any]) (*CallToolResult, error) {
return nil, errTestFailure
})
s.AddPrompt(&Prompt{
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(codeReviewPrompt, codReviewPromptHandler)
s.AddPrompt(&Prompt{Name: "fail"}, func(_ context.Context, _ *ServerSession, _ *GetPromptParams) (*GetPromptResult, error) {
return nil, errTestFailure
})

View file

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

View file

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

View file

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

View file

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

View file

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