mcp: add conformance tests on client (#1063)
This PR integrates the latest conformance tests on client
This commit is contained in:
parent
1af1a087ef
commit
1dcf38f76f
3 changed files with 225 additions and 11 deletions
4
.github/workflows/conformance.yml
vendored
4
.github/workflows/conformance.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue