> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcat.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rust runtimes

> How the Rust SDK runs: the tokio runtime it needs, the delivery thread, flush and shutdown on stdio and streamable HTTP servers, the environment variables, and logging to a file or to tracing.

The Rust SDK, `agentcat-sdk`, tracks MCP servers built on [`rmcp`](https://github.com/modelcontextprotocol/rust-sdk), 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](https://docs.agentcat.com/sdk/rust/rmcp.md) shows how to track each kind of server. The [crate guide](https://docs.rs/agentcat-sdk) on docs.rs is the full reference, and [Rust: behavior and limits](https://docs.agentcat.com/sdk/rust/limits.md) lists the supported versions and the known limits.

## Overview

| Server | When the last events go out | What you do |
| - | - | - |
| stdio | When the client closes stdin, the server ends, and the drop of the last handle delivers the queued events, for up to 5 seconds | Nothing |
| Streamable HTTP, stateful or stateless | When you call `AgentCat::shutdown` after the server stops | Call `shutdown` after `axum::serve` returns |
| Any server on a current-thread runtime | When you call `AgentCat::shutdown` before the runtime ends | Call `shutdown` before the runtime ends |

## 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](https://docs.agentcat.com/sdk/rust/limits.md#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:

```rust theme={null}
use std::time::Duration;

// ... serve until the server stops ...
let delivered = agentcat.shutdown(Duration::from_secs(5)).await;
if !delivered {
    // Some events did not go out within 5 s.
}
```

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:

```rust theme={null}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Reads AGENTCAT_PROJECT_ID, and AGENTCAT_API_URL when set. Without a project ID, the handle
    // builds events but sends none.
    let server = AgentCat::from_env(Options::default()).track(Demo);

    // Serve the wrapper where you served `Demo`.
    let running = server.serve(stdio()).await?;
    running.waiting().await?;
    Ok(())
}
```

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](#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](https://docs.agentcat.com/sdk/stateless-servers.md). 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](https://docs.agentcat.com/quickstart.md):

```rust theme={null}
use std::sync::Arc;
use std::time::Duration;

use agentcat_sdk::{AgentCat, Options};
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use rmcp::transport::streamable_http_server::{StreamableHttpServerConfig, StreamableHttpService};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // One handle for the whole process.
    let agentcat = AgentCat::new("proj_...", Options::default());

    // rmcp calls this closure for each new MCP session. Wrap each new server.
    let handle = agentcat.clone();
    let service = StreamableHttpService::new(
        move || Ok(handle.track(Calculator)),
        Arc::new(LocalSessionManager::default()),
        StreamableHttpServerConfig::default(),
    );
    let router = axum::Router::new().route_service("/mcp", service);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await?;
    tokio::select! {
        served = axum::serve(listener, router) => served?,
        _ = tokio::signal::ctrl_c() => {}
    }

    // Deliver the queued events before the process ends.
    agentcat.shutdown(Duration::from_secs(5)).await;
    Ok(())
}
```

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

| Variable | Effect |
| - | - |
| `AGENTCAT_PROJECT_ID` | The project ID that `AgentCat::from_env` reads. When it is unset or empty, the handle is telemetry only. |
| `AGENTCAT_API_URL` | The base URL of the AgentCat API, when the options set none. |
| `MCPCAT_API_URL` | The former name of `AGENTCAT_API_URL`, read when that one is unset or empty. |
| `AGENTCAT_ENABLED` | `false`, `0`, `no` or `off`, in any case, turns the SDK off. Any other value, or none, leaves it on. |

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:

```text theme={null}
[2026-10-28T09:15:02.137Z] [sdk=1.0.0 rust=1.88.0 rmcp=3.6.0] Successfully sent event ...
```

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`:

```rust theme={null}
use agentcat_sdk::{LogDestination, Options};

let options = Options::default().log_destination(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`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.