#![expect(
clippy::print_stderr,
reason = "this example logs to stderr; AgentCat writes nothing to stdout or stderr itself"
)]
#![expect(
clippy::doc_markdown,
reason = "AgentCat is the name of the product, not code"
)]
#![expect(
clippy::unused_async_trait_impl,
reason = "the methods #[tool_handler] generates are async without an await"
)]
use std::sync::Arc;
use std::time::Duration;
use agentcat_sdk::{AgentCat, Options};
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{Implementation, InitializeResult, ServerCapabilities};
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use rmcp::transport::streamable_http_server::{StreamableHttpServerConfig, StreamableHttpService};
use rmcp::{ServerHandler, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::Deserialize;
use tokio::net::TcpListener;
#[derive(Deserialize, JsonSchema)]
struct AddArgs {
a: f64,
b: f64,
}
#[derive(Deserialize, JsonSchema)]
struct EchoArgs {
text: String,
}
#[derive(Clone)]
struct Demo;
#[tool_router]
impl Demo {
#[tool(description = "Adds two numbers")]
async fn add(&self, Parameters(args): Parameters<AddArgs>) -> String {
(args.a + args.b).to_string()
}
#[tool(description = "Sends the text back")]
async fn echo(&self, Parameters(args): Parameters<EchoArgs>) -> String {
args.text
}
}
#[tool_handler]
impl ServerHandler for Demo {
fn get_info(&self) -> InitializeResult {
InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(Implementation::new(
"agentcat-example-stateless-http",
"1.0.0",
))
}
}
#[expect(
clippy::disallowed_methods,
reason = "an example reads its port from the environment"
)]
fn port() -> u16 {
std::env::var("PORT")
.ok()
.and_then(|port| port.parse().ok())
.unwrap_or(8000)
}
async fn stop_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut terminate) = signal(SignalKind::terminate()) {
tokio::select! {
_ = terminate.recv() => {}
_ = tokio::signal::ctrl_c() => {}
}
return;
}
}
let _ = tokio::signal::ctrl_c().await;
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let agentcat = AgentCat::from_env(Options::default());
let handle = agentcat.clone();
let service = StreamableHttpService::new(
move || Ok(handle.track(Demo)),
Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default()
.with_legacy_session_mode(false)
.with_json_response(true),
);
let router = axum::Router::new().route_service("/mcp", service);
let listener = TcpListener::bind(("127.0.0.1", port())).await?;
eprintln!("listening on http://{}/mcp", listener.local_addr()?);
tokio::select! {
served = axum::serve(listener, router) => served?,
() = stop_signal() => {}
}
let delivered = agentcat.shutdown(Duration::from_secs(5)).await;
eprintln!("stopped; all events delivered: {delivered}");
Ok(())
}