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;
|
||||
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue