Skip to content

Commit 3662d20

Browse files
authored
test: enable supported draft SEP coverage (#971)
* test: enable supported draft SEP coverage * test: add SEP-2322 MRTR conformance scenarios * refactor: rename run_mrtr_client to run_stateless_client
1 parent d8331d9 commit 3662d20

4 files changed

Lines changed: 586 additions & 8 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ concurrency:
1414
env:
1515
# Pinned for reproducible runs; bump deliberately when the suite updates.
1616
CONFORMANCE_VERSION: "0.1.16"
17+
DRAFT_CONFORMANCE_VERSION: "0.2.0-alpha.9"
1718

1819
jobs:
1920
server:
@@ -64,6 +65,61 @@ jobs:
6465
-o conformance-results
6566
done
6667
68+
- name: Start draft conformance server
69+
run: |
70+
STATELESS=1 PORT=8002 ./target/debug/conformance-server &
71+
echo $! > draft-server.pid
72+
for _ in $(seq 1 30); do
73+
if curl -s -o /dev/null http://127.0.0.1:8002/mcp; then
74+
exit 0
75+
fi
76+
sleep 1
77+
done
78+
echo "draft conformance server did not become ready" >&2
79+
exit 1
80+
81+
- name: Run draft SEP scenarios
82+
run: |
83+
for scenario in sep-2164-resource-not-found caching http-header-validation; do
84+
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
85+
--url http://127.0.0.1:8002/mcp \
86+
--scenario "$scenario" \
87+
--spec-version draft \
88+
-o conformance-results
89+
done
90+
91+
# SEP-2322 MRTR scenarios (spec 2026-07-28). They speak the stateless
92+
# lifecycle (bare JSON-RPC POSTs, no initialize handshake), so they run
93+
# against the stateless draft server.
94+
- name: Run SEP-2322 MRTR scenarios
95+
run: |
96+
for scenario in \
97+
input-required-result-basic-elicitation \
98+
input-required-result-basic-sampling \
99+
input-required-result-basic-list-roots \
100+
input-required-result-request-state \
101+
input-required-result-multiple-input-requests \
102+
input-required-result-multi-round \
103+
input-required-result-missing-input-response \
104+
input-required-result-non-tool-request \
105+
input-required-result-result-type \
106+
input-required-result-unsupported-methods \
107+
input-required-result-tampered-state \
108+
input-required-result-capability-check \
109+
input-required-result-ignore-extra-params \
110+
input-required-result-validate-input \
111+
; do
112+
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" server \
113+
--url http://127.0.0.1:8002/mcp \
114+
--scenario "$scenario" \
115+
-o conformance-results
116+
done
117+
118+
- name: Stop draft conformance server
119+
if: always()
120+
run: kill "$(cat draft-server.pid)" 2>/dev/null || true
121+
122+
67123
- name: Stop conformance server
68124
if: always()
69125
run: kill "$(cat server.pid)" 2>/dev/null || true
@@ -98,6 +154,14 @@ jobs:
98154
--spec-version 2025-11-25 \
99155
-o conformance-client-results/full
100156
157+
# SEP-2322 MRTR client scenario (spec 2026-07-28).
158+
- name: Run SEP-2322 MRTR client scenario
159+
run: |
160+
npx -y "@modelcontextprotocol/conformance@${DRAFT_CONFORMANCE_VERSION}" client \
161+
--command "$(pwd)/target/debug/conformance-client" \
162+
--scenario sep-2322-client-request-state \
163+
-o conformance-client-results/mrtr
164+
101165
- name: Upload results
102166
if: always()
103167
uses: actions/upload-artifact@v7

conformance/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ rmcp = { path = "../crates/rmcp", features = [
1818
"client",
1919
"elicitation",
2020
"auth",
21+
"request-state",
2122
"transport-streamable-http-server",
2223
"transport-streamable-http-client-reqwest",
2324
] }

conformance/src/bin/client.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use rmcp::{
22
ClientHandler, ErrorData, RoleClient, ServiceExt,
33
model::*,
4-
service::RequestContext,
4+
service::{RequestContext, serve_directly},
55
transport::{
66
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
77
auth::{AuthorizationCallback, OAuthState},
@@ -824,6 +824,97 @@ async fn run_elicitation_defaults_client(server_url: &str) -> anyhow::Result<()>
824824
Ok(())
825825
}
826826

827+
/// A minimal stateless client transport: every outgoing message is one HTTP
828+
/// POST and the JSON response body (if any) is queued for `receive()`.
829+
///
830+
/// The SEP-2322 client scenario's mock server speaks the stateless lifecycle
831+
/// (no `initialize` handshake, plain JSON responses), which the session-based
832+
/// `StreamableHttpClientTransport` cannot do. The transport is harness
833+
/// plumbing; the behavior under test — the SDK's MRTR retry driver — runs
834+
/// unchanged on top of it.
835+
struct StatelessHttpTransport {
836+
http: reqwest::Client,
837+
uri: std::sync::Arc<str>,
838+
tx: tokio::sync::mpsc::Sender<ServerJsonRpcMessage>,
839+
rx: tokio::sync::mpsc::Receiver<ServerJsonRpcMessage>,
840+
}
841+
842+
impl StatelessHttpTransport {
843+
fn new(uri: &str) -> Self {
844+
let (tx, rx) = tokio::sync::mpsc::channel(16);
845+
Self {
846+
http: reqwest::Client::new(),
847+
uri: uri.into(),
848+
tx,
849+
rx,
850+
}
851+
}
852+
}
853+
854+
impl rmcp::transport::Transport<RoleClient> for StatelessHttpTransport {
855+
type Error = std::io::Error;
856+
857+
fn send(
858+
&mut self,
859+
item: rmcp::model::ClientJsonRpcMessage,
860+
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send + 'static {
861+
let http = self.http.clone();
862+
let uri = self.uri.clone();
863+
let tx = self.tx.clone();
864+
async move {
865+
let response = http
866+
.post(uri.as_ref())
867+
.header("MCP-Protocol-Version", "2026-07-28")
868+
.json(&item)
869+
.send()
870+
.await
871+
.map_err(std::io::Error::other)?;
872+
match response.json::<ServerJsonRpcMessage>().await {
873+
Ok(message) => {
874+
let _ = tx.send(message).await;
875+
}
876+
Err(_) => {
877+
// No JSON-RPC body (e.g. 202/204 for notifications).
878+
}
879+
}
880+
Ok(())
881+
}
882+
}
883+
884+
async fn receive(&mut self) -> Option<ServerJsonRpcMessage> {
885+
self.rx.recv().await
886+
}
887+
888+
async fn close(&mut self) -> Result<(), Self::Error> {
889+
Ok(())
890+
}
891+
}
892+
893+
/// A stateless-lifecycle client: the scenario's server has no `initialize`
894+
/// handler, so skip the handshake with `serve_directly`, list the tools, and
895+
/// call each one via the high-level `call_tool` helper (which drives SEP-2322
896+
/// `input_required` retry rounds when the server requests them). Used by the
897+
/// `sep-2322-client-request-state` scenario, whose mock server verifies
898+
/// requestState echo, fresh JSON-RPC ids on retry, state omission, isolation
899+
/// between tools, and the `resultType` default.
900+
async fn run_stateless_client(server_url: &str) -> anyhow::Result<()> {
901+
let transport = StatelessHttpTransport::new(server_url);
902+
let peer_info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
903+
.with_protocol_version(ProtocolVersion::V_2026_07_28);
904+
let client = serve_directly(FullClientHandler, transport, Some(peer_info));
905+
906+
let tools = client.list_tools(Default::default()).await?;
907+
tracing::debug!("Listed {} tools", tools.tools.len());
908+
for tool in &tools.tools {
909+
let result = client
910+
.call_tool(CallToolRequestParams::new(tool.name.clone()))
911+
.await;
912+
tracing::debug!("Called {}: {:?}", tool.name, result.is_ok());
913+
}
914+
client.cancel().await?;
915+
Ok(())
916+
}
917+
827918
async fn run_sse_retry_client(server_url: &str) -> anyhow::Result<()> {
828919
let transport = StreamableHttpClientTransport::from_uri(server_url);
829920
let client = BasicClientHandler.serve(transport).await?;
@@ -869,6 +960,7 @@ async fn main() -> anyhow::Result<()> {
869960
run_elicitation_defaults_client(&server_url).await?
870961
}
871962
"sse-retry" => run_sse_retry_client(&server_url).await?,
963+
"sep-2322-client-request-state" => run_stateless_client(&server_url).await?,
872964

873965
// Auth scenarios - standard OAuth flow
874966
"auth/metadata-default"

0 commit comments

Comments
 (0)