Skip to content

Commit c738380

Browse files
committed
chore: run full conformance CI suite
the expected failures may need to be updated based on when this merges
1 parent 61b40af commit c738380

4 files changed

Lines changed: 97 additions & 70 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ concurrency:
1414
env:
1515
# Pinned for reproducible runs; bump deliberately when the suite updates.
1616
CONFORMANCE_VERSION: "0.1.16"
17+
# When updating DRAFT_CONFORMANCE_VERSION, diff
18+
# `conformance list --spec-version 2026-07-28`
19+
# and update #977
1720
DRAFT_CONFORMANCE_VERSION: "0.2.0-alpha.9"
1821

1922
jobs:
2023
server:
2124
runs-on: ubuntu-latest
25+
timeout-minutes: 10
2226
permissions:
2327
contents: read
2428
steps:
@@ -81,7 +85,6 @@ jobs:
8185
echo "draft conformance server did not become ready" >&2
8286
exit 1
8387
84-
# Run discovery separately until #985 enables the full draft suite.
8588
- name: Run SEP-2575 discovery contract
8689
run: |
8790
endpoint=http://127.0.0.1:8002/mcp
@@ -91,6 +94,8 @@ jobs:
9194
-H "Mcp-Method: server/discover"
9295
)
9396
97+
# The stateless server streams responses as SSE, so unwrap the
98+
# `data:` payload before parsing as JSON.
9499
discover_response="$(
95100
curl --fail-with-body --silent --show-error \
96101
"${common_headers[@]}" \
@@ -110,7 +115,8 @@ jobs:
110115
}
111116
}
112117
}' \
113-
"$endpoint"
118+
"$endpoint" \
119+
| sed -n 's/^data: //p'
114120
)"
115121
jq -e '
116122
.result.resultType == "complete" and
@@ -152,35 +158,14 @@ jobs:
152158
(.error.data.supported | index("2026-07-28") != null)
153159
' /tmp/unsupported-version.json
154160
155-
# Keep this explicit list until the full draft suite is enabled by #985.
156-
- name: Run supported draft server scenarios
161+
- name: Run 2026-07-28 server suite
157162
run: |
158-
for scenario in \
159-
sep-2164-resource-not-found \
160-
caching \
161-
http-header-validation \
162-
http-custom-header-server-validation \
163-
input-required-result-basic-elicitation \
164-
input-required-result-basic-sampling \
165-
input-required-result-basic-list-roots \
166-
input-required-result-request-state \
167-
input-required-result-multiple-input-requests \
168-
input-required-result-multi-round \
169-
input-required-result-missing-input-response \
170-
input-required-result-non-tool-request \
171-
input-required-result-result-type \
172-
input-required-result-unsupported-methods \
173-
input-required-result-tampered-state \
174-
input-required-result-capability-check \
175-
input-required-result-ignore-extra-params \
176-
input-required-result-validate-input \
177-
; do
178-
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
179-
--url http://127.0.0.1:8002/mcp \
180-
--scenario "$scenario" \
181-
--spec-version draft \
182-
-o conformance-results
183-
done
163+
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
164+
--url http://127.0.0.1:8002/mcp \
165+
--suite all \
166+
--spec-version 2026-07-28 \
167+
--expected-failures conformance/expected-failures-2026-07-28.yaml \
168+
-o conformance-results
184169
185170
- name: Stop conformance servers
186171
if: always()
@@ -197,6 +182,7 @@ jobs:
197182

198183
client:
199184
runs-on: ubuntu-latest
185+
timeout-minutes: 10
200186
permissions:
201187
contents: read
202188
steps:
@@ -218,21 +204,14 @@ jobs:
218204
--spec-version 2025-11-25 \
219205
-o conformance-client-results/full
220206
221-
# SEP-2322 MRTR client scenario (spec 2026-07-28).
222-
- name: Run draft SEP-2322 client scenario
207+
- name: Run 2026-07-28 client suite
223208
run: |
224209
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" client \
225210
--command "$(pwd)/target/debug/conformance-client" \
226-
--scenario sep-2322-client-request-state \
227-
-o conformance-client-results/mrtr
228-
229-
- name: Run draft SEP-2575 client scenario
230-
run: |
231-
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" client \
232-
--command "$(pwd)/target/debug/conformance-client" \
233-
--scenario request-metadata \
234-
--spec-version draft \
235-
-o conformance-client-results/sep-2575
211+
--suite all \
212+
--spec-version 2026-07-28 \
213+
--expected-failures conformance/expected-failures-2026-07-28.yaml \
214+
-o conformance-client-results/draft
236215
237216
- name: Upload results
238217
if: always()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Known failures for the pinned 2026-07-28 draft conformance suite
2+
# (@modelcontextprotocol/conformance DRAFT_CONFORMANCE_VERSION).
3+
#
4+
# The full suites run in CI with `--expected-failures` pointing at this file:
5+
# - a scenario failing that is NOT listed here fails the build
6+
# - a scenario listed here that starts passing also fails the build (stale entry),
7+
# so remove it from this list when the underlying issue is fixed.
8+
#
9+
# When bumping DRAFT_CONFORMANCE_VERSION, diff
10+
# `conformance list --spec-version 2026-07-28` and update #977.
11+
12+
server:
13+
# SEP-2575 stateless lifecycle gaps: discover capability declaration,
14+
# missing-capability rejection, HTTP 404 for removed methods, and
15+
# diagnostic tools (test_missing_capability, test_streaming_elicitation,
16+
# test_logging_tool) not yet implemented.
17+
- server-stateless
18+
# SEP-2106: composition/conditional/$anchor keywords are stripped from
19+
# published tool input schemas.
20+
- json-schema-2020-12
21+
22+
client:
23+
# Client does not yet send MCP-Protocol-Version header pre-initialize as
24+
# required by the 2026-07-28 stateless lifecycle.
25+
- tools_call
26+
# Auth feature gaps in the 2026-07-28 auth scenarios.
27+
- auth/scope-step-up
28+
# SEP-2352: SDK lacks issuer-stamped credential storage (#879), so the
29+
# sep-2352-reregister-on-as-change check fails.
30+
- auth/authorization-server-migration
31+
# SEP-2243 client-side header handling gaps.
32+
- http-standard-headers
33+
- http-custom-headers
34+
- http-invalid-tool-headers

