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
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Run conformance tests
|
- name: Run conformance tests (full suite)
|
||||||
run: |
|
run: |
|
||||||
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
|
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
|
||||||
--command "go run ./conformance/everything-client" \
|
--command "go run ./conformance/everything-client" \
|
||||||
--suite core \
|
--suite all \
|
||||||
--expected-failures ./conformance/baseline.yml
|
--expected-failures ./conformance/baseline.yml
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,13 @@
|
||||||
server: [] # All tests pass!
|
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() {
|
func init() {
|
||||||
registerScenario("initialize", runBasicClient)
|
registerScenario("initialize", runBasicClient)
|
||||||
registerScenario("tools_call", runToolsCallClient)
|
registerScenario("tools_call", runToolsCallClient)
|
||||||
|
registerScenario("request-metadata", runRequestMetadataClient)
|
||||||
registerScenario("elicitation-sep1034-client-defaults", runElicitationDefaultsClient)
|
registerScenario("elicitation-sep1034-client-defaults", runElicitationDefaultsClient)
|
||||||
registerScenario("sse-retry", runSSERetryClient)
|
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{
|
authScenarios := []string{
|
||||||
"auth/2025-03-26-oauth-metadata-backcompat",
|
"auth/2025-03-26-oauth-metadata-backcompat",
|
||||||
"auth/2025-03-26-oauth-endpoint-fallback",
|
"auth/2025-03-26-oauth-endpoint-fallback",
|
||||||
|
"auth/authorization-server-migration",
|
||||||
"auth/basic-cimd",
|
"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-default",
|
||||||
|
"auth/metadata-issuer-mismatch",
|
||||||
"auth/metadata-var1",
|
"auth/metadata-var1",
|
||||||
"auth/metadata-var2",
|
"auth/metadata-var2",
|
||||||
"auth/metadata-var3",
|
"auth/metadata-var3",
|
||||||
|
"auth/offline-access-not-supported",
|
||||||
|
"auth/offline-access-scope",
|
||||||
"auth/pre-registration",
|
"auth/pre-registration",
|
||||||
"auth/resource-mismatch",
|
"auth/resource-mismatch",
|
||||||
"auth/scope-from-www-authenticate",
|
|
||||||
"auth/scope-from-scopes-supported",
|
"auth/scope-from-scopes-supported",
|
||||||
|
"auth/scope-from-www-authenticate",
|
||||||
"auth/scope-omitted-when-undefined",
|
"auth/scope-omitted-when-undefined",
|
||||||
"auth/scope-step-up",
|
|
||||||
"auth/scope-retry-limit",
|
"auth/scope-retry-limit",
|
||||||
|
"auth/scope-step-up",
|
||||||
"auth/token-endpoint-auth-basic",
|
"auth/token-endpoint-auth-basic",
|
||||||
"auth/token-endpoint-auth-post",
|
|
||||||
"auth/token-endpoint-auth-none",
|
"auth/token-endpoint-auth-none",
|
||||||
"auth/iss-supported",
|
"auth/token-endpoint-auth-post",
|
||||||
"auth/iss-not-advertised",
|
|
||||||
"auth/iss-supported-missing",
|
|
||||||
"auth/iss-wrong-issuer",
|
|
||||||
"auth/iss-unexpected",
|
|
||||||
}
|
}
|
||||||
for _, scenario := range authScenarios {
|
for _, scenario := range authScenarios {
|
||||||
registerScenario(scenario, runAuthClient)
|
registerScenario(scenario, runAuthClient)
|
||||||
|
|
@ -293,6 +304,198 @@ func runAuthClient(ctx context.Context, serverURL string, configCtx map[string]a
|
||||||
return nil
|
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
|
// Main entry point
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue