#![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 agentcat_sdk::{
AgentCat, CallContext, CustomEvent, Event, HookError, Options, ToolCall, UserIdentity,
capture_error,
};
use rmcp::handler::server::common::Extension;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{Implementation, InitializeResult, ServerCapabilities};
use rmcp::transport::stdio;
use rmcp::{ServerHandler, ServiceExt, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{Map, Value};
#[derive(Deserialize, JsonSchema)]
struct AddArgs {
a: f64,
b: f64,
}
#[derive(Deserialize, JsonSchema)]
struct EchoArgs {
text: String,
}
#[derive(Clone)]
struct Demo {
project_id: String,
}
#[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>,
Extension(call): Extension<CallContext>,
) -> String {
let mut event = CustomEvent::new()
.resource_name("echo-called")
.message("the echo tool ran");
if let Some(session) = call.session_id() {
event = event.session_id(session);
}
let _ = call.publish_custom_event(&self.project_id, event);
args.text
}
#[tool(description = "Always fails")]
async fn fail(&self) -> Result<String, String> {
let error = std::io::Error::new(std::io::ErrorKind::NotFound, "the record does not exist");
capture_error(&error);
Err(error.to_string())
}
}
#[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-hooks", "1.0.0"))
}
}
fn one_entry(key: &str, value: &str) -> Map<String, Value> {
Map::from_iter([(key.to_owned(), Value::String(value.to_owned()))])
}
fn options() -> Options {
Options::default()
.identify(|_call: ToolCall| async move {
Ok::<_, HookError>(Some(UserIdentity::new("demo-user").user_name("Demo User")))
})
.event_tags(|_call: ToolCall| async move {
Ok::<_, HookError>(Some(one_entry("example", "hooks")))
})
.event_properties(|_call: ToolCall| async move {
Ok::<_, HookError>(Some(one_entry("source", "hooks-example")))
})
.redact_sensitive_information(|text: String| async move {
Ok::<_, HookError>(text.replace("secret-token", "[REDACTED]"))
})
.redact_event(|mut event: Event| async move {
event.properties.get_or_insert_with(Map::new).insert(
"redacted_by".to_owned(),
Value::String("example".to_owned()),
);
Ok::<_, HookError>(Some(event))
})
}
#[expect(
clippy::disallowed_methods,
reason = "an example reads its project ID from the environment"
)]
fn project_id() -> String {
std::env::var(agentcat_sdk::AGENTCAT_PROJECT_ID).unwrap_or_default()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let demo = Demo {
project_id: project_id(),
};
let server = AgentCat::from_env(options()).track(demo);
let running = server.serve(stdio()).await?;
running.waiting().await?;
Ok(())
}