mcp: fix conformance tests (#132)
- Use new AddXXX API. - Update goldens to reflect new capability logic. Fixes #131.
This commit is contained in:
parent
bf7e518215
commit
de4b788e59
7 changed files with 70 additions and 75 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
6
mcp/testdata/conformance/server/tools.txtar
vendored
6
mcp/testdata/conformance/server/tools.txtar
vendored
|
|
@ -30,12 +30,6 @@ greet
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"completions": {},
|
"completions": {},
|
||||||
"logging": {},
|
"logging": {},
|
||||||
"prompts": {
|
|
||||||
"listChanged": true
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"listChanged": true
|
|
||||||
},
|
|
||||||
"tools": {
|
"tools": {
|
||||||
"listChanged": true
|
"listChanged": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue