Skip to content

Commit 492c859

Browse files
committed
Conformance: introduce @ConditionalOnScenario
- Useful for incoming auth scenarios in Conformance v2 / 2026-07-28 spec Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent b1a7d5f commit 492c859

4 files changed

Lines changed: 116 additions & 4 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.conformance.client.condition;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
import org.springframework.context.annotation.Conditional;
14+
15+
/**
16+
* Condition to include beans only when certain scenarios are active / inactive. Checks
17+
* the value of the {@code MCP_CONFORMANCE_SCENARIO} environment variable and matches
18+
* against {@link #included()} and {@link #excluded()}. Exactly one of these attributes
19+
* must be defined.
20+
* <p>
21+
* Usage: <pre>
22+
*
23+
* &#64;Configuration
24+
* &#64;ConditionalOnScenario(excluded =
25+
* {
26+
* "auth/pre-registration",
27+
* "auth/client-credentials-basic"
28+
* }
29+
* )
30+
* public class DefaultConfiguration {
31+
* // ...
32+
* }
33+
* </pre>
34+
*
35+
* @author Daniel Garnier-Moiroux
36+
* @see OnScenarioCondition
37+
*/
38+
@Target({ ElementType.TYPE, ElementType.METHOD })
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Documented
41+
@Conditional(OnScenarioCondition.class)
42+
public @interface ConditionalOnScenario {
43+
44+
String[] included() default {};
45+
46+
String[] excluded() default {};
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.conformance.client.condition;
6+
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
import org.jspecify.annotations.Nullable;
12+
13+
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
14+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
15+
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
16+
import org.springframework.context.annotation.ConditionContext;
17+
import org.springframework.core.type.AnnotatedTypeMetadata;
18+
import org.springframework.util.Assert;
19+
20+
/**
21+
* Condition implementation for {@link ConditionalOnScenario}.
22+
*
23+
* @author Daniel Garnier-Moiroux
24+
*/
25+
class OnScenarioCondition extends SpringBootCondition {
26+
27+
private static final String ENV_VAR = "MCP_CONFORMANCE_SCENARIO";
28+
29+
@Override
30+
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
31+
Map<String, @Nullable Object> attributes = metadata
32+
.getAnnotationAttributes(ConditionalOnScenario.class.getName());
33+
Assert.state(attributes != null, "'attributes' must not be null");
34+
35+
String[] included = (String[]) attributes.get("included");
36+
String[] excluded = (String[]) attributes.get("excluded");
37+
38+
boolean hasIncluded = included != null && included.length > 0;
39+
boolean hasExcluded = excluded != null && excluded.length > 0;
40+
41+
Assert.state(hasIncluded ^ hasExcluded,
42+
"@ConditionalOnScenario must have exactly one of 'included' or 'excluded' defined");
43+
44+
String scenario = System.getenv(ENV_VAR);
45+
46+
if (hasIncluded) {
47+
List<String> includedList = Arrays.asList(included);
48+
boolean matches = scenario != null && includedList.contains(scenario);
49+
ConditionMessage message = ConditionMessage.forCondition(ConditionalOnScenario.class)
50+
.because("scenario '" + scenario + "' " + (matches ? "is" : "is not") + " in included list "
51+
+ includedList);
52+
return matches ? ConditionOutcome.match(message) : ConditionOutcome.noMatch(message);
53+
}
54+
else {
55+
List<String> excludedList = Arrays.asList(excluded);
56+
boolean matches = scenario == null || !excludedList.contains(scenario);
57+
ConditionMessage message = ConditionMessage.forCondition(ConditionalOnScenario.class)
58+
.because("scenario '" + scenario + "' " + (matches ? "is not" : "is") + " in excluded list "
59+
+ excludedList);
60+
return matches ? ConditionOutcome.match(message) : ConditionOutcome.noMatch(message);
61+
}
62+
}
63+
64+
}

conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/DefaultConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.modelcontextprotocol.conformance.client.configuration;
66

77
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
8+
import io.modelcontextprotocol.conformance.client.condition.ConditionalOnScenario;
89
import io.modelcontextprotocol.conformance.client.scenario.DefaultScenario;
910
import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
1011
import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2CimdHttpClientTransportCustomizer;
@@ -16,7 +17,6 @@
1617

1718
import org.springframework.ai.mcp.customizer.McpClientCustomizer;
1819
import org.springframework.beans.factory.annotation.Value;
19-
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
2020
import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext;
2121
import org.springframework.context.annotation.Bean;
2222
import org.springframework.context.annotation.Configuration;
@@ -26,7 +26,7 @@
2626
import org.springframework.security.web.SecurityFilterChain;
2727

2828
@Configuration
29-
@ConditionalOnExpression("#{environment['MCP_CONFORMANCE_SCENARIO'] != 'auth/pre-registration'}")
29+
@ConditionalOnScenario(excluded = { "auth/pre-registration", "auth/client-credentials-basic" })
3030
public class DefaultConfiguration {
3131

3232
private final String TEST_CLIENT_ID_URL = "https://conformance-test.local/client-metadata.json";

conformance-tests/client-spring-http-client/src/main/java/io/modelcontextprotocol/conformance/client/configuration/PreRegistrationConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
package io.modelcontextprotocol.conformance.client.configuration;
66

7+
import io.modelcontextprotocol.conformance.client.condition.ConditionalOnScenario;
78
import io.modelcontextprotocol.conformance.client.scenario.PreRegistrationScenario;
89
import org.springaicommunity.mcp.security.client.sync.config.McpClientOAuth2Configurer;
910
import org.springaicommunity.mcp.security.client.sync.oauth2.metadata.McpMetadataDiscoveryService;
1011
import org.springaicommunity.mcp.security.client.sync.oauth2.registration.McpClientRegistrationRepository;
1112

12-
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1313
import org.springframework.context.annotation.Bean;
1414
import org.springframework.context.annotation.Configuration;
1515
import org.springframework.security.config.Customizer;
@@ -18,7 +18,7 @@
1818
import org.springframework.security.web.SecurityFilterChain;
1919

2020
@Configuration
21-
@ConditionalOnProperty(name = "mcp.conformance.scenario", havingValue = "auth/pre-registration")
21+
@ConditionalOnScenario(included = { "auth/pre-registration", "auth/client-credentials-basic" })
2222
public class PreRegistrationConfiguration {
2323

2424
@Bean

0 commit comments

Comments
 (0)