chore: update client conformance test scenarios (#991)
This commit is contained in:
parent
98bb2635f4
commit
2fe90a245b
1 changed files with 58 additions and 27 deletions
|
|
@ -13,8 +13,17 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
// ─── Context parsed from MCP_CONFORMANCE_CONTEXT ────────────────────────────
|
// ─── Context parsed from MCP_CONFORMANCE_CONTEXT ────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Deserialize)]
|
||||||
|
struct ConformanceToolCall {
|
||||||
|
name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
arguments: Option<serde_json::Map<String, Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Deserialize)]
|
#[derive(Debug, Default, serde::Deserialize)]
|
||||||
struct ConformanceContext {
|
struct ConformanceContext {
|
||||||
|
#[serde(default)]
|
||||||
|
tool_calls: Vec<ConformanceToolCall>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
client_id: Option<String>,
|
client_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -793,16 +802,29 @@ async fn run_basic_client(server_url: &str) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_tools_call_client(server_url: &str) -> anyhow::Result<()> {
|
async fn run_tools_call_client(server_url: &str, ctx: &ConformanceContext) -> anyhow::Result<()> {
|
||||||
let transport = StreamableHttpClientTransport::from_uri(server_url);
|
let transport = StreamableHttpClientTransport::from_uri(server_url);
|
||||||
let client = FullClientHandler.serve(transport).await?;
|
let client = FullClientHandler.serve(transport).await?;
|
||||||
let tools = client.list_tools(Default::default()).await?;
|
let tools = client.list_tools(Default::default()).await?;
|
||||||
for tool in &tools.tools {
|
|
||||||
let args = build_tool_arguments(tool);
|
if ctx.tool_calls.is_empty() {
|
||||||
let _ = client
|
for tool in &tools.tools {
|
||||||
.call_tool(call_tool_params(tool.name.clone(), args))
|
let args = build_tool_arguments(tool);
|
||||||
.await?;
|
client
|
||||||
|
.call_tool(call_tool_params(tool.name.clone(), args))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for tool_call in &ctx.tool_calls {
|
||||||
|
client
|
||||||
|
.call_tool(call_tool_params(
|
||||||
|
tool_call.name.clone().into(),
|
||||||
|
tool_call.arguments.clone(),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.cancel().await?;
|
client.cancel().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -851,6 +873,13 @@ impl StatelessHttpTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn conformance_protocol_version() -> ProtocolVersion {
|
||||||
|
std::env::var("MCP_CONFORMANCE_PROTOCOL_VERSION")
|
||||||
|
.ok()
|
||||||
|
.and_then(|version| serde_json::from_value(Value::String(version)).ok())
|
||||||
|
.unwrap_or(ProtocolVersion::V_2026_07_28)
|
||||||
|
}
|
||||||
|
|
||||||
impl rmcp::transport::Transport<RoleClient> for StatelessHttpTransport {
|
impl rmcp::transport::Transport<RoleClient> for StatelessHttpTransport {
|
||||||
type Error = std::io::Error;
|
type Error = std::io::Error;
|
||||||
|
|
||||||
|
|
@ -864,7 +893,10 @@ impl rmcp::transport::Transport<RoleClient> for StatelessHttpTransport {
|
||||||
async move {
|
async move {
|
||||||
let response = http
|
let response = http
|
||||||
.post(uri.as_ref())
|
.post(uri.as_ref())
|
||||||
.header("MCP-Protocol-Version", "2026-07-28")
|
.header(
|
||||||
|
"MCP-Protocol-Version",
|
||||||
|
conformance_protocol_version().as_str(),
|
||||||
|
)
|
||||||
.json(&item)
|
.json(&item)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|
@ -890,17 +922,19 @@ impl rmcp::transport::Transport<RoleClient> for StatelessHttpTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A stateless-lifecycle client: the scenario's server has no `initialize`
|
/// Runs a client using the draft stateless lifecycle.
|
||||||
/// handler, so skip the handshake with `serve_directly`, list the tools, and
|
///
|
||||||
/// call each one via the high-level `call_tool` helper (which drives SEP-2322
|
/// Stateless servers do not implement the `initialize` handshake, so this
|
||||||
/// `input_required` retry rounds when the server requests them). Used by the
|
/// uses `serve_directly`. The protocol version comes from
|
||||||
/// `sep-2322-client-request-state` scenario, whose mock server verifies
|
/// `MCP_CONFORMANCE_PROTOCOL_VERSION` (defaulting to `2026-07-28`) and is
|
||||||
/// requestState echo, fresh JSON-RPC ids on retry, state omission, isolation
|
/// used for both peer configuration and outgoing HTTP request headers.
|
||||||
/// between tools, and the `resultType` default.
|
///
|
||||||
|
/// Lists available tools and calls each one, allowing the SDK's high-level
|
||||||
|
/// tool-call handling to process any request retries.
|
||||||
async fn run_stateless_client(server_url: &str) -> anyhow::Result<()> {
|
async fn run_stateless_client(server_url: &str) -> anyhow::Result<()> {
|
||||||
let transport = StatelessHttpTransport::new(server_url);
|
let transport = StatelessHttpTransport::new(server_url);
|
||||||
let peer_info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
|
let peer_info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
|
||||||
.with_protocol_version(ProtocolVersion::V_2026_07_28);
|
.with_protocol_version(conformance_protocol_version());
|
||||||
let client = serve_directly(FullClientHandler, transport, Some(peer_info));
|
let client = serve_directly(FullClientHandler, transport, Some(peer_info));
|
||||||
|
|
||||||
let tools = client.list_tools(Default::default()).await?;
|
let tools = client.list_tools(Default::default()).await?;
|
||||||
|
|
@ -955,12 +989,18 @@ async fn main() -> anyhow::Result<()> {
|
||||||
match scenario.as_str() {
|
match scenario.as_str() {
|
||||||
// Non-auth scenarios
|
// Non-auth scenarios
|
||||||
"initialize" => run_basic_client(&server_url).await?,
|
"initialize" => run_basic_client(&server_url).await?,
|
||||||
"tools_call" => run_tools_call_client(&server_url).await?,
|
"json-schema-ref-no-deref" => run_stateless_client(&server_url).await?,
|
||||||
|
"tools_call" => run_tools_call_client(&server_url, &ctx).await?,
|
||||||
"elicitation-sep1034-client-defaults" => {
|
"elicitation-sep1034-client-defaults" => {
|
||||||
run_elicitation_defaults_client(&server_url).await?
|
run_elicitation_defaults_client(&server_url).await?
|
||||||
}
|
}
|
||||||
"sse-retry" => run_sse_retry_client(&server_url).await?,
|
"sse-retry" => run_sse_retry_client(&server_url).await?,
|
||||||
"sep-2322-client-request-state" => run_stateless_client(&server_url).await?,
|
"request-metadata" | "sep-2322-client-request-state" => {
|
||||||
|
run_stateless_client(&server_url).await?
|
||||||
|
}
|
||||||
|
"http-standard-headers" | "http-custom-headers" | "http-invalid-tool-headers" => {
|
||||||
|
run_tools_call_client(&server_url, &ctx).await?
|
||||||
|
}
|
||||||
|
|
||||||
// Auth scenarios - standard OAuth flow
|
// Auth scenarios - standard OAuth flow
|
||||||
"auth/metadata-default"
|
"auth/metadata-default"
|
||||||
|
|
@ -1008,16 +1048,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
run_cross_app_access_client(&server_url, &ctx).await?
|
run_cross_app_access_client(&server_url, &ctx).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
unknown => anyhow::bail!("Unsupported conformance scenario: {unknown}"),
|
||||||
tracing::warn!("Unknown scenario '{}', trying auth flow", scenario);
|
|
||||||
match run_auth_client(&server_url, &ctx).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::debug!("Auth flow failed for unknown scenario: {e}");
|
|
||||||
run_basic_client(&server_url).await?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue