mod common;
use common::{launch_example, prompt_has_text, read_text, shutdown, tool_text};
use rmcp::model::{CallToolRequestParams, GetPromptRequestParams, ReadResourceRequestParams};
#[tokio::test(flavor = "current_thread")]
async fn custom_resources_and_prompts_round_trip() {
let client = launch_example("custom_resources_prompts")
.await
.expect("client should launch");
let resources = client
.list_resources(None)
.await
.expect("resource list should work")
.resources;
assert!(
resources
.iter()
.any(|resource| resource.uri == "clap://schema")
);
assert!(
resources
.iter()
.any(|resource| resource.uri == "example://readme")
);
let readme = client
.read_resource(ReadResourceRequestParams::new("example://readme"))
.await
.expect("custom resource should be readable");
assert!(read_text(&readme).contains("Custom resources & prompts example"));
let schema = client
.read_resource(ReadResourceRequestParams::new("clap://schema"))
.await
.expect("schema resource should be readable");
assert!(read_text(&schema).contains("\"name\": \"custom-resources-prompts\""));
let prompts = client
.list_prompts(None)
.await
.expect("prompt list should work")
.prompts;
assert!(prompts.iter().any(|prompt| prompt.name == "example-prompt"));
let prompt = client
.get_prompt(GetPromptRequestParams::new("example-prompt"))
.await
.expect("custom prompt should resolve");
assert!(prompt_has_text(&prompt.messages, "prefer the echo tool"));
shutdown(client).await;
}
#[tokio::test(flavor = "current_thread")]
async fn unknown_resources_prompts_tools_and_arguments_return_errors() {
let client = launch_example("custom_resources_prompts")
.await
.expect("client should launch");
let resource_error = client
.read_resource(ReadResourceRequestParams::new("example://missing"))
.await
.expect_err("unknown resource should error");
assert!(format!("{resource_error:?}").contains("unknown resource uri"));
let prompt_error = client
.get_prompt(GetPromptRequestParams::new("missing-prompt"))
.await
.expect_err("unknown prompt should error");
assert!(format!("{prompt_error:?}").contains("unknown prompt"));
shutdown(client).await;
let client = launch_example("subcommands")
.await
.expect("subcommands client should launch");
let tool_error = client
.call_tool(
CallToolRequestParams::new("missing-tool").with_arguments(serde_json::Map::new()),
)
.await
.expect_err("unknown tool should error");
assert!(
tool_error
.to_string()
.to_lowercase()
.contains("unknown tool")
);
let invalid_arg_error =
client
.call_tool(CallToolRequestParams::new("greet").with_arguments(
serde_json::Map::from_iter([("bogus".to_string(), serde_json::json!("value"))]),
))
.await
.expect_err("unknown argument should error");
assert!(
invalid_arg_error
.to_string()
.contains("unknown argument: bogus")
);
shutdown(client).await;
}
#[tokio::test(flavor = "current_thread")]
async fn in_process_outputs_and_required_args_are_preserved() {
let client = launch_example("structured")
.await
.expect("structured client should launch");
let structured = client
.call_tool(
CallToolRequestParams::new("add").with_arguments(serde_json::Map::from_iter([
("a".to_string(), serde_json::json!(2)),
("b".to_string(), serde_json::json!(3)),
])),
)
.await
.expect("structured call should succeed");
assert_ne!(structured.is_error, Some(true));
assert_eq!(
structured
.structured_content
.as_ref()
.and_then(|content| content.get("sum"))
.and_then(|value| value.as_i64()),
Some(5)
);
assert!(tool_text(&structured).contains("\"sum\": 5"));
shutdown(client).await;
let client = launch_example("result_output")
.await
.expect("result-output client should launch");
let structured_error =
client
.call_tool(CallToolRequestParams::new("check").with_arguments(
serde_json::Map::from_iter([("x".to_string(), serde_json::json!(0))]),
))
.await
.expect("structured error should still be a tool response");
assert_eq!(structured_error.is_error, Some(true));
assert_eq!(
structured_error
.structured_content
.as_ref()
.and_then(|content| content.get("code"))
.and_then(|value| value.as_i64()),
Some(-1)
);
shutdown(client).await;
let client = launch_example("optional_commands_and_args")
.await
.expect("optional args client should launch");
let missing_arg = client
.call_tool(CallToolRequestParams::new("read").with_arguments(serde_json::Map::new()))
.await
.expect("missing required arg should be returned as tool result");
assert_eq!(missing_arg.is_error, Some(true));
assert!(tool_text(&missing_arg).contains("Missing required argument(s): path"));
shutdown(client).await;
}
#[tokio::test(flavor = "current_thread")]
async fn subprocess_and_direct_server_paths_return_expected_text() {
let client = launch_example("stderr_success")
.await
.expect("stderr success client should launch");
let success = client
.call_tool(
CallToolRequestParams::new("succeed-with-stderr")
.with_arguments(serde_json::Map::new()),
)
.await
.expect("successful subprocess call should succeed");
let success_text = tool_text(&success);
assert!(success_text.contains("stdout ok"));
assert!(success_text.contains("stderr:\nstderr note"));
shutdown(client).await;
let client = launch_example("subprocess_exit_handling")
.await
.expect("subprocess exit client should launch");
let failure = client
.call_tool(CallToolRequestParams::new("exit-fail").with_arguments(serde_json::Map::new()))
.await
.expect("non-zero exit should still yield a tool result");
assert_eq!(failure.is_error, Some(true));
assert!(tool_text(&failure).contains("exited with non-zero status"));
shutdown(client).await;
let client = launch_example("placeholder_server")
.await
.expect("placeholder server client should launch");
let placeholder =
client
.call_tool(CallToolRequestParams::new("echo").with_arguments(
serde_json::Map::from_iter([("message".to_string(), serde_json::json!("hi"))]),
))
.await
.expect("placeholder call should succeed");
assert!(tool_text(&placeholder).contains("Would invoke clap command 'echo'"));
shutdown(client).await;
let client = launch_example("invalid_executable_server")
.await
.expect("invalid executable client should launch");
let invalid =
client
.call_tool(CallToolRequestParams::new("echo").with_arguments(
serde_json::Map::from_iter([("message".to_string(), serde_json::json!("hi"))]),
))
.await
.expect("invalid executable path should still yield a tool result");
assert_eq!(invalid.is_error, Some(true));
assert!(tool_text(&invalid).contains("Failed to run command"));
shutdown(client).await;
}
#[tokio::test(flavor = "current_thread")]
#[cfg(unix)]
async fn capture_stdout_merges_only_text_outputs() {
let client = launch_example("capture_stdout")
.await
.expect("capture stdout client should launch");
let printed_only = client
.call_tool(
CallToolRequestParams::new("printed-only").with_arguments(serde_json::Map::new()),
)
.await
.expect("printed-only call should succeed");
assert_eq!(tool_text(&printed_only), "captured only");
let printed_and_text = client
.call_tool(
CallToolRequestParams::new("printed-and-text").with_arguments(serde_json::Map::new()),
)
.await
.expect("printed-and-text call should succeed");
assert_eq!(
tool_text(&printed_and_text),
"returned text\ncaptured extra"
);
let structured = client
.call_tool(CallToolRequestParams::new("structured").with_arguments(serde_json::Map::new()))
.await
.expect("structured call should succeed");
assert!(tool_text(&structured).contains("\"status\": \"ok\""));
assert_eq!(
structured
.structured_content
.as_ref()
.and_then(|content| content.get("status"))
.and_then(|value| value.as_str()),
Some("ok")
);
shutdown(client).await;
}
#[tokio::test(flavor = "current_thread")]
async fn logging_enabled_servers_expose_the_logging_guide_prompt() {
let client = launch_example("tracing_bridge")
.await
.expect("tracing bridge client should launch");
let prompts = client
.list_prompts(None)
.await
.expect("prompt list should succeed")
.prompts;
assert!(
prompts
.iter()
.any(|prompt| prompt.name == clap_mcp::PROMPT_LOGGING_GUIDE)
);
let guide = client
.get_prompt(GetPromptRequestParams::new(clap_mcp::PROMPT_LOGGING_GUIDE))
.await
.expect("logging guide prompt should resolve");
assert!(prompt_has_text(&guide.messages, "logger"));
shutdown(client).await;
}