mcp: add conformance tests on client (#1063)

This PR integrates the latest conformance tests on client
This commit is contained in:
Guglielmo Colombo 2026-07-09 10:48:07 +02:00 • committed by GitHub
commit 1dcf38f76f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 225 additions and 11 deletions

View file

@ -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

View file

@ -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

View file

@ -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
// ============================================================================