use rmcp::ServiceExt;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{CallToolResult, ContentBlock, Meta, ServerCapabilities, ServerInfo};
use rmcp::transport::stdio;
use rmcp::{ErrorData, Json, ServerHandler, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use time::format_description::well_known::Rfc3339;
use time::{Duration, OffsetDateTime};
fn signal_schema() -> Value {
json!({
"type": "object",
"properties": {"paid": {"type": "boolean"}},
"required": ["paid"],
})
}
fn salvor_meta(request: Value) -> Meta {
let mut meta = Meta::new();
meta.insert("salvor".to_owned(), request);
meta
}
#[derive(Debug, Deserialize, JsonSchema)]
struct AppendArgs {
line: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct NapArgs {
seconds: i64,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct BadParkArgs {
shape: String,
}
#[derive(Debug, Serialize, JsonSchema)]
struct Receipt {
settlement_id: String,
}
#[derive(Clone)]
struct Fixture {
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl Fixture {
fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(
description = "Read the note. Observes state only.",
annotations(read_only_hint = true)
)]
async fn read_note(&self) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![ContentBlock::text(
"the note says hello",
)]))
}
#[tool(
description = "Append a line to the note. Idempotent for a given line.",
annotations(idempotent_hint = true)
)]
async fn append_note(
&self,
Parameters(AppendArgs { line }): Parameters<AppendArgs>,
) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"appended: {line}"
))]))
}
#[tool(description = "Do something with unstated effects.")]
async fn mutate(&self) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![ContentBlock::text("mutated")]))
}
#[tool(description = "Always fails with a tool-reported error.")]
async fn explode(&self) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::error(vec![ContentBlock::text(
"boom: the explode tool always fails",
)]))
}
#[tool(
description = "Park the calling run until the given number of seconds from now.",
annotations(read_only_hint = true)
)]
async fn nap(
&self,
Parameters(NapArgs { seconds }): Parameters<NapArgs>,
) -> Result<CallToolResult, ErrorData> {
let wake_at = OffsetDateTime::now_utc() + Duration::seconds(seconds);
let wake_at = wake_at
.format(&Rfc3339)
.map_err(|error| ErrorData::internal_error(error.to_string(), None))?;
Ok(
CallToolResult::success(vec![ContentBlock::text(format!("napping until {wake_at}"))])
.with_meta(Some(salvor_meta(json!({"sleep_until": wake_at})))),
)
}
#[tool(
description = "Park the calling run until an external system reports back.",
annotations(read_only_hint = true)
)]
async fn await_signal(&self) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![ContentBlock::text(
"waiting on the settlement webhook",
)])
.with_meta(Some(salvor_meta(json!({"suspend": {
"reason": "waiting on the settlement webhook",
"input_schema": signal_schema(),
"kind": "signal",
}})))))
}
#[tool(
description = "Park the calling run until a person answers.",
annotations(read_only_hint = true)
)]
async fn await_person(&self) -> Result<CallToolResult, ErrorData> {
Ok(
CallToolResult::success(vec![ContentBlock::text("a person must confirm this")])
.with_meta(Some(salvor_meta(json!({"suspend": {
"reason": "a person must confirm this",
"input_schema": signal_schema(),
}})))),
)
}
#[tool(description = "Return a malformed park request of the named shape.")]
async fn bad_park(
&self,
Parameters(BadParkArgs { shape }): Parameters<BadParkArgs>,
) -> Result<CallToolResult, ErrorData> {
let namespace = match shape.as_str() {
"not_an_object" => json!("suspend please"),
"both" => json!({
"suspend": {"reason": "either way", "input_schema": signal_schema()},
"sleep_until": "2026-08-14T09:00:00Z",
}),
"unknown_key" => json!({"sleepUntil": "2026-08-14T09:00:00Z"}),
"bad_timestamp" => json!({"sleep_until": "in about an hour"}),
"no_reason" => json!({"suspend": {"input_schema": signal_schema()}}),
"error_and_park" => {
return Ok(CallToolResult::error(vec![ContentBlock::text(
"the settlement service refused",
)])
.with_meta(Some(salvor_meta(
json!({"sleep_until": "2026-08-14T09:00:00Z"}),
))));
}
other => {
return Err(ErrorData::invalid_params(
format!("unknown bad_park shape `{other}`"),
None,
));
}
};
Ok(
CallToolResult::success(vec![ContentBlock::text("asking for a park")])
.with_meta(Some(salvor_meta(namespace))),
)
}
#[tool(description = "Stamp a settlement receipt.")]
async fn stamp_receipt(&self) -> Result<Json<Receipt>, ErrorData> {
Ok(Json(Receipt {
settlement_id: "settlement-123".to_owned(),
}))
}
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for Fixture {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_instructions("Salvor MCP integration-test fixture server.")
}
}
fn record(var: &str, value: u32) {
if let Ok(path) = std::env::var(var) {
let _ = std::fs::write(path, value.to_string());
}
}
#[cfg(unix)]
fn ignore_polite_signals() {
unsafe {
for sig in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP, libc::SIGPIPE] {
libc::signal(sig, libc::SIG_IGN);
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let stubborn = std::env::var_os("SALVOR_MCP_FIXTURE_STUBBORN").is_some();
#[cfg(unix)]
if stubborn {
ignore_polite_signals();
}
record("SALVOR_MCP_FIXTURE_PIDFILE", std::process::id());
if std::env::var_os("SALVOR_MCP_FIXTURE_GRANDCHILD").is_some() {
let child = std::process::Command::new("sleep")
.arg("300")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.spawn()?;
record("SALVOR_MCP_FIXTURE_GRANDCHILD", child.id());
}
let served = async {
let service = Fixture::new().serve(stdio()).await?;
service.waiting().await?;
Ok::<(), Box<dyn std::error::Error>>(())
}
.await;
if stubborn {
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
}
served
}