use rmcp::ServiceExt;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{CallToolResult, ContentBlock, ServerCapabilities, ServerInfo};
use rmcp::transport::stdio;
use rmcp::{ErrorData, Json, ServerHandler, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, JsonSchema)]
struct AppendArgs {
line: 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 = "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
}