use std::sync::Arc;
use protocol_droid::application::client::handler::{
ConnectOptions, HandlerRefusal, ServerRequestHandler, apply_elicitation_defaults,
};
use protocol_droid::application::client::inputs::{InputAnswers, answer_input_required};
use protocol_droid::application::client::{Client, ClientTransport};
use protocol_droid::domain::ir::JsonObject;
use protocol_droid::domain::{
Capability, CapabilitySet, ProtocolVersion, SupportedVersions, TransportKind, TransportSpec,
};
use protocol_droid::serde_json::{self, Value, json};
pub type Failure = Box<dyn std::error::Error>;
pub async fn run(scenario: &str, url: &str) -> Result<(), Failure> {
if super::auth_scenarios::handles(scenario) {
return super::auth_scenarios::run(scenario, url).await;
}
match scenario {
"initialize" | "tools_call" | "tools-call" => basic(url).await,
"json-schema-ref-no-deref" => list_only(url).await,
"request-metadata" => request_metadata(url).await,
"http-standard-headers" => standard_headers(url).await,
"http-custom-headers" => custom_headers(url).await,
"http-invalid-tool-headers" => invalid_tool_headers(url).await,
"sep-2322-client-request-state" => request_state(url).await,
"elicitation-sep1034-client-defaults" | "elicitation-defaults" => {
elicitation_defaults(url).await
}
"sse-retry" => sse_retry(url).await,
other => Err(format!("this client has no handler for `{other}`").into()),
}
}
fn runner_version() -> Option<ProtocolVersion> {
std::env::var("MCP_CONFORMANCE_PROTOCOL_VERSION")
.ok()
.and_then(|text| text.parse().ok())
}
fn stateless() -> bool {
runner_version().is_some_and(|version| !protocol_droid::domain::handshake_defined_in(version))
}
fn everything_declared() -> CapabilitySet {
CapabilitySet::empty()
.with(Capability::Roots)
.with(Capability::Sampling)
.with(Capability::Elicitation)
}
pub async fn connect(url: &str, options: ConnectOptions) -> Result<Client, Failure> {
let (kind, versions) = if stateless() {
let version = runner_version().ok_or("a stateless run names its revision")?;
(
TransportKind::StatelessHttp,
SupportedVersions::from_slice(&[version])?,
)
} else {
(TransportKind::StreamableHttp, SupportedVersions::all()?)
};
let spec = TransportSpec::new(kind, url)?;
Ok(Client::connect_with(&spec, &versions, options).await?)
}
async fn connect_stateless(url: &str) -> Result<Client, Failure> {
let version = runner_version()
.filter(|version| !protocol_droid::domain::handshake_defined_in(*version))
.unwrap_or(ProtocolVersion::V2026_07_28);
let spec = TransportSpec::new(TransportKind::StatelessHttp, url)?;
Ok(Client::connect_with(
&spec,
&SupportedVersions::from_slice(&[version])?,
ConnectOptions::new().declaring(everything_declared()),
)
.await?)
}
fn names(listing: &JsonObject, key: &str, member: &str) -> Vec<String> {
serde_json::from_str::<Value>(listing.as_str())
.ok()
.and_then(|value| value.get(key).and_then(Value::as_array).cloned())
.unwrap_or_default()
.iter()
.filter_map(|entry| {
entry
.get(member)
.and_then(Value::as_str)
.map(ToOwned::to_owned)
})
.collect()
}
fn object(value: &Value) -> JsonObject {
JsonObject::from_value(value.clone()).unwrap_or_default()
}
async fn basic(url: &str) -> Result<(), Failure> {
let client = connect(url, ConnectOptions::new()).await?;
let tools = client
.send_unchecked("tools/list", &JsonObject::default())
.await?;
if let Some(first) = names(&tools, "tools", "name").first() {
let call = object(&json!({ "name": first, "arguments": { "a": 2, "b": 3 } }));
client.send_unchecked("tools/call", &call).await?;
}
Ok(())
}
async fn list_only(url: &str) -> Result<(), Failure> {
let client = connect(url, ConnectOptions::new()).await?;
client
.send_unchecked("tools/list", &JsonObject::default())
.await?;
Ok(())
}
async fn request_metadata(url: &str) -> Result<(), Failure> {
let client = connect_stateless(url).await?;
client
.send_unchecked("tools/list", &JsonObject::default())
.await?;
Ok(())
}
async fn standard_headers(url: &str) -> Result<(), Failure> {
let client = connect_stateless(url).await?;
let tools = client.list_tools().await?;
for tool in names(&tools, "tools", "name") {
client.call_tool(&tool, &JsonObject::default()).await?;
}
let resources = client.list_resources().await?;
for uri in names(&resources, "resources", "uri") {
client.read_resource(&uri).await?;
}
let prompts = client.list_prompts().await?;
for prompt in names(&prompts, "prompts", "name") {
client.get_prompt(&prompt, &JsonObject::default()).await?;
}
Ok(())
}
async fn custom_headers(url: &str) -> Result<(), Failure> {
let context: Value = serde_json::from_str(&std::env::var("MCP_CONFORMANCE_CONTEXT")?)?;
let client = connect_stateless(url).await?;
client.list_tools().await?;
for call in context
.get("toolCalls")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let name = call
.get("name")
.and_then(Value::as_str)
.ok_or("a tool call has no name")?;
let arguments = object(call.get("arguments").unwrap_or(&Value::Null));
client.call_tool(name, &arguments).await?;
}
Ok(())
}
async fn invalid_tool_headers(url: &str) -> Result<(), Failure> {
let client = connect_stateless(url).await?;
let tools = client.list_tools().await?;
for tool in names(&tools, "tools", "name") {
let _ = client
.call_tool(&tool, &object(&json!({ "region": "us-west1" })))
.await;
}
Ok(())
}
async fn request_state(url: &str) -> Result<(), Failure> {
use protocol_droid::adapters::secondary::transport::http::{
HttpClientConfig, StatelessHttpClient,
};
let version = runner_version().unwrap_or(ProtocolVersion::V2026_07_28);
let transport = StatelessHttpClient::new(
HttpClientConfig::new(url)?,
version,
json!({ "elicitation": {}, "sampling": {}, "roots": {} }),
)?;
let transport: &dyn ClientTransport = &transport;
let confirm = |_key: String, method: String, _params: JsonObject| async move {
(method == "elicitation/create")
.then(|| object(&json!({ "action": "accept", "content": { "confirmed": true } })))
};
transport.send("tools/list", &JsonObject::default()).await?;
let echo = object(&json!({ "name": "test_mrtr_echo_state", "arguments": {} }));
let first = transport.send("tools/call", &echo).await?;
if let Some(answers) = answer_input_required(&first, &confirm).await? {
transport
.send(
"tools/call",
&object(&json!({ "name": "test_mrtr_unrelated", "arguments": {} })),
)
.await?;
transport
.send("tools/call", &answers.applied_to(&echo))
.await?;
}
let plain = object(&json!({ "name": "test_mrtr_no_state", "arguments": {} }));
let second = transport.send("tools/call", &plain).await?;
if let Some(answers) = answer_input_required(&second, &confirm).await? {
let answers = InputAnswers {
state: None,
..answers
};
transport
.send("tools/call", &answers.applied_to(&plain))
.await?;
}
transport
.send(
"tools/call",
&object(&json!({ "name": "test_mrtr_no_result_type", "arguments": {} })),
)
.await?;
Ok(())
}
#[derive(Debug)]
struct EmptyAnswers;
impl ServerRequestHandler for EmptyAnswers {
fn handle<'a>(
&'a self,
method: &'a str,
params: &'a JsonObject,
) -> std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, HandlerRefusal>> + Send + 'a>>
{
Box::pin(async move {
if method != "elicitation/create" {
return Err(HandlerRefusal::unsupported(method));
}
let accepted = object(&json!({ "action": "accept", "content": {} }));
Ok(apply_elicitation_defaults(params, &accepted))
})
}
}
async fn elicitation_defaults(url: &str) -> Result<(), Failure> {
let client = connect(
url,
ConnectOptions::new()
.declaring(CapabilitySet::empty().with(Capability::Elicitation))
.answering(Arc::new(EmptyAnswers)),
)
.await?;
let tools = client.list_tools().await?;
let tool = names(&tools, "tools", "name")
.into_iter()
.find(|name| name == "test_client_elicitation_defaults")
.ok_or("the test tool was not listed")?;
client.call_tool(&tool, &JsonObject::default()).await?;
Ok(())
}
async fn sse_retry(url: &str) -> Result<(), Failure> {
let client = connect(url, ConnectOptions::new()).await?;
let tools = client.list_tools().await?;
let tool = names(&tools, "tools", "name")
.into_iter()
.next()
.ok_or("no tool was listed")?;
client.call_tool(&tool, &JsonObject::default()).await?;
Ok(())
}