use rust_web_server::mcp::{InputRequired, LogLevel, McpContent, McpContext, McpServer, PromptArgDef, PromptMessage, PromptReply, SamplingRequest, TaskError};
use rust_web_server::server::Server;
const TEST_IMAGE_BASE64: &str =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
const TEST_AUDIO_BASE64: &str = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=";
fn arg(args: &str, name: &str) -> String {
rust_web_server::mcp::extract_arg(args, name).unwrap_or_default()
}
fn form(field: &str, kind: &str) -> String {
format!(r#"{{"type":"object","properties":{{"{field}":{{"type":"{kind}"}}}},"required":["{field}"]}}"#)
}
fn elicited(ctx: &McpContext, key: &str, field: &str) -> Option<String> {
rust_web_server::mcp::extract_arg(&ctx.elicitation_content(key)?, field)
}
fn sampling(prompt: &str, max_tokens: u32) -> SamplingRequest {
SamplingRequest { messages: vec![PromptMessage::user(prompt)], max_tokens, system_prompt: None }
}
const NO_ARGS: &str = r#"{"type":"object"}"#;
pub fn server() -> McpServer {
let port = std::env::var("RWS_CONFIG_PORT").unwrap_or_else(|_| "7878".into());
let server = McpServer::new("rws-conformance", env!("CARGO_PKG_VERSION"))
.allowed_origins([format!("http://127.0.0.1:{port}"), format!("http://localhost:{port}")])
.logging_enabled()
.tool("test_simple_text", "Tests simple text content response", r#"{"type":"object"}"#, |_| {
Ok(McpContent::text("This is a simple text response for testing."))
})
.tool("test_image_content", "Tests image content response", r#"{"type":"object"}"#, |_| {
Ok(McpContent::image(TEST_IMAGE_BASE64, "image/png"))
})
.tool("test_audio_content", "Tests audio content response", r#"{"type":"object"}"#, |_| {
Ok(McpContent::audio(TEST_AUDIO_BASE64, "audio/wav"))
})
.tool("test_multiple_content_types", "Tests response with multiple content types (text, image, resource)", r#"{"type":"object"}"#, |_| {
Ok(McpContent::blocks([
McpContent::text("Multiple content types test:"),
McpContent::image(TEST_IMAGE_BASE64, "image/png"),
McpContent::embedded("test://mixed-content-resource", r#"{"test":"data","value":123}"#, "application/json"),
]))
})
.tool("test_embedded_resource", "Tests embedded resource content response", r#"{"type":"object"}"#, |_| {
Ok(McpContent::embedded("test://embedded-resource", "This is an embedded resource content.", "text/plain"))
})
.tool_with_context(
"test_tool_with_progress",
"Tests tool that reports progress notifications",
r#"{"type":"object"}"#,
|ctx, _| {
for step in [0.0, 50.0, 100.0] {
ctx.report_progress(step, Some(100.0), Some(&format!("Completed step {step} of 100")));
std::thread::sleep(std::time::Duration::from_millis(50));
}
Ok(McpContent::text(ctx.progress_token.clone().unwrap_or_else(|| "0".into()).trim_matches('"').to_string()))
},
)
.tool("test_error_handling", "Tests error response handling", r#"{"type":"object"}"#, |_| {
Err("This tool intentionally returns an error for testing".into())
})
.tool_with_context(
"test_sampling",
"Tests server-initiated sampling (LLM completion request)",
r#"{"type":"object","properties":{"prompt":{"type":"string","description":"The prompt to send to the LLM"}},"required":["prompt"]}"#,
|ctx, args| {
let request = SamplingRequest { messages: vec![PromptMessage::user(arg(args, "prompt"))], max_tokens: 100, system_prompt: None };
Ok(McpContent::text(match ctx.sample(request, std::time::Duration::from_secs(10)) {
Ok(reply) => format!("LLM response: {}", reply.content.text),
Err(e) => format!("Sampling not supported or error: {e}"),
}))
},
)
.tool(
"json_schema_2020_12_tool",
"Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613)",
concat!(
r##"{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","##,
r##""$defs":{"address":{"$anchor":"addressDef","type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"}}}},"##,
r##""properties":{"name":{"type":"string"},"address":{"$ref":"#/$defs/address"},"contactMethod":{"type":"string","enum":["phone","email"]},"##,
r##""phone":{"type":"string"},"email":{"type":"string"}},"##,
r##""allOf":[{"anyOf":[{"required":["phone"]},{"required":["email"]}]}],"##,
r##""if":{"properties":{"contactMethod":{"const":"phone"}},"required":["contactMethod"]},"then":{"required":["phone"]},"else":{"required":["email"]},"##,
r##""additionalProperties":false}"##
),
|args| Ok(McpContent::text(format!("JSON Schema 2020-12 tool called with: {args}"))),
)
.resource("test://static-text", "static-text", "A static text resource for testing", |_| {
Ok(McpContent::text("This is the content of the static text resource."))
})
.resource("test://static-binary", "static-binary", "A static binary resource (image) for testing", |_| {
Ok(McpContent::blob(TEST_IMAGE_BASE64, "image/png"))
})
.resource("test://template/{id}/data", "template", "A resource template with parameter substitution", |uri| {
let id = uri.trim_start_matches("test://template/").trim_end_matches("/data");
Ok(McpContent::json(format!(r#"{{"id":"{id}","templateTest":true,"data":"Data for ID: {id}"}}"#)))
})
.resource("test://watched-resource", "watched-resource", "A resource that auto-updates every 3 seconds", |_| {
Ok(McpContent::text("Watched resource content"))
})
.prompt("test_simple_prompt", "A simple prompt without arguments", |_| {
Ok(vec![PromptMessage::user("This is a simple prompt for testing.")])
})
.prompt_with_args(
"test_prompt_with_arguments",
"A prompt with required arguments",
vec![PromptArgDef::required("arg1", "First test argument"), PromptArgDef::required("arg2", "Second test argument")],
|args| Ok(vec![PromptMessage::user(format!("Prompt with arguments: arg1='{}', arg2='{}'", arg(args, "arg1"), arg(args, "arg2")))]),
)
.prompt_with_args(
"test_prompt_with_embedded_resource",
"A prompt that includes an embedded resource",
vec![PromptArgDef::required("resourceUri", "URI of the resource to embed")],
|args| {
Ok(vec![
PromptMessage {
role: "user",
content: McpContent::embedded(arg(args, "resourceUri"), "Embedded resource content for testing.", "text/plain"),
},
PromptMessage::user("Please process the embedded resource above."),
])
},
)
.prompt("test_prompt_with_image", "A prompt that includes image content", |_| {
Ok(vec![
PromptMessage { role: "user", content: McpContent::image(TEST_IMAGE_BASE64, "image/png") },
PromptMessage::user("Please analyze the image above."),
])
})
.completion("ref/prompt", "test_prompt_with_arguments", |_, _| Ok(Vec::new()))
.tool_with_context("test_input_required_result_elicitation", "Asks for your name, then greets you", NO_ARGS, |ctx, _| {
Ok(match elicited(&ctx, "user_name", "name") {
Some(name) => McpContent::text(format!("Hello, {name}!")),
None => McpContent::input_required(InputRequired::new().elicit("user_name", "What is your name?", &form("name", "string"))),
})
})
.tool_with_context("test_input_required_result_sampling", "Asks the client's LLM a question", NO_ARGS, |ctx, _| {
Ok(match ctx.input_response("capital_question") {
Some(reply) => McpContent::text(format!("The LLM said: {}", arg(&reply, "text"))),
None => McpContent::input_required(InputRequired::new().sample("capital_question", &sampling("What is the capital of France?", 100))),
})
})
.tool_with_context("test_input_required_result_list_roots", "Asks for the client's roots", NO_ARGS, |ctx, _| {
Ok(match ctx.input_response("client_roots") {
Some(roots) => McpContent::text(format!("Client roots: {roots}")),
None => McpContent::input_required(InputRequired::new().list_roots("client_roots")),
})
})
.tool_with_context("test_input_required_result_request_state", "Round-trips a requestState", NO_ARGS, |ctx, _| {
Ok(match (ctx.request_state(), ctx.input_response("confirm")) {
(Some("confirm-state"), Some(_)) => McpContent::text("state-ok: the requestState came back intact"),
_ => McpContent::input_required(InputRequired::new().elicit("confirm", "Please confirm", &form("ok", "boolean")).state("confirm-state")),
})
})
.tool_with_context("test_input_required_result_tampered_state", "Signs its requestState; an altered one is refused", NO_ARGS, |ctx, _| {
Ok(match (ctx.request_state(), ctx.input_response("confirm")) {
(Some("signed-state"), Some(_)) => McpContent::text("state-ok"),
_ => McpContent::input_required(InputRequired::new().elicit("confirm", "Please confirm", &form("ok", "boolean")).state("signed-state")),
})
})
.tool_with_context("test_input_required_result_multi_round", "Asks two questions, one per round", NO_ARGS, |ctx, _| {
Ok(match ctx.request_state() {
Some("round-2") if ctx.input_response("step2").is_some() => McpContent::text("Both steps complete"),
Some("round-1") if ctx.input_response("step1").is_some() => McpContent::input_required(
InputRequired::new().elicit("step2", "Step 2: What is your favorite color?", &form("color", "string")).state("round-2"),
),
_ => McpContent::input_required(InputRequired::new().elicit("step1", "Step 1: What is your name?", &form("name", "string")).state("round-1")),
})
})
.tool_with_context("test_input_required_result_multiple_inputs", "Asks for a name, a greeting and the roots at once", NO_ARGS, |ctx, _| {
let answered = ["user_name", "greeting", "client_roots"].iter().all(|key| ctx.input_response(key).is_some());
Ok(if answered && ctx.request_state() == Some("multi") {
McpContent::text("All three inputs received")
} else {
McpContent::input_required(
InputRequired::new()
.elicit("user_name", "What is your name?", &form("name", "string"))
.sample("greeting", &sampling("Generate a greeting", 50))
.list_roots("client_roots")
.state("multi"),
)
})
})
.tool_with_context("test_input_required_result_capabilities", "Asks only for what the client declared", NO_ARGS, |ctx, _| {
let answered = ctx.input_response("question").is_some() || ctx.input_response("name").is_some();
Ok(if answered {
McpContent::text("Thanks")
} else {
let mut input = InputRequired::new();
if ctx.client_supports("sampling") {
input = input.sample("question", &sampling("Say hi", 10));
}
if ctx.client_supports("elicitation") {
input = input.elicit("name", "What is your name?", &form("name", "string"));
}
McpContent::input_required(input)
})
})
.tool_with_context("test_missing_capability", "Needs the client's sampling capability", NO_ARGS, |ctx, _| {
Ok(match ctx.input_response("question") {
Some(_) => McpContent::text("sampled"),
None => McpContent::input_required(InputRequired::new().sample("question", &sampling("Say hi", 10))),
})
})
.tool_with_context("test_streaming_elicitation", "Asks for input (on 2026-07-28, as a result, never as a request)", NO_ARGS, |ctx, _| {
Ok(match elicited(&ctx, "answer", "value") {
Some(value) => McpContent::text(format!("You said {value}")),
None => McpContent::input_required(InputRequired::new().elicit("answer", "Say something", &form("value", "string"))),
})
})
.tool(
"test_custom_headers",
"Its region and priority are mirrored into Mcp-Param-* headers",
r#"{"type":"object","properties":{"region":{"type":"string","x-mcp-header":"Region"},"priority":{"type":"integer","x-mcp-header":"Priority"}},"required":["region"]}"#,
|args| Ok(McpContent::text(format!("region {}", arg(args, "region")))),
)
.tool_with_context("test_logging_tool", "Logs three messages about this request", NO_ARGS, |ctx, _| {
for message in ["Tool execution started", "Tool processing data", "Tool execution completed"] {
ctx.log(LogLevel::Info, &format!(r#""{message}""#));
}
Ok(McpContent::text("Logged"))
})
.tool("greet", "Sync-only greeting", r#"{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}"#, |args| {
Ok(McpContent::text(format!("Hello, {}!", arg(args, "name"))))
})
.tool_with_context("slow_compute", "Sleeps `seconds`, then answers; as a task when the client can", r#"{"type":"object","properties":{"seconds":{"type":"number"},"label":{"type":"string"}}}"#, |ctx, args| {
let seconds: f64 = arg(args, "seconds").parse().unwrap_or(1.0);
let label = Some(arg(args, "label")).filter(|l| !l.is_empty()).unwrap_or_else(|| "compute".into());
ctx.start_task(move |task| {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(seconds.max(0.0));
while std::time::Instant::now() < deadline {
if task.is_cancelled() {
return Err(TaskError::Cancelled);
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
Ok(McpContent::text(format!("slow_compute({label}) done after {seconds}s")))
})
})
.tool_with_context("failing_job", "Always a tool execution error, after a second", NO_ARGS, |ctx, _| {
ctx.start_task(|_task| {
std::thread::sleep(std::time::Duration::from_secs(1));
Err(TaskError::Tool("failing_job: intentional tool execution error".into()))
})
})
.tool_with_context("protocol_error_job", "Fails with a protocol-level error", NO_ARGS, |ctx, _| {
ctx.start_task(|_task| {
std::thread::sleep(std::time::Duration::from_millis(200));
Err(TaskError::Failed { code: -32603, message: "protocol_error_job: intentional protocol-level failure".into() })
})
})
.tool_with_context("confirm_delete", "Asks to confirm, inside the task", r#"{"type":"object","properties":{"filename":{"type":"string"}}}"#, |ctx, args| {
let filename = Some(arg(args, "filename")).filter(|f| !f.is_empty()).unwrap_or_else(|| "file.txt".into());
ctx.start_task(move |task| {
let answers = task.input(InputRequired::new().elicit("confirm", &format!("Delete {filename}?"), &form("confirm", "boolean")))?;
let confirmed = answers.iter().any(|(_, answer)| answer.contains(r#""confirm":true"#));
Ok(McpContent::text(format!("confirm_delete({filename}): confirmed = {confirmed}")))
})
})
.tool_with_context("multi_input", "Asks two questions at once, inside the task", NO_ARGS, |ctx, _| {
ctx.start_task(|task| {
let answers = task.input(InputRequired::new().elicit("input-a", "Provide value A", &form("value", "string")).elicit("input-b", "Provide value B", &form("value", "string")))?;
Ok(McpContent::text(format!("multi_input: {} answers", answers.len())))
})
})
.tool_with_context("test_tool_with_task", "Asks your name, then works on it as a task", NO_ARGS, |ctx, _| {
match elicited(&ctx, "user_name", "name") {
None => Ok(McpContent::input_required(InputRequired::new().elicit("user_name", "What is your name?", &form("name", "string")))),
Some(name) => ctx.start_task(move |_task| {
std::thread::sleep(std::time::Duration::from_millis(200));
Ok(McpContent::text(format!("Hello, {name}! (async)")))
}),
}
})
.task_required("failing_job")
.task_required("confirm_delete")
.task_required("multi_input")
.task_required("test_tool_with_task")
.prompt_with_context("test_input_required_result_prompt", "A prompt that asks for context first", vec![], |ctx, _| {
Ok(match elicited(&ctx, "user_context", "context") {
Some(context) => PromptReply::Messages(vec![PromptMessage::user(format!("Use this context: {context}"))]),
None => PromptReply::InputRequired(InputRequired::new().elicit("user_context", "What context should the prompt use?", &form("context", "string"))),
})
});
let logger = server.clone();
server.register_tool("test_tool_with_logging", "Tests tool that emits log messages during execution", r#"{"type":"object"}"#, move |_| {
for message in ["Tool execution started", "Tool processing data", "Tool execution completed"] {
logger.log(LogLevel::Info, None, &format!(r#""{message}""#));
std::thread::sleep(std::time::Duration::from_millis(50));
}
Ok(McpContent::text("Tool with logging executed successfully"))
});
let tools = server.clone();
server.register_tool("test_trigger_tool_change", "Adds and removes a tool", NO_ARGS, move |_| {
tools.register_tool("test_transient_tool", "Here for a moment", NO_ARGS, |_| Ok(McpContent::text("transient")));
tools.remove_tool("test_transient_tool");
Ok(McpContent::text("The tool list changed"))
});
let prompts = server.clone();
server.register_tool("test_trigger_prompt_change", "Adds and removes a prompt", NO_ARGS, move |_| {
prompts.register_prompt("test_transient_prompt", "Here for a moment", |_| Ok(vec![PromptMessage::user("transient")]));
prompts.remove_prompt("test_transient_prompt");
Ok(McpContent::text("The prompt list changed"))
});
server
}
#[tokio::main]
async fn main() {
let (listener, pool) = Server::setup().unwrap();
Server::run_tls(listener, pool, server()).await;
}
#[cfg(test)]
mod tests {
use super::*;
fn body(server: &McpServer, request: &str) -> String {
String::from_utf8(server.handle_request(request).content_range_list[0].body.clone()).unwrap()
}
#[test]
fn the_fixtures_answer() {
let server = server();
let tools = body(&server, r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#);
for tool in ["test_simple_text", "test_image_content", "test_error_handling", "test_tool_with_logging", "json_schema_2020_12_tool"] {
assert!(tools.contains(tool), "{tool}: {tools}");
}
let text = body(&server, r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"test_simple_text","arguments":{}}}"#);
assert!(text.contains("This is a simple text response for testing."), "{text}");
let template = body(&server, r#"{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"test://template/42/data"}}"#);
assert!(template.contains(r#"\"id\":\"42\""#), "{template}");
let prompt = body(&server, r#"{"jsonrpc":"2.0","id":4,"method":"prompts/get","params":{"name":"test_prompt_with_arguments","arguments":{"arg1":"a","arg2":"b"}}}"#);
assert!(prompt.contains("arg1='a', arg2='b'"), "{prompt}");
}
}