diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index cca700d..feb5000 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -68,9 +68,9 @@ jobs: uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: 22 - - name: Run conformance tests + - name: Run conformance tests (full suite) run: | npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \ --command "go run ./conformance/everything-client" \ - --suite core \ + --suite all \ --expected-failures ./conformance/baseline.yml diff --git a/conformance/baseline.yml b/conformance/baseline.yml index 9d7bef8..49f68b6 100644 --- a/conformance/baseline.yml +++ b/conformance/baseline.yml @@ -1,2 +1,13 @@ server: [] # All tests pass! -client: [] # All tests pass! + +client: + # OAuth mechanisms not yet implemented by the Go MCP SDK. + - auth/client-credentials-basic + - auth/client-credentials-jwt + - auth/enterprise-managed-authorization + + # Upstream conformance mock inconsistency: the scenario's hand-rolled + # server/discover handler advertises "2026-07-28" but delegates every + # other method to a pinned @modelcontextprotocol/sdk that rejects that + # version at the HTTP layer. + - json-schema-ref-no-deref diff --git a/conformance/everything-client/main.go b/conformance/everything-client/main.go index 55a55ad..3776522 100644 --- a/conformance/everything-client/main.go +++ b/conformance/everything-client/main.go @@ -45,32 +45,43 @@ func registerScenario(name string, handler scenarioHandler) { func init() { registerScenario("initialize", runBasicClient) registerScenario("tools_call", runToolsCallClient) + registerScenario("request-metadata", runRequestMetadataClient) registerScenario("elicitation-sep1034-client-defaults", runElicitationDefaultsClient) registerScenario("sse-retry", runSSERetryClient) + registerScenario("json-schema-ref-no-deref", runJSONSchemaRefNoDerefClient) + registerScenario("sep-2322-client-request-state", runMrtrClient) + registerScenario("http-standard-headers", runHTTPStandardHeadersClient) + registerScenario("http-custom-headers", runHTTPCustomHeadersClient) + registerScenario("http-invalid-tool-headers", runHTTPInvalidToolHeadersClient) authScenarios := []string{ "auth/2025-03-26-oauth-metadata-backcompat", "auth/2025-03-26-oauth-endpoint-fallback", + "auth/authorization-server-migration", "auth/basic-cimd", + "auth/iss-normalized", + "auth/iss-not-advertised", + "auth/iss-supported", + "auth/iss-supported-missing", + "auth/iss-unexpected", + "auth/iss-wrong-issuer", "auth/metadata-default", + "auth/metadata-issuer-mismatch", "auth/metadata-var1", "auth/metadata-var2", "auth/metadata-var3", + "auth/offline-access-not-supported", + "auth/offline-access-scope", "auth/pre-registration", "auth/resource-mismatch", - "auth/scope-from-www-authenticate", "auth/scope-from-scopes-supported", + "auth/scope-from-www-authenticate", "auth/scope-omitted-when-undefined", - "auth/scope-step-up", "auth/scope-retry-limit", + "auth/scope-step-up", "auth/token-endpoint-auth-basic", - "auth/token-endpoint-auth-post", "auth/token-endpoint-auth-none", - "auth/iss-supported", - "auth/iss-not-advertised", - "auth/iss-supported-missing", - "auth/iss-wrong-issuer", - "auth/iss-unexpected", + "auth/token-endpoint-auth-post", } for _, scenario := range authScenarios { registerScenario(scenario, runAuthClient) @@ -293,6 +304,198 @@ func runAuthClient(ctx context.Context, serverURL string, configCtx map[string]a return nil } +// ============================================================================ +// request-metadata scenario (SEP-2575) +// ============================================================================ + +// runRequestMetadataClient exercises the SEP-2575 wire-level negotiation: +// every request must carry the MCP-Protocol-Version header and the per-request +// _meta envelope, and the client must retry with a supported version when its +// first choice is rejected with -32022. The Go SDK's Connect() drives +// server/discover unconditionally for 2026-07-28, which is exactly that +// mechanism. +func runRequestMetadataClient(ctx context.Context, serverURL string, _ map[string]any) error { + session, err := connectToServer(ctx, serverURL) + if err != nil { + return err + } + defer session.Close() + return nil +} + +// ============================================================================ +// json-schema-ref-no-deref scenario (SEP-2106) +// ============================================================================ + +// runJSONSchemaRefNoDerefClient asserts that listTools does not dereference +// network $ref URLs in tool schemas. The Go SDK never fetches external refs; +// a plain connect → listTools is sufficient — if the SDK ever regressed and +// tried to GET the canary URL the conformance referee would record the +// failure. +func runJSONSchemaRefNoDerefClient(ctx context.Context, serverURL string, _ map[string]any) error { + session, err := connectToServer(ctx, serverURL) + if err != nil { + return err + } + defer session.Close() + + _, _ = session.ListTools(ctx, nil) + return nil +} + +// ============================================================================ +// SEP-2322 multi-round-trip client scenario +// ============================================================================ + +// runMrtrClient drives the SEP-2322 client-request-state scenario. The +// SDK's built-in client-side MRTR middleware transparently fulfills embedded +// elicitation requests via ElicitationHandler and echoes requestState. +func runMrtrClient(ctx context.Context, serverURL string, _ map[string]any) error { + elicitationHandler := func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) { + return &mcp.ElicitResult{ + Action: "accept", + Content: map[string]any{"confirmed": true}, + }, nil + } + session, err := connectToServer(ctx, serverURL, + withClientOptions(&mcp.ClientOptions{ + Capabilities: &mcp.ClientCapabilities{Elicitation: &mcp.ElicitationCapabilities{}}, + ElicitationHandler: elicitationHandler, + }), + ) + if err != nil { + return err + } + defer session.Close() + + toolNames := []string{ + "test_mrtr_echo_state", + "test_mrtr_no_state", + "test_mrtr_unrelated", + "test_mrtr_no_result_type", + } + for _, name := range toolNames { + if _, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: name, + Arguments: map[string]any{}, + }); err != nil { + log.Printf("CallTool(%q) rejected locally (expected for some cases): %v", name, err) + } + } + return nil +} + +// ============================================================================ +// SEP-2243 HTTP header scenarios +// ============================================================================ + +// runHTTPStandardHeadersClient (SEP-2243 standard headers) drives tools, +// resources and prompts once each so the referee sees Mcp-Method / Mcp-Name +// headers on every verb the SDK supports. +func runHTTPStandardHeadersClient(ctx context.Context, serverURL string, _ map[string]any) error { + session, err := connectToServer(ctx, serverURL) + if err != nil { + return err + } + defer session.Close() + + if tools, err := session.ListTools(ctx, nil); err == nil && len(tools.Tools) > 0 { + _, _ = session.CallTool(ctx, &mcp.CallToolParams{ + Name: tools.Tools[0].Name, + Arguments: map[string]any{}, + }) + } + if res, err := session.ListResources(ctx, nil); err == nil && len(res.Resources) > 0 { + _, _ = session.ReadResource(ctx, &mcp.ReadResourceParams{URI: res.Resources[0].URI}) + } + if prompts, err := session.ListPrompts(ctx, nil); err == nil && len(prompts.Prompts) > 0 { + _, _ = session.GetPrompt(ctx, &mcp.GetPromptParams{ + Name: prompts.Prompts[0].Name, + Arguments: map[string]string{}, + }) + } + return nil +} + +// runHTTPCustomHeadersClient (SEP-2243 custom headers) lists tools (so the +// SDK caches inputSchema and its x-mcp-header annotations), then makes the +// runner-supplied tool calls so the referee validates the Mcp-Param-* +// headers. +func runHTTPCustomHeadersClient(ctx context.Context, serverURL string, configCtx map[string]any) error { + session, err := connectToServer(ctx, serverURL) + if err != nil { + return err + } + defer session.Close() + + if _, err := session.ListTools(ctx, nil); err != nil { + return fmt.Errorf("session.ListTools(): %v", err) + } + for _, call := range readToolCallsContext(configCtx) { + if _, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: call.Name, + Arguments: call.Arguments, + }); err != nil { + log.Printf("CallTool(%q): %v", call.Name, err) + } + } + return nil +} + +// runHTTPInvalidToolHeadersClient (SEP-2243 invalid-tool filtering) lists +// tools — a correct SDK leaves only the valid ones — then calls each survivor +// so the referee records SUCCESS for every excluded tool never called. +func runHTTPInvalidToolHeadersClient(ctx context.Context, serverURL string, _ map[string]any) error { + session, err := connectToServer(ctx, serverURL) + if err != nil { + return err + } + defer session.Close() + + tools, err := session.ListTools(ctx, nil) + if err != nil { + return fmt.Errorf("session.ListTools(): %v", err) + } + for _, tool := range tools.Tools { + _, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: tool.Name, + Arguments: map[string]any{"region": "us-west1"}, + }) + if err != nil { + log.Printf("call %q rejected: %v", tool.Name, err) + } + } + return nil +} + +type toolCall struct { + Name string + Arguments map[string]any +} + +// readToolCallsContext parses the `toolCalls` array from MCP_CONFORMANCE_CONTEXT. +// Format: {"toolCalls": [{"name":"...", "arguments":{...}}, ...]}. +func readToolCallsContext(configCtx map[string]any) []toolCall { + raw, ok := configCtx["toolCalls"].([]any) + if !ok { + return nil + } + out := make([]toolCall, 0, len(raw)) + for _, item := range raw { + obj, ok := item.(map[string]any) + if !ok { + continue + } + name, _ := obj["name"].(string) + args, _ := obj["arguments"].(map[string]any) + if args == nil { + args = map[string]any{} + } + out = append(out, toolCall{Name: name, Arguments: args}) + } + return out +} + // ============================================================================ // Main entry point // ============================================================================