Skip to content

Commit 1dcf38f

Browse files
mcp: add conformance tests on client (#1063)
This PR integrates the latest conformance tests on client
1 parent 1af1a08 commit 1dcf38f

3 files changed

Lines changed: 225 additions & 11 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ jobs:
6868
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
6969
with:
7070
node-version: 22
71-
- name: Run conformance tests
71+
- name: Run conformance tests (full suite)
7272
run: |
7373
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
7474
--command "go run ./conformance/everything-client" \
75-
--suite core \
75+
--suite all \
7676
--expected-failures ./conformance/baseline.yml

conformance/baseline.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
11
server: [] # All tests pass!
2-
client: [] # All tests pass!
2+
3+
client:
4+
# OAuth mechanisms not yet implemented by the Go MCP SDK.
5+
- auth/client-credentials-basic
6+
- auth/client-credentials-jwt
7+
- auth/enterprise-managed-authorization
8+
9+
# Upstream conformance mock inconsistency: the scenario's hand-rolled
10+
# server/discover handler advertises "2026-07-28" but delegates every
11+
# other method to a pinned @modelcontextprotocol/sdk that rejects that
12+
# version at the HTTP layer.
13+
- json-schema-ref-no-deref

conformance/everything-client/main.go

Lines changed: 211 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,43 @@ func registerScenario(name string, handler scenarioHandler) {
4545
func init() {
4646
registerScenario("initialize", runBasicClient)
4747
registerScenario("tools_call", runToolsCallClient)
48+
registerScenario("request-metadata", runRequestMetadataClient)
4849
registerScenario("elicitation-sep1034-client-defaults", runElicitationDefaultsClient)
4950
registerScenario("sse-retry", runSSERetryClient)
51+
registerScenario("json-schema-ref-no-deref", runJSONSchemaRefNoDerefClient)
52+
registerScenario("sep-2322-client-request-state", runMrtrClient)
53+
registerScenario("http-standard-headers", runHTTPStandardHeadersClient)
54+
registerScenario("http-custom-headers", runHTTPCustomHeadersClient)
55+
registerScenario("http-invalid-tool-headers", runHTTPInvalidToolHeadersClient)
5056

5157
authScenarios := []string{
5258
"auth/2025-03-26-oauth-metadata-backcompat",
5359
"auth/2025-03-26-oauth-endpoint-fallback",
60+
"auth/authorization-server-migration",
5461
"auth/basic-cimd",
62+
"auth/iss-normalized",
63+
"auth/iss-not-advertised",
64+
"auth/iss-supported",
65+
"auth/iss-supported-missing",
66+
"auth/iss-unexpected",
67+
"auth/iss-wrong-issuer",
5568
"auth/metadata-default",
69+
"auth/metadata-issuer-mismatch",
5670
"auth/metadata-var1",
5771
"auth/metadata-var2",
5872
"auth/metadata-var3",
73+
"auth/offline-access-not-supported",
74+
"auth/offline-access-scope",
5975
"auth/pre-registration",
6076
"auth/resource-mismatch",
61-
"auth/scope-from-www-authenticate",
6277
"auth/scope-from-scopes-supported",
78+
"auth/scope-from-www-authenticate",
6379
"auth/scope-omitted-when-undefined",
64-
"auth/scope-step-up",
6580
"auth/scope-retry-limit",
81+
"auth/scope-step-up",
6682
"auth/token-endpoint-auth-basic",
67-
"auth/token-endpoint-auth-post",
6883
"auth/token-endpoint-auth-none",
69-
"auth/iss-supported",
70-
"auth/iss-not-advertised",
71-
"auth/iss-supported-missing",
72-
"auth/iss-wrong-issuer",
73-
"auth/iss-unexpected",
84+
"auth/token-endpoint-auth-post",
7485
}
7586
for _, scenario := range authScenarios {
7687
registerScenario(scenario, runAuthClient)
@@ -293,6 +304,198 @@ func runAuthClient(ctx context.Context, serverURL string, configCtx map[string]a
293304
return nil
294305
}
295306

307+
// ============================================================================
308+
// request-metadata scenario (SEP-2575)
309+
// ============================================================================
310+
311+
// runRequestMetadataClient exercises the SEP-2575 wire-level negotiation:
312+
// every request must carry the MCP-Protocol-Version header and the per-request
313+
// _meta envelope, and the client must retry with a supported version when its
314+
// first choice is rejected with -32022. The Go SDK's Connect() drives
315+
// server/discover unconditionally for 2026-07-28, which is exactly that
316+
// mechanism.
317+
func runRequestMetadataClient(ctx context.Context, serverURL string, _ map[string]any) error {
318+
session, err := connectToServer(ctx, serverURL)
319+
if err != nil {
320+
return err
321+
}
322+
defer session.Close()
323+
return nil
324+
}
325+
326+
// ============================================================================
327+
// json-schema-ref-no-deref scenario (SEP-2106)
328+
// ============================================================================
329+
330+
// runJSONSchemaRefNoDerefClient asserts that listTools does not dereference
331+
// network $ref URLs in tool schemas. The Go SDK never fetches external refs;
332+
// a plain connect → listTools is sufficient — if the SDK ever regressed and
333+
// tried to GET the canary URL the conformance referee would record the
334+
// failure.
335+
func runJSONSchemaRefNoDerefClient(ctx context.Context, serverURL string, _ map[string]any) error {
336+
session, err := connectToServer(ctx, serverURL)
337+
if err != nil {
338+
return err
339+
}
340+
defer session.Close()
341+
342+
_, _ = session.ListTools(ctx, nil)
343+
return nil
344+
}
345+
346+
// ============================================================================
347+
// SEP-2322 multi-round-trip client scenario
348+
// ============================================================================
349+
350+
// runMrtrClient drives the SEP-2322 client-request-state scenario. The
351+
// SDK's built-in client-side MRTR middleware transparently fulfills embedded
352+
// elicitation requests via ElicitationHandler and echoes requestState.
353+
func runMrtrClient(ctx context.Context, serverURL string, _ map[string]any) error {
354+
elicitationHandler := func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
355+
return &mcp.ElicitResult{
356+
Action: "accept",
357+
Content: map[string]any{"confirmed": true},
358+
}, nil
359+
}
360+
session, err := connectToServer(ctx, serverURL,
361+
withClientOptions(&mcp.ClientOptions{
362+
Capabilities: &mcp.ClientCapabilities{Elicitation: &mcp.ElicitationCapabilities{}},
363+
ElicitationHandler: elicitationHandler,
364+
}),
365+
)
366+
if err != nil {
367+
return err
368+
}
369+
defer session.Close()
370+
371+
toolNames := []string{
372+
"test_mrtr_echo_state",
373+
"test_mrtr_no_state",
374+
"test_mrtr_unrelated",
375+
"test_mrtr_no_result_type",
376+
}
377+
for _, name := range toolNames {
378+
if _, err := session.CallTool(ctx, &mcp.CallToolParams{
379+
Name: name,
380+
Arguments: map[string]any{},
381+
}); err != nil {
382+
log.Printf("CallTool(%q) rejected locally (expected for some cases): %v", name, err)
383+
}
384+
}
385+
return nil
386+
}
387+
388+
// ============================================================================
389+
// SEP-2243 HTTP header scenarios
390+
// ============================================================================
391+
392+
// runHTTPStandardHeadersClient (SEP-2243 standard headers) drives tools,
393+
// resources and prompts once each so the referee sees Mcp-Method / Mcp-Name
394+
// headers on every verb the SDK supports.
395+
func runHTTPStandardHeadersClient(ctx context.Context, serverURL string, _ map[string]any) error {
396+
session, err := connectToServer(ctx, serverURL)
397+
if err != nil {
398+
return err
399+
}
400+
defer session.Close()
401+
402+
if tools, err := session.ListTools(ctx, nil); err == nil && len(tools.Tools) > 0 {
403+
_, _ = session.CallTool(ctx, &mcp.CallToolParams{
404+
Name: tools.Tools[0].Name,
405+
Arguments: map[string]any{},
406+
})
407+
}
408+
if res, err := session.ListResources(ctx, nil); err == nil && len(res.Resources) > 0 {
409+
_, _ = session.ReadResource(ctx, &mcp.ReadResourceParams{URI: res.Resources[0].URI})
410+
}
411+
if prompts, err := session.ListPrompts(ctx, nil); err == nil && len(prompts.Prompts) > 0 {
412+
_, _ = session.GetPrompt(ctx, &mcp.GetPromptParams{
413+
Name: prompts.Prompts[0].Name,
414+
Arguments: map[string]string{},
415+
})
416+
}
417+
return nil
418+
}
419+
420+
// runHTTPCustomHeadersClient (SEP-2243 custom headers) lists tools (so the
421+
// SDK caches inputSchema and its x-mcp-header annotations), then makes the
422+
// runner-supplied tool calls so the referee validates the Mcp-Param-*
423+
// headers.
424+
func runHTTPCustomHeadersClient(ctx context.Context, serverURL string, configCtx map[string]any) error {
425+
session, err := connectToServer(ctx, serverURL)
426+
if err != nil {
427+
return err
428+
}
429+
defer session.Close()
430+
431+
if _, err := session.ListTools(ctx, nil); err != nil {
432+
return fmt.Errorf("session.ListTools(): %v", err)
433+
}
434+
for _, call := range readToolCallsContext(configCtx) {
435+
if _, err := session.CallTool(ctx, &mcp.CallToolParams{
436+
Name: call.Name,
437+
Arguments: call.Arguments,
438+
}); err != nil {
439+
log.Printf("CallTool(%q): %v", call.Name, err)
440+
}
441+
}
442+
return nil
443+
}
444+
445+
// runHTTPInvalidToolHeadersClient (SEP-2243 invalid-tool filtering) lists
446+
// tools — a correct SDK leaves only the valid ones — then calls each survivor
447+
// so the referee records SUCCESS for every excluded tool never called.
448+
func runHTTPInvalidToolHeadersClient(ctx context.Context, serverURL string, _ map[string]any) error {
449+
session, err := connectToServer(ctx, serverURL)
450+
if err != nil {
451+
return err
452+
}
453+
defer session.Close()
454+
455+
tools, err := session.ListTools(ctx, nil)
456+
if err != nil {
457+
return fmt.Errorf("session.ListTools(): %v", err)
458+
}
459+
for _, tool := range tools.Tools {
460+
_, err := session.CallTool(ctx, &mcp.CallToolParams{
461+
Name: tool.Name,
462+
Arguments: map[string]any{"region": "us-west1"},
463+
})
464+
if err != nil {
465+
log.Printf("call %q rejected: %v", tool.Name, err)
466+
}
467+
}
468+
return nil
469+
}
470+
471+
type toolCall struct {
472+
Name string
473+
Arguments map[string]any
474+
}
475+
476+
// readToolCallsContext parses the `toolCalls` array from MCP_CONFORMANCE_CONTEXT.
477+
// Format: {"toolCalls": [{"name":"...", "arguments":{...}}, ...]}.
478+
func readToolCallsContext(configCtx map[string]any) []toolCall {
479+
raw, ok := configCtx["toolCalls"].([]any)
480+
if !ok {
481+
return nil
482+
}
483+
out := make([]toolCall, 0, len(raw))
484+
for _, item := range raw {
485+
obj, ok := item.(map[string]any)
486+
if !ok {
487+
continue
488+
}
489+
name, _ := obj["name"].(string)
490+
args, _ := obj["arguments"].(map[string]any)
491+
if args == nil {
492+
args = map[string]any{}
493+
}
494+
out = append(out, toolCall{Name: name, Arguments: args})
495+
}
496+
return out
497+
}
498+
296499
// ============================================================================
297500
// Main entry point
298501
// ============================================================================

0 commit comments

Comments
 (0)