agentcat-sdk, tracks MCP servers built on rmcp, the official Rust MCP SDK. It runs your hooks on the tokio runtime of each tool call, and it sends the events from a thread of its own, so a tool call never waits for the network. This page says what the SDK needs from your runtime, how it delivers the events, what you do so that the last events go out when your server stops, and where the SDK logs. Rust: rmcp shows how to track each kind of server. The crate guide on docs.rs is the full reference, and Rust: behavior and limits lists the supported versions and the known limits.
Overview
The tokio runtime
- The time driver. A tracked server needs a tokio runtime with the time driver. The SDK times a rebuild of its tool registry with
tokio::time::timeout, and tokio panics at such a timer on a runtime without the driver.rmcpuses timers too.#[tokio::main],#[tokio::test]andtokio::runtime::Runtime::newturn the time driver on. A runtime fromtokio::runtime::Builderneedsenable_timeorenable_all. - Hooks run on the runtime of the call. The SDK starts the
identify,event_tags,event_propertiesandresolve_session_idhooks asSendtasks on the tokio runtime of the tool call, so it does not support thelocalfeature ofrmcp. The call never waits for these hooks, so a slow hook never delays a tool call. When the event is ready to go out, the SDK waits at most 30 seconds for each hook. It stops a hook that has not completed by then, and the event goes out without the value of that hook. - Redaction hooks.
redact_eventand thenredact_sensitive_informationrun on the tokio runtime that serves the call too, before the event leaves the process. They have no time limit (see Known limits).
Current-thread runtimes
On a current-thread runtime, such as#[tokio::main(flavor = "current_thread")], a drop of the last handle on the thread of the runtime blocks its only thread. Then no hook can run during the wait, and the SDK drops an event that still needs a hook after 5 seconds. On such a runtime, call shutdown before the runtime ends, while the hooks can still run:
agentcat_sdk::track returns holds the only clone of its handle. On a current-thread runtime, clone the handle with Tracked::agentcat before you serve, and call its shutdown before the runtime ends.
The delivery thread
EachAgentCat handle delivers its events from its own thread, agentcat-delivery. The first event or log line of the handle starts the thread. A tool call only puts its event in the queue of the handle, so the call never waits for the network. The delivery has these limits:
- The queue holds at most 10,000 events. When it is full, the SDK drops the oldest event.
- At most 5 events of a handle are in progress at a time.
- The SDK tries a failed send 3 more times, after 1, 2 and 4 seconds. Each attempt has a timeout of 10 seconds.
api_base_url option, else AGENTCAT_API_URL, else MCPCAT_API_URL, else https://api.agentcat.com. A clone of a handle is cheap and shares everything, so build one handle for the process and clone it where you need it.
Stdio servers
A stdio server ends when the client closes stdin. Then the last clone of the handle drops, and the drop delivers the queued events, for up to 5 seconds. A stdio server needs noshutdown call:
Streamable HTTP servers
A streamable HTTP server builds a new server handler for each MCP session, or for each request on a stateless server. Build oneAgentCat handle for the process, and wrap each new handler with AgentCat::track. A clone of the handle is cheap, and track does no other work, so the handle can serve every session.
This server needs the transport-streamable-http-server feature of rmcp, the macros, rt-multi-thread, net and signal features of tokio, and axum. Calculator is the server of the quickstart:
AgentCat::shutdown after your server stops: the queued events then go out before the process ends.
Flush and shutdown
Your code can wait for the delivery:AgentCat::flush(timeout)waits until the handle delivered every event that it took, for at mosttimeout. It returnstruewhen that happened in time, and returnstrueat once when the handle has taken no event yet. Events still go out after afalse.AgentCat::shutdown(timeout)does the same, then stops the handle for good: the handle drops every later event, for all its clones. A secondshutdownafter the first has ended returnstrueat once.AgentCat::flush_blockingandAgentCat::shutdown_blockingblock the calling thread instead. They need no tokio runtime. On a thread of a runtime, they block that thread.
flush or shutdown needs no timer.
The drain of the last drop
When the last clone of a handle drops, the SDK runsshutdown with a timeout of 5 seconds. The Tracked servers hold clones of the handle too. The drop blocks the dropping thread until the shutdown ends: at most about 5 seconds, less when the events finish sooner. The events that have not finished at the deadline are dropped. A drop on the delivery thread itself does not wait, and a handle whose delivery thread never started has nothing to drain.
The drain never runs in these cases:
- The process ends at once. The drain does not run after
std::process::exit, or after a panic withpanic = "abort". - A hook holds a clone of its handle. The handle holds its options, and the options hold the hook, so the handle never drops. Let the hook capture only what it needs, or call
shutdownbefore the process ends. A tool reaches its handle throughCallContext::agentcatand needs no clone.
Environment variables
A telemetry-only handle (an empty project ID) builds each event and runs the hooks, but sends nothing to AgentCat.
When
AGENTCAT_ENABLED turns the SDK off, a handle starts no thread, and its Tracked servers forward every request to your handler untouched. A tool still gets a CallContext, with no session. Custom events publish nothing, and flush and shutdown return true. The SDK has no test detection: it stays on in tests unless you set this variable.
Logging
The SDK never writes to stdout or stderr. It writes its own log lines to the fileagentcat.log in your home directory (~/agentcat.log). Each line holds the time, the versions of the SDK, of Rust and of rmcp, and the message:
tracing instead, set LogDestination::Tracing:
- The target. The
tracingevents have the targetagentcat. A line about a failure or an error is anERRORevent. A warning is aWARNevent, and any other line is anINFOevent. - A global subscriber. The delivery thread of the handle emits the events, so set a global subscriber, for example with
tracing::subscriber::set_global_default. A subscriber thatwith_defaultsets on another thread does not see them. - One destination for the process. The log destination applies to the whole process. The first handle built in the process decides it.
Logging on stdio servers
A stdio server talks JSON-RPC on stdout, so any other text on stdout breaks the connection to the client. Send your own logs and yourtracing output to stderr. The fmt subscriber of tracing-subscriber writes to stdout unless you give it another writer, for example .with_writer(std::io::stderr). This also holds when you send the lines of the SDK to tracing.