use std::io::Write as _;
use std::path::PathBuf;
use std::sync::Arc;
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, ServerHandler, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
struct RecordArgs {
line: String,
}
#[derive(Clone)]
struct Fixture {
tool_router: ToolRouter<Self>,
count_file: Arc<PathBuf>,
}
#[tool_router]
impl Fixture {
fn new(count_file: PathBuf) -> Self {
Self {
tool_router: Self::tool_router(),
count_file: Arc::new(count_file),
}
}
#[tool(description = "Record one line by appending it durably to the count file.")]
async fn record(
&self,
Parameters(RecordArgs { line }): Parameters<RecordArgs>,
) -> Result<CallToolResult, ErrorData> {
let appended = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(self.count_file.as_ref())
.and_then(|mut file| writeln!(file, "{line}"));
match appended {
Ok(()) => Ok(CallToolResult::success(vec![ContentBlock::text(format!(
"recorded: {line}"
))])),
Err(error) => Ok(CallToolResult::error(vec![ContentBlock::text(format!(
"failed to record: {error}"
))])),
}
}
}
#[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 CLI counting fixture server.")
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let count_file = std::env::args()
.nth(1)
.expect("the count-file path is required as the first argument");
let service = Fixture::new(PathBuf::from(count_file))
.serve(stdio())
.await?;
service.waiting().await?;
Ok(())
}