conformance/src/bin/client.rs

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -982,25 +982,49 @@ async fn main() -> anyhow::Result<()> {
982982

983983
tracing::info!("Running scenario '{}' against {}", scenario, server_url);
984984

985-
match scenario.as_str() {
985+
// Safety net: some harness servers intentionally misbehave (e.g. reply
986+
// with an id-less error instead of answering a request), which would
987+
// leave the client waiting forever. Exit on our own before the harness's
988+
// 30s client timeout so it never has to kill us (which has been observed
989+
// to wedge the harness process in CI).
990+
let timeout_secs: u64 = std::env::var("MCP_CONFORMANCE_TIMEOUT_SECS")
991+
.ok()
992+
.and_then(|v| v.parse().ok())
993+
.unwrap_or(25);
994+
tokio::time::timeout(
995+
std::time::Duration::from_secs(timeout_secs),
996+
run_scenario(&scenario, &server_url, &ctx),
997+
)
998+
.await
999+
.map_err(|_| anyhow::anyhow!("Scenario '{scenario}' timed out after {timeout_secs}s"))??;
1000+
1001+
Ok(())
1002+
}
1003+
1004+
async fn run_scenario(
1005+
scenario: &str,
1006+
server_url: &str,
1007+
ctx: &ConformanceContext,
1008+
) -> anyhow::Result<()> {
1009+
match scenario {
9861010
// Non-auth scenarios
987-
"initialize" => run_basic_client(&server_url).await?,
1011+
"initialize" => run_basic_client(server_url).await?,
9881012
// SEP-2106: the scenario serves a tool whose schema carries a network
9891013
// `$ref`; the check passes when the client lists tools without
9901014
// dereferencing (fetching) that URL. A plain connect → list_tools →
9911015
// close is sufficient; the scenario's mock server does not implement
9921016
// the discover lifecycle, so `run_discover_client` hangs against it.
993-
"json-schema-ref-no-deref" => run_basic_client(&server_url).await?,
994-
"tools_call" => run_tools_call_client(&server_url, &ctx).await?,
1017+
"json-schema-ref-no-deref" => run_basic_client(server_url).await?,
1018+
"tools_call" => run_tools_call_client(server_url, ctx).await?,
9951019
"elicitation-sep1034-client-defaults" => {
996-
run_elicitation_defaults_client(&server_url).await?
1020+
run_elicitation_defaults_client(server_url).await?
9971021
}
998-
"sse-retry" => run_sse_retry_client(&server_url).await?,
1022+
"sse-retry" => run_sse_retry_client(server_url).await?,
9991023
"request-metadata" | "sep-2322-client-request-state" => {
1000-
run_discover_client(&server_url).await?
1024+
run_discover_client(server_url).await?
10011025
}
10021026
"http-standard-headers" | "http-custom-headers" | "http-invalid-tool-headers" => {
1003-
run_tools_call_client(&server_url, &ctx).await?
1027+
run_tools_call_client(server_url, ctx).await?
10041028
}
10051029

10061030
// Auth scenarios - standard OAuth flow
@@ -1032,34 +1056,26 @@ async fn main() -> anyhow::Result<()> {
10321056
| "auth/iss-wrong-issuer"
10331057
| "auth/iss-unexpected"
10341058
| "auth/iss-normalized"
1035-
| "auth/metadata-issuer-mismatch"
1036-
| "auth/metadata-issuer-mismatch"
1037-
// SEP-2352: PRM `authorization_servers` switches between calls; a
1038-
// compliant client re-registers at the new AS. Known partial failure:
1039-
// the SDK lacks issuer-stamped credential storage (#879), so the
1040-
// `sep-2352-reregister-on-as-change` check fails. Left on the standard
1041-
// flow rather than fixture-orchestrated re-registration so the
1042-
// conformance result reflects real SDK behavior.
1043-
| "auth/authorization-server-migration" => run_auth_client(&server_url, &ctx).await?,
1059+
| "auth/metadata-issuer-mismatch" => run_auth_client(server_url, ctx).await?,
10441060

10451061
// Auth - scope step-up
1046-
"auth/scope-step-up" => run_auth_scope_step_up_client(&server_url, &ctx).await?,
1062+
"auth/scope-step-up" => run_auth_scope_step_up_client(server_url, ctx).await?,
10471063

10481064
// Auth - scope retry limit
1049-
"auth/scope-retry-limit" => run_auth_scope_retry_limit_client(&server_url, &ctx).await?,
1065+
"auth/scope-retry-limit" => run_auth_scope_retry_limit_client(server_url, ctx).await?,
10501066

10511067
// Auth - authorization server migration (SEP-2352)
10521068
"auth/authorization-server-migration" => {
1053-
run_auth_server_migration_client(&server_url, &ctx).await?
1069+
run_auth_server_migration_client(server_url, ctx).await?
10541070
}
10551071

10561072
// Auth - pre-registration
1057-
"auth/pre-registration" => run_auth_preregistered_client(&server_url, &ctx).await?,
1073+
"auth/pre-registration" => run_auth_preregistered_client(server_url, ctx).await?,
10581074

10591075
// Auth - resource mismatch (should fail to auth → pass)
10601076
"auth/resource-mismatch" => {
10611077
// Try to auth; it should fail because PRM resource doesn't match
1062-
match run_auth_client(&server_url, &ctx).await {
1078+
match run_auth_client(server_url, ctx).await {
10631079
Ok(_) => {
10641080
tracing::warn!("Auth succeeded despite resource mismatch!");
10651081
}
@@ -1070,12 +1086,12 @@ async fn main() -> anyhow::Result<()> {
10701086
}
10711087

10721088
// Auth - client credentials
1073-
"auth/client-credentials-basic" => run_client_credentials_basic(&server_url, &ctx).await?,
1074-
"auth/client-credentials-jwt" => run_client_credentials_jwt(&server_url, &ctx).await?,
1089+
"auth/client-credentials-basic" => run_client_credentials_basic(server_url, ctx).await?,
1090+
"auth/client-credentials-jwt" => run_client_credentials_jwt(server_url, ctx).await?,
10751091

10761092
// Auth - cross-app access
10771093
"auth/cross-app-access-complete-flow" => {
1078-
run_cross_app_access_client(&server_url, &ctx).await?
1094+
run_cross_app_access_client(server_url, ctx).await?
10791095
}
10801096

10811097
unknown => anyhow::bail!("Unsupported conformance scenario: {unknown}"),

conformance/src/bin/server.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,9 +1228,7 @@ async fn main() -> anyhow::Result<()> {
12281228

12291229
let server = ConformanceServer::new();
12301230
let stateless = std::env::var_os("STATELESS").is_some();
1231-
let config = StreamableHttpServerConfig::default()
1232-
.with_stateful_mode(!stateless)
1233-
.with_json_response(stateless);
1231+
let config = StreamableHttpServerConfig::default().with_stateful_mode(!stateless);
12341232
let service = StreamableHttpService::new(
12351233
move || Ok(server.clone()),
12361234
LocalSessionManager::default().into(),

0 commit comments

Comments
 (0)