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:
Daniel Garnier-Moiroux 2026-06-24 12:12:57 +02:00 • committed by Daniel Garnier-Moiroux
commit 492c859eb8
4 changed files with 116 additions and 4 deletions

View file

@ -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>
*
* &#64;Configuration
* &#64;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 {};
}

View file

@ -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);
}
}
}

View file

@ -5,6 +5,7 @@
package io.modelcontextprotocol.conformance.client.configuration;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.conformance.client.condition.ConditionalOnScenario;
import io.modelcontextprotocol.conformance.client.scenario.DefaultScenario;
import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
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.beans.factory.annotation.Value;
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;
@ -26,7 +26,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@ConditionalOnExpression("#{environment['MCP_CONFORMANCE_SCENARIO'] != 'auth/pre-registration'}")
@ConditionalOnScenario(excluded = { "auth/pre-registration", "auth/client-credentials-basic" })
public class DefaultConfiguration {
private final String TEST_CLIENT_ID_URL = "https://conformance-test.local/client-metadata.json";

View file

@ -4,12 +4,12 @@
package io.modelcontextprotocol.conformance.client.configuration;
import io.modelcontextprotocol.conformance.client.condition.ConditionalOnScenario;
import io.modelcontextprotocol.conformance.client.scenario.PreRegistrationScenario;
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.registration.McpClientRegistrationRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
@ -18,7 +18,7 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@ConditionalOnProperty(name = "mcp.conformance.scenario", havingValue = "auth/pre-registration")
@ConditionalOnScenario(included = { "auth/pre-registration", "auth/client-credentials-basic" })
public class PreRegistrationConfiguration {
@Bean