diff --git a/mcp/conformance_test.go b/mcp/conformance_test.go index 3be54a3..883d8a8 100644 --- a/mcp/conformance_test.go +++ b/mcp/conformance_test.go @@ -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 { diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index 1952e8d..7da2b85 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -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 }) diff --git a/mcp/testdata/conformance/server/prompts.txtar b/mcp/testdata/conformance/server/prompts.txtar index 6168ce8..078e091 100644 --- a/mcp/testdata/conformance/server/prompts.txtar +++ b/mcp/testdata/conformance/server/prompts.txtar @@ -29,12 +29,6 @@ code_review "logging": {}, "prompts": { "listChanged": true - }, - "resources": { - "listChanged": true - }, - "tools": { - "listChanged": true } }, "protocolVersion": "2024-11-05", diff --git a/mcp/testdata/conformance/server/resources.txtar b/mcp/testdata/conformance/server/resources.txtar index 3e7031a..5bb5515 100644 --- a/mcp/testdata/conformance/server/resources.txtar +++ b/mcp/testdata/conformance/server/resources.txtar @@ -47,14 +47,8 @@ info.txt "capabilities": { "completions": {}, "logging": {}, - "prompts": { - "listChanged": true - }, "resources": { "listChanged": true - }, - "tools": { - "listChanged": true } }, "protocolVersion": "2024-11-05", diff --git a/mcp/testdata/conformance/server/tools.txtar b/mcp/testdata/conformance/server/tools.txtar index a43cd07..07ad942 100644 --- a/mcp/testdata/conformance/server/tools.txtar +++ b/mcp/testdata/conformance/server/tools.txtar @@ -30,12 +30,6 @@ greet "capabilities": { "completions": {}, "logging": {}, - "prompts": { - "listChanged": true - }, - "resources": { - "listChanged": true - }, "tools": { "listChanged": true } diff --git a/mcp/testdata/conformance/server/version-latest.txtar b/mcp/testdata/conformance/server/version-latest.txtar index 760bf8b..89454fb 100644 --- a/mcp/testdata/conformance/server/version-latest.txtar +++ b/mcp/testdata/conformance/server/version-latest.txtar @@ -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" diff --git a/mcp/testdata/conformance/server/version-older.txtar b/mcp/testdata/conformance/server/version-older.txtar index 97f7b79..5524095 100644 --- a/mcp/testdata/conformance/server/version-older.txtar +++ b/mcp/testdata/conformance/server/version-older.txtar @@ -19,16 +19,7 @@ support. "result": { "capabilities": { "completions": {}, - "logging": {}, - "prompts": { - "listChanged": true - }, - "resources": { - "listChanged": true - }, - "tools": { - "listChanged": true - } + "logging": {} }, "protocolVersion": "2024-11-05", "serverInfo": {