Skip to main content
The Rust SDK, 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. rmcp uses timers too. #[tokio::main], #[tokio::test] and tokio::runtime::Runtime::new turn the time driver on. A runtime from tokio::runtime::Builder needs enable_time or enable_all.
  • Hooks run on the runtime of the call. The SDK starts the identify, event_tags, event_properties and resolve_session_id hooks as Send tasks on the tokio runtime of the tool call, so it does not support the local feature of rmcp. 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_event and then redact_sensitive_information run 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:
The server that 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

Each AgentCat 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.
Each handle has its own queue and its own API URL: the URL of the 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 no shutdown call:
A stdio server talks JSON-RPC on stdout. The SDK writes nothing to stdout or stderr. Send your own logs to stderr (see Logging on stdio servers).

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 one AgentCat 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:
An HTTP server does not end when a client goes away, and the SDK installs no signal handler. So call 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 most timeout. It returns true when that happened in time, and returns true at once when the handle has taken no event yet. Events still go out after a false.
  • AgentCat::shutdown(timeout) does the same, then stops the handle for good: the handle drops every later event, for all its clones. A second shutdown after the first has ended returns true at once.
  • AgentCat::flush_blocking and AgentCat::shutdown_blocking block the calling thread instead. They need no tokio runtime. On a thread of a runtime, they block that thread.
The delivery thread keeps the time, so the runtime that calls flush or shutdown needs no timer.

The drain of the last drop

When the last clone of a handle drops, the SDK runs shutdown 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 with panic = "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 shutdown before the process ends. A tool reaches its handle through CallContext::agentcat and 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 file agentcat.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:
When the SDK finds no home directory, it writes no log. It ignores the errors of the file. To send the lines to tracing instead, set LogDestination::Tracing:
  • The target. The tracing events have the target agentcat. A line about a failure or an error is an ERROR event. A warning is a WARN event, and any other line is an INFO event.
  • 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 that with_default sets 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 your tracing 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.