From 0a2cc864c50d403e45017743ffeb8d4d2ddfe24f Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 31 Mar 2026 16:32:11 +0200 Subject: [PATCH] conformance: update to mcp-security 0.1.5, pass scope-step-up Signed-off-by: Daniel Garnier-Moiroux --- conformance-tests/VALIDATION_RESULTS.md | 37 +++++++++---------- .../client-spring-http-client/README.md | 12 +++--- .../client-spring-http-client/pom.xml | 14 +++++-- .../ConformanceSpringClientApplication.java | 14 ++++++- .../client/McpClientController.java | 12 ++++++ .../configuration/DefaultConfiguration.java | 12 +++--- .../client/scenario/DefaultScenario.java | 27 ++++++++++---- .../scenario/PreRegistrationScenario.java | 2 +- conformance-tests/conformance-baseline.yml | 2 - 9 files changed, 86 insertions(+), 46 deletions(-) diff --git a/conformance-tests/VALIDATION_RESULTS.md b/conformance-tests/VALIDATION_RESULTS.md index 8edc7ad71..e4ce396bc 100644 --- a/conformance-tests/VALIDATION_RESULTS.md +++ b/conformance-tests/VALIDATION_RESULTS.md @@ -4,7 +4,7 @@ **Server Tests:** 40/40 passed (100%) **Client Tests:** 3/4 scenarios passed (9/10 checks passed) -**Auth Tests:** 12/14 scenarios fully passing (178 passed, 1 failed, 1 warning, 85.7% scenarios, 98.9% checks) +**Auth Tests:** 14/15 scenarios fully passing (196 passed, 0 failed, 1 warning, 93.3% scenarios, 99.5% checks) ## Server Test Results @@ -37,35 +37,35 @@ ## Auth Test Results (Spring HTTP Client) -**Status: 178 passed, 1 failed, 1 warning across 14 scenarios** +**Status: 196 passed, 0 failed, 1 warning across 15 scenarios** Uses the `client-spring-http-client` module with Spring Security OAuth2 and the [mcp-client-security](https://github.com/springaicommunity/mcp-client-security) library. -### Fully Passing (12/14 scenarios) +### Fully Passing (14/15 scenarios) -- **auth/metadata-default (12/12):** Default metadata discovery -- **auth/metadata-var1 (12/12):** Metadata discovery variant 1 -- **auth/metadata-var2 (12/12):** Metadata discovery variant 2 -- **auth/metadata-var3 (12/12):** Metadata discovery variant 3 -- **auth/scope-from-www-authenticate (13/13):** Scope extraction from WWW-Authenticate header -- **auth/scope-from-scopes-supported (13/13):** Scope extraction from scopes_supported -- **auth/scope-omitted-when-undefined (13/13):** Scope omitted when not defined +- **auth/metadata-default (13/13):** Default metadata discovery +- **auth/metadata-var1 (13/13):** Metadata discovery variant 1 +- **auth/metadata-var2 (13/13):** Metadata discovery variant 2 +- **auth/metadata-var3 (13/13):** Metadata discovery variant 3 +- **auth/scope-from-www-authenticate (14/14):** Scope extraction from WWW-Authenticate header +- **auth/scope-from-scopes-supported (14/14):** Scope extraction from scopes_supported +- **auth/scope-omitted-when-undefined (14/14):** Scope omitted when not defined +- **auth/scope-step-up (16/16):** Scope step-up challenge - **auth/scope-retry-limit (11/11):** Scope retry limit handling -- **auth/token-endpoint-auth-basic (17/17):** Token endpoint with HTTP Basic auth -- **auth/token-endpoint-auth-post (17/17):** Token endpoint with POST body auth -- **auth/token-endpoint-auth-none (17/17):** Token endpoint with no client auth +- **auth/token-endpoint-auth-basic (18/18):** Token endpoint with HTTP Basic auth +- **auth/token-endpoint-auth-post (18/18):** Token endpoint with POST body auth +- **auth/token-endpoint-auth-none (18/18):** Token endpoint with no client auth +- **auth/resource-mismatch (2/2):** Resource mismatch handling - **auth/pre-registration (6/6):** Pre-registered client credentials flow -### Partially Passing (2/14 scenarios) +### Partially Passing (1/15 scenarios) -- **auth/basic-cimd (12/12 + 1 warning):** Basic Client-Initiated Metadata Discovery — all checks pass, minor warning -- **auth/scope-step-up (11/12):** Scope step-up challenge — 1 failure, client does not fully handle scope escalation after initial authorization +- **auth/basic-cimd (13/13 + 1 warning):** Basic Client-Initiated Metadata Discovery — all checks pass, minor warning ## Known Limitations 1. **Client SSE Retry:** Client doesn't parse or respect the `retry:` field, reconnects immediately, and doesn't send Last-Event-ID header -2. **Auth Scope Step-Up:** Client does not fully handle scope step-up challenges where the server requests additional scopes after initial authorization -3. **Auth Basic CIMD:** Minor conformance warning in the basic Client-Initiated Metadata Discovery flow +2. **Auth Basic CIMD:** Minor conformance warning in the basic Client-Initiated Metadata Discovery flow ## Running Tests @@ -113,4 +113,3 @@ npx @modelcontextprotocol/conformance@0.1.15 client \ ### High Priority 1. Fix client SSE retry field handling in `HttpClientStreamableHttpTransport` 2. Implement CIMD -3. Implement scope step up diff --git a/conformance-tests/client-spring-http-client/README.md b/conformance-tests/client-spring-http-client/README.md index afbf64773..e5ed016c3 100644 --- a/conformance-tests/client-spring-http-client/README.md +++ b/conformance-tests/client-spring-http-client/README.md @@ -26,7 +26,7 @@ Test with @modelcontextprotocol/conformance@0.1.15. | auth/scope-from-www-authenticate | ✅ Pass | 13/13 | | auth/scope-from-scopes-supported | ✅ Pass | 13/13 | | auth/scope-omitted-when-undefined | ✅ Pass | 13/13 | -| auth/scope-step-up | ❌ Fail | 11/12 (1 failed) | +| auth/scope-step-up | ✅ Pass | 12/12 | | auth/scope-retry-limit | ✅ Pass | 11/11 | | auth/token-endpoint-auth-basic | ✅ Pass | 17/17 | | auth/token-endpoint-auth-post | ✅ Pass | 17/17 | @@ -67,7 +67,7 @@ cd conformance-tests/client-spring-http-client This creates an executable JAR at: ``` -target/client-spring-http-client-1.1.0-SNAPSHOT.jar +target/client-spring-http-client-2.0.0-SNAPSHOT.jar ``` ## Running Tests @@ -79,7 +79,7 @@ Run the full auth suite: ```bash npx @modelcontextprotocol/conformance@0.1.15 client \ --spec-version 2025-11-25 \ - --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-1.1.0-SNAPSHOT.jar" \ + --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-2.0.0-SNAPSHOT.jar" \ --suite auth ``` @@ -88,7 +88,7 @@ Run a single scenario: ```bash npx @modelcontextprotocol/conformance@0.1.15 client \ --spec-version 2025-11-25 \ - --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-1.1.0-SNAPSHOT.jar" \ + --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-2.0.0-SNAPSHOT.jar" \ --scenario auth/metadata-default ``` @@ -97,7 +97,7 @@ Run with verbose output: ```bash npx @modelcontextprotocol/conformance@0.1.15 client \ --spec-version 2025-11-25 \ - --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-1.1.0-SNAPSHOT.jar" \ + --command "java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-2.0.0-SNAPSHOT.jar" \ --scenario auth/metadata-default \ --verbose ``` @@ -108,7 +108,7 @@ You can also run the client manually if you have a test server: ```bash export MCP_CONFORMANCE_SCENARIO=auth/metadata-default -java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-1.1.0-SNAPSHOT.jar http://localhost:3000/mcp +java -jar conformance-tests/client-spring-http-client/target/client-spring-http-client-2.0.0-SNAPSHOT.jar http://localhost:3000/mcp ``` ## Known Issues diff --git a/conformance-tests/client-spring-http-client/pom.xml b/conformance-tests/client-spring-http-client/pom.xml index 06b53887d..44aa7f925 100644 --- a/conformance-tests/client-spring-http-client/pom.xml +++ b/conformance-tests/client-spring-http-client/pom.xml @@ -22,8 +22,9 @@ 17 - 4.0.2 - 2.0.0-M2 + 4.0.5 + 2.0.0-M4 + 0.1.5 true @@ -64,7 +65,12 @@ org.springaicommunity mcp-client-security - 0.1.2 + ${spring-ai-mcp-security.version} + + + io.modelcontextprotocol.sdk + mcp-core + ${project.version} @@ -106,4 +112,4 @@ - \ No newline at end of file + diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceSpringClientApplication.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceSpringClientApplication.java index 00582c9f2..63c3601f0 100644 --- a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceSpringClientApplication.java +++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/ConformanceSpringClientApplication.java @@ -8,8 +8,11 @@ import io.modelcontextprotocol.conformance.client.scenario.Scenario; import org.springaicommunity.mcp.security.client.sync.oauth2.metadata.McpMetadataDiscoveryService; +import org.springaicommunity.mcp.security.client.sync.oauth2.registration.DefaultMcpOAuth2ClientManager; import org.springaicommunity.mcp.security.client.sync.oauth2.registration.DynamicClientRegistrationService; import org.springaicommunity.mcp.security.client.sync.oauth2.registration.InMemoryMcpClientRegistrationRepository; +import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository; +import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpOAuth2ClientManager; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; @@ -49,8 +52,15 @@ McpMetadataDiscoveryService discovery() { } @Bean - InMemoryMcpClientRegistrationRepository clientRegistrationRepository(McpMetadataDiscoveryService discovery) { - return new InMemoryMcpClientRegistrationRepository(new DynamicClientRegistrationService(), discovery); + McpClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryMcpClientRegistrationRepository(); + } + + @Bean + McpOAuth2ClientManager mcpOAuth2ClientManager(McpClientRegistrationRepository mcpClientRegistrationRepository, + McpMetadataDiscoveryService mcpMetadataDiscoveryService) { + return new DefaultMcpOAuth2ClientManager(mcpClientRegistrationRepository, + new DynamicClientRegistrationService(), mcpMetadataDiscoveryService); } @Bean diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/McpClientController.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/McpClientController.java index e02cfd416..1b1910298 100644 --- a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/McpClientController.java +++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/McpClientController.java @@ -5,6 +5,7 @@ package io.modelcontextprotocol.conformance.client; import io.modelcontextprotocol.conformance.client.scenario.Scenario; +import io.modelcontextprotocol.spec.McpSchema; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -27,4 +28,15 @@ public String execute() { return "OK"; } + @GetMapping("/tools-list") + public String toolsList() { + return "OK"; + } + + @GetMapping("/tools-call") + public String toolsCall() { + this.scenario.getMcpClient().callTool(McpSchema.CallToolRequest.builder().name("test-tool").build()); + return "OK"; + } + } diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java index 12a9c4a5c..febd0f461 100644 --- a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java +++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java @@ -8,15 +8,16 @@ import io.modelcontextprotocol.conformance.client.scenario.DefaultScenario; import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer; import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository; +import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpOAuth2ClientManager; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.web.SecurityFilterChain; -import static io.modelcontextprotocol.conformance.client.ConformanceSpringClientApplication.REGISTRATION_ID; @Configuration @ConditionalOnExpression("#{environment['MCP_CONFORMANCE_SCENARIO'] != 'auth/pre-registration'}") @@ -25,15 +26,16 @@ public class DefaultConfiguration { @Bean DefaultScenario defaultScenario(McpClientRegistrationRepository clientRegistrationRepository, ServletWebServerApplicationContext serverCtx, - OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository) { - return new DefaultScenario(clientRegistrationRepository, serverCtx, oAuth2AuthorizedClientRepository); + OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository, + McpOAuth2ClientManager mcpOAuth2ClientManager) { + return new DefaultScenario(clientRegistrationRepository, serverCtx, oAuth2AuthorizedClientRepository, + mcpOAuth2ClientManager); } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http, ConformanceSpringClientApplication.ServerUrl serverUrl) { return http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()) - .with(new McpClientOAuth2Configurer(), - mcp -> mcp.registerMcpOAuth2Client(REGISTRATION_ID, serverUrl.value())) + .with(new McpClientOAuth2Configurer(), Customizer.withDefaults()) .build(); } diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java index 907cea10d..b1fb78a14 100644 --- a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java +++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/DefaultScenario.java @@ -17,15 +17,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springaicommunity.mcp.security.client.sync.AuthenticationMcpTransportContextProvider; -import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2AuthorizationCodeSyncHttpRequestCustomizer; +import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2HttpClientTransportCustomizer; import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository; +import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpOAuth2ClientManager; import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.web.client.RestClient; -import static io.modelcontextprotocol.conformance.client.ConformanceSpringClientApplication.REGISTRATION_ID; +import org.springframework.web.util.UriComponentsBuilder; public class DefaultScenario implements Scenario { @@ -35,12 +36,19 @@ public class DefaultScenario implements Scenario { private final DefaultOAuth2AuthorizedClientManager authorizedClientManager; + private final McpClientRegistrationRepository clientRegistrationRepository; + + private final McpOAuth2ClientManager mcpOAuth2ClientManager; + private McpSyncClient client; public DefaultScenario(McpClientRegistrationRepository clientRegistrationRepository, ServletWebServerApplicationContext serverCtx, - OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository) { + OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository, + McpOAuth2ClientManager mcpOAuth2ClientManager) { this.serverCtx = serverCtx; + this.clientRegistrationRepository = clientRegistrationRepository; + this.mcpOAuth2ClientManager = mcpOAuth2ClientManager; this.authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientRepository); } @@ -51,10 +59,13 @@ public void execute(String serverUrl) { var testServerUrl = "http://localhost:" + serverCtx.getWebServer().getPort(); var testClient = buildTestClient(testServerUrl); - var customizer = new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(authorizedClientManager, REGISTRATION_ID); - HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl) - .httpRequestCustomizer(customizer) - .build(); + var customizer = new OAuth2HttpClientTransportCustomizer(authorizedClientManager, clientRegistrationRepository, + mcpOAuth2ClientManager); + var baseUri = UriComponentsBuilder.fromUriString(serverUrl).replacePath(null).toUriString(); + var path = UriComponentsBuilder.fromUriString(serverUrl).build().getPath(); + var transportBuilder = HttpClientStreamableHttpTransport.builder(baseUri).endpoint(path); + customizer.customize("default-transport", transportBuilder); + HttpClientStreamableHttpTransport transport = transportBuilder.build(); this.client = McpClient.sync(transport) .transportContextProvider(new AuthenticationMcpTransportContextProvider()) @@ -64,6 +75,8 @@ public void execute(String serverUrl) { try { testClient.get().uri("/initialize-mcp-client").retrieve().toBodilessEntity(); + testClient.get().uri("/tools-list").retrieve().toBodilessEntity(); + testClient.get().uri("/tools-call").retrieve().toBodilessEntity(); } finally { // Close the client (which will close the transport) diff --git a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java index 8e6bbe228..accb7862a 100644 --- a/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java +++ b/conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/scenario/PreRegistrationScenario.java @@ -87,7 +87,7 @@ private void setClientRegistration(String mcpServerUrl, PreRegistrationContext o .clientSecret(oauthCredentials.clientSecret()) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .build(); - clientRegistrationRepository.addPreRegisteredClient(registration, + clientRegistrationRepository.addClientRegistration(registration, metadata.protectedResourceMetadata().resource()); } diff --git a/conformance-tests/conformance-baseline.yml b/conformance-tests/conformance-baseline.yml index d2990c155..37cdb3110 100644 --- a/conformance-tests/conformance-baseline.yml +++ b/conformance-tests/conformance-baseline.yml @@ -9,5 +9,3 @@ client: - sse-retry # CIMD not implemented yet - auth/basic-cimd - # Scope step up beyond initial authorization request not implemented - - auth/scope-step-up