Conformance: introduce @ConditionalOnScenario
- Useful for incoming auth scenarios in Conformance v2 / 2026-07-28 spec Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
b1a7d5fe88
commit
492c859eb8
4 changed files with 116 additions and 4 deletions
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2026-2026 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.conformance.client.condition;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Conditional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition to include beans only when certain scenarios are active / inactive. Checks
|
||||||
|
* the value of the {@code MCP_CONFORMANCE_SCENARIO} environment variable and matches
|
||||||
|
* against {@link #included()} and {@link #excluded()}. Exactly one of these attributes
|
||||||
|
* must be defined.
|
||||||
|
* <p>
|
||||||
|
* Usage: <pre>
|
||||||
|
*
|
||||||
|
* @Configuration
|
||||||
|
* @ConditionalOnScenario(excluded =
|
||||||
|
* {
|
||||||
|
* "auth/pre-registration",
|
||||||
|
* "auth/client-credentials-basic"
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* public class DefaultConfiguration {
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author Daniel Garnier-Moiroux
|
||||||
|
* @see OnScenarioCondition
|
||||||
|
*/
|
||||||
|
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Conditional(OnScenarioCondition.class)
|
||||||
|
public @interface ConditionalOnScenario {
|
||||||
|
|
||||||
|
String[] included() default {};
|
||||||
|
|
||||||
|
String[] excluded() default {};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2026-2026 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.modelcontextprotocol.conformance.client.condition;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||||
|
import org.springframework.context.annotation.ConditionContext;
|
||||||
|
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition implementation for {@link ConditionalOnScenario}.
|
||||||
|
*
|
||||||
|
* @author Daniel Garnier-Moiroux
|
||||||
|
*/
|
||||||
|
class OnScenarioCondition extends SpringBootCondition {
|
||||||
|
|
||||||
|
private static final String ENV_VAR = "MCP_CONFORMANCE_SCENARIO";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||||
|
Map<String, @Nullable Object> attributes = metadata
|
||||||
|
.getAnnotationAttributes(ConditionalOnScenario.class.getName());
|
||||||
|
Assert.state(attributes != null, "'attributes' must not be null");
|
||||||
|
|
||||||
|
String[] included = (String[]) attributes.get("included");
|
||||||
|
String[] excluded = (String[]) attributes.get("excluded");
|
||||||
|
|
||||||
|
boolean hasIncluded = included != null && included.length > 0;
|
||||||
|
boolean hasExcluded = excluded != null && excluded.length > 0;
|
||||||
|
|
||||||
|
Assert.state(hasIncluded ^ hasExcluded,
|
||||||
|
"@ConditionalOnScenario must have exactly one of 'included' or 'excluded' defined");
|
||||||
|
|
||||||
|
String scenario = System.getenv(ENV_VAR);
|
||||||
|
|
||||||
|
if (hasIncluded) {
|
||||||
|
List<String> includedList = Arrays.asList(included);
|
||||||
|
boolean matches = scenario != null && includedList.contains(scenario);
|
||||||
|
ConditionMessage message = ConditionMessage.forCondition(ConditionalOnScenario.class)
|
||||||
|
.because("scenario '" + scenario + "' " + (matches ? "is" : "is not") + " in included list "
|
||||||
|
+ includedList);
|
||||||
|
return matches ? ConditionOutcome.match(message) : ConditionOutcome.noMatch(message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
List<String> excludedList = Arrays.asList(excluded);
|
||||||
|
boolean matches = scenario == null || !excludedList.contains(scenario);
|
||||||
|
ConditionMessage message = ConditionMessage.forCondition(ConditionalOnScenario.class)
|
||||||
|
.because("scenario '" + scenario + "' " + (matches ? "is not" : "is") + " in excluded list "
|
||||||
|
+ excludedList);
|
||||||
|
return matches ? ConditionOutcome.match(message) : ConditionOutcome.noMatch(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
package io.modelcontextprotocol.conformance.client.configuration;
|
package io.modelcontextprotocol.conformance.client.configuration;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
|
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
|
||||||
|
import io.modelcontextprotocol.conformance.client.condition.ConditionalOnScenario;
|
||||||
import io.modelcontextprotocol.conformance.client.scenario.DefaultScenario;
|
import io.modelcontextprotocol.conformance.client.scenario.DefaultScenario;
|
||||||
import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
|
import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
|
||||||
import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2CimdHttpClientTransportCustomizer;
|
import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2CimdHttpClientTransportCustomizer;
|
||||||
|
|
@ -16,7 +17,6 @@ import org.springaicommunity.mcp.security.client.sync.oauth2.registration.cimd.M
|
||||||
|
|
||||||
import org.springframework.ai.mcp.customizer.McpClientCustomizer;
|
import org.springframework.ai.mcp.customizer.McpClientCustomizer;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
|
||||||
import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext;
|
import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
@ -26,7 +26,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnExpression("#{environment['MCP_CONFORMANCE_SCENARIO'] != 'auth/pre-registration'}")
|
@ConditionalOnScenario(excluded = { "auth/pre-registration", "auth/client-credentials-basic" })
|
||||||
public class DefaultConfiguration {
|
public class DefaultConfiguration {
|
||||||
|
|
||||||
private final String TEST_CLIENT_ID_URL = "https://conformance-test.local/client-metadata.json";
|
private final String TEST_CLIENT_ID_URL = "https://conformance-test.local/client-metadata.json";
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.conformance.client.configuration;
|
package io.modelcontextprotocol.conformance.client.configuration;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.conformance.client.condition.ConditionalOnScenario;
|
||||||
import io.modelcontextprotocol.conformance.client.scenario.PreRegistrationScenario;
|
import io.modelcontextprotocol.conformance.client.scenario.PreRegistrationScenario;
|
||||||
import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
|
import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
|
||||||
import org.springaicommunity.mcp.security.client.sync.oauth2.metadata.McpMetadataDiscoveryService;
|
import org.springaicommunity.mcp.security.client.sync.oauth2.metadata.McpMetadataDiscoveryService;
|
||||||
import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository;
|
import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
|
|
@ -18,7 +18,7 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(name = "mcp.conformance.scenario", havingValue = "auth/pre-registration")
|
@ConditionalOnScenario(included = { "auth/pre-registration", "auth/client-credentials-basic" })
|
||||||
public class PreRegistrationConfiguration {
|
public class PreRegistrationConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue