> ## 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 reference

> Options, transports, delivery, logging, and limits of the AgentCat Rust SDK.

This page describes the `agentcat-sdk` crate, version 1.x, for `rmcp` 3.x. For the first setup, see [Rust setup](https://docs.agentcat.com/languages/rust.md). The [crate guide](https://docs.rs/agentcat-sdk) on docs.rs is the full API reference.

## Options

`Options::default()` has the defaults below. Each setter returns the changed options, which you pass to `AgentCat::new(project_id, options)`, `AgentCat::from_env(options)`, or `agentcat_sdk::track(server, project_id, options)`.

| Setter | Default | What it does |
| - | - | - |
| `identify` | none | An async closure that takes a `ToolCall` and returns `Result<Option<UserIdentity>, HookError>`. Runs on every tool call and names the user. See [Identifying users](https://docs.agentcat.com/languages/rust.md#identifying-users). |
| `enable_tool_call_context` | `true` | Adds a `context` parameter to each tool, where the agent explains why it calls the tool. This is the source of intent data. |
| `custom_context_description` | built-in text | Replaces the description of the `context` parameter. |
| `enable_report_missing` | `true` | Adds the `get_more_tools` tool, so agents can report tools they could not find. |
| `enable_tracing` | `true` | Records tool calls and adds `session_id` to each tool. |
| `enable_agent_tracking` | `false` | Adds an `agent_id` parameter to each tool. Each agent makes its own ID and sends it on every call, and AgentCat tags the event `agentcat_agent_id`. A strict client can refuse a call without it, so it is off by default. |
| `resolve_session_id` | none | An async closure that takes a `ToolCall` and returns your own task ID. AgentCat then adds no `session_id` parameter to the tools. |
| `redact_sensitive_information` | none | An async closure that changes each text value of the event (`String` in, `String` out) before the event leaves your server. |
| `redact_event` | none | An async closure that takes the full `Event` and returns the changed event, or `Ok(None)` to drop it. It runs first. |
| `event_tags` | none | Takes a `ToolCall` and returns a `serde_json::Map` of tags that you can filter on. AgentCat drops invalid entries and logs each one. See [Tags and properties](https://docs.agentcat.com/sdk/event-tags-properties.md). |
| `event_properties` | none | Takes a `ToolCall` and returns a `serde_json::Map` of JSON data for each event. |
| `api_base_url` | `https://api.agentcat.com` | The address that receives events. If not set, the SDK reads `AGENTCAT_API_URL`, then `MCPCAT_API_URL`. |
| `log_destination` | `LogDestination::File` | Writes the SDK log to `~/agentcat.log`. `LogDestination::Tracing` sends it to `tracing` instead. |

> [!NOTE]
>
> We recommend the defaults of `enable_tool_call_context`, `enable_report_missing`, and `enable_tracing`, because each feeds a part of the dashboard: intent data, missing-tool reports, and sessions.

`UserIdentity::new(user_id)` takes the user ID, and the setters `user_name` and `user_data` (a `serde_json::Map`) add the optional fields.

An empty project ID turns on telemetry-only mode: the handle builds each event and runs your hooks, but sends nothing. `AgentCat::from_env` does the same when `AGENTCAT_PROJECT_ID` is unset or empty.

The SDK reads these environment variables:

| Variable | What it does |
| - | - |
| `AGENTCAT_PROJECT_ID` | The project ID that `AgentCat::from_env` reads. |
| `AGENTCAT_API_URL` | The address for events, when `api_base_url` is not set. `MCPCAT_API_URL` is the old name. |
| `AGENTCAT_ENABLED` | `false`, `0`, `no`, or `off`, in any case, turns the SDK off. |

The SDK has no exporters or diagnostics reports yet. When `AGENTCAT_ENABLED` turns the SDK off, a handle starts no thread and its `Tracked` servers pass every request to your handler untouched. Custom events publish nothing, and `flush` and `shutdown` return `true`.

## Framework and transport details

### rmcp 3.x

* **Stateless HTTP.** `StreamableHttpService::new` calls your closure for each request, and the setup snippet wraps the new handler there with `AgentCat::track`. It also sets `with_legacy_session_mode(false)` and `with_json_response(true)` and mounts the service in `axum` with `route_service`. On a stateless server, an event names the client only when the request `_meta` does. The default `allowed_hosts` of `rmcp` accepts only loopback hosts, so a server behind a public hostname needs `with_allowed_hosts`.
* **Streamable HTTP with sessions.** The default `StreamableHttpServerConfig` keeps one MCP session per client and answers with server-sent events, for clients on protocol versions before 2026-07-28. Requests that negotiate 2026-07-28 always get stateless service. `rmcp` calls your closure once per new session. The `session_id` parameter groups calls on every transport, and the SDK ignores the `mcp-session-id` header.
* **stdio.** `AgentCat::from_env(options).track(server)` returns a `Tracked` server that you serve with `rmcp::transport::stdio()`.
* **The handle.** `AgentCat` starts no thread until the first event or log line, and a clone is cheap and shares everything with the original. We recommend one handle for the process.
* **Crate features.** `rustls` (default) or `native-tls` (with `default-features = false`) connects to the AgentCat API, and `test-util` adds the `testing` module. Without a TLS feature, the SDK can send events only to an `http:` URL.

### What `track()` changes in your server

* Each tool gets `session_id` and `context` parameters (and `agent_id` with `enable_agent_tracking(true)`), and the tool list gets `get_more_tools`. AgentCat removes the injected values before your tool runs.
* The first call of a task gets a text block that begins `[session_id issued` before the result content, error results included. A JSON-RPC error (`Err(ErrorData)`) has no content, so it gets no block.
* A `session_id` that is neither `start` nor a valid `ses_` ID gets a text block that begins `[session_id unrecognized`, and the call has no session.
* When a tool declares an output schema, AgentCat adds an `mcp_session` property to it, and a structured result gets `mcp_session` as its first key.
* A tool that declares its own `session_id`, `agent_id`, or `context` keeps it, and AgentCat adds none.
* A hand-written `ServerHandler` without `get_tool` depends on the registry of tool listings that the SDK keeps. The `#[tool_handler]` macro writes `get_tool` for you.

### The call context in a tool

A tool reads its `CallContext` as an `Extension<CallContext>` parameter, next to `Parameters<..>`:

* `session_id()` returns the session of the call, or `None`. `agent_id()` returns the `agent_id` that the agent sent, when agent tracking is on.
* `agentcat()` returns the handle that tracks the call.
* `publish_custom_event(project_id, event)` queues a [custom event](https://docs.agentcat.com/sdk/custom-events.md) with the client and the server of this call, and returns at once.

To attach the event to the session of the call, pass `call.session_id()` to `CustomEvent::session_id`. Code outside a tool call has no `CallContext`, so it uses `AgentCat::publish_custom_event(session_id, project_id, event)` or `Tracked::publish_custom_event(project_id, event)`. Both only queue the event, which goes to the project of the handle, so the project ID argument only has to be non-empty.

### Errors

* A tool that returns `Err(ErrorData)` gives the client a JSON-RPC error, and the event gets the name of the error code as `error.type`. Event errors have the platform `rust` and no stack frames.
* A cancelled tool call gives the error type `cancelled`. A panic in a tool is recorded, then goes on unchanged.
* `rmcp` turns a returned error into the text of an error result. Calling `capture_error(&error)` before the tool returns it adds the error type and the `source()` chain texts to the event. It works in the tool's own future only, and the last call counts.

## Runtimes, delivery, and shutdown

The SDK needs a tokio runtime with the time driver. `#[tokio::main]` and `Runtime::new` turn it on, and a runtime from `tokio::runtime::Builder` needs `enable_time` or `enable_all`.

The `identify`, `event_tags`, `event_properties`, and `resolve_session_id` hooks run as `Send` tasks on the tokio runtime of the tool call, so they never delay it. AgentCat waits up to 30 seconds per hook, then sends the event without that value. Redaction hooks have no time limit.

Each `AgentCat` handle delivers its events from a thread of its own, `agentcat-delivery`, so a tool call never waits for the network. The queue holds up to 10,000 events and drops the oldest when full. Up to 5 events are in progress at a time, and AgentCat tries a failed send 3 more times (after 1, 2, and 4 seconds, with a timeout of 10 seconds each).

| Server | When the last events go out | What you do |
| - | - | - |
| stdio | The client closes stdin and the last handle drops (up to 5 seconds). | Nothing |
| Streamable HTTP, with or without sessions | When you call `AgentCat::shutdown` after the server stops. | Call `shutdown` after `axum::serve` returns, and stop on SIGTERM as well as Ctrl-C. |
| Any server on a current-thread runtime | When you call `AgentCat::shutdown` before the runtime ends. | See the current-thread note below. |

* `AgentCat::flush(timeout)` waits up to `timeout` until the handle delivers every event. `AgentCat::shutdown(timeout)` does the same, then stops the handle for good. Both return `true` when the events went out in time. `flush_blocking` and `shutdown_blocking` block the calling thread and need no tokio runtime.
* The drain of the last drop does not run after `std::process::exit`, or after a panic with `panic = "abort"`. A `shutdown` call before them sends the queued events.
* On a current-thread runtime, the drop of the last handle blocks the only thread, so no hook can run, and the SDK drops an event that still needs a hook after 5 seconds. Calling `shutdown` before the runtime ends avoids this. The server that `agentcat_sdk::track` returns holds the only clone of its handle, so you can clone the handle with `Tracked::agentcat` before you serve and call `shutdown` on that clone.
* A hook that holds a clone of its own handle makes a reference cycle, so the handle never drops. Hooks that capture only what they need avoid this, and a tool reaches its handle through `CallContext::agentcat`.

## Logging and debug mode

The SDK has no debug option. By default it writes its log to `~/agentcat.log`, never to stdout or stderr, because stdout carries JSON-RPC on a stdio server. With no home directory, the SDK writes no log. Each line holds the time, the SDK, Rust, and `rmcp` versions, 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 ... | <user>
```

`log_destination(LogDestination::Tracing)` sends the lines to `tracing` instead, with the target `agentcat`. They need a global subscriber, such as one from `tracing::subscriber::set_global_default`, because the delivery thread emits them. The first handle built in the process sets the log destination for the whole process.

On a stdio server, the `fmt` subscriber of `tracing-subscriber` writes to stdout unless you give it another writer, for example `.with_writer(std::io::stderr)`.

## Testing

The SDK does not detect test runs, so AgentCat stays on in tests unless `AGENTCAT_ENABLED` is `false`. The `test-util` feature adds `testing::Fake`, which builds handles whose events stay in the process, recorded after hooks and redaction:

```toml theme={null}
[dev-dependencies]
agentcat-sdk = { version = "1", features = ["test-util"] }
```

```rust theme={null}
let fake = agentcat_sdk::testing::Fake::new();
let server = fake.agentcat("proj_test", Options::default()).track(Calculator);
// Serve `server` to an rmcp client, call a tool, then:
fake.assert_tracked("add").await;
```

`Fake::events` returns the recorded events. `Fake` also has `assert_tracked_count`, `assert_nothing_tracked`, and `assert_custom_event_published`. The first call of a task starts with the `[session_id issued` block, which matters for tests that read the first content block.

## Differences and known limits

* Only `tools/call` requests give events, one each, plus the custom events that your code publishes. Tool listings, resources, and prompts give none. Events go to AgentCat only, as the SDK has no exporters.
* On streamable HTTP, a tool call event carries every request header, `authorization` and `cookie` included, under `parameters.extra.requestInfo.headers`, and the request URL under `parameters.extra.requestInfo.url`. A stdio server sends no headers. A `redact_event` hook can remove what you prefer not to send.
* `axum::Router::nest_service` removes the path prefix before the request reaches the service, so the event records the URL without it. `route_service` keeps the full path.
* The SDK turns on the `preserve_order` feature of `serde_json`, which Cargo applies to the whole build, so every `serde_json::Map` in your program keeps insertion order.
* The `local` feature of `rmcp` is not supported, as hooks run as `Send` tasks.
* A redaction hook that never completes holds one of the 5 delivery slots for good, and five such hooks stop delivery.
* The registry of tool listings never removes an entry, so tool names that differ per tenant grow it.
* When another crate turns on the `arbitrary_precision` or `float_roundtrip` feature of `serde_json`, the text of some numbers in events can change. The SDK logs a warning that names the feature.
* Rust 1.88 or later is supported, following `rmcp` 3. An increase of the minimum is a minor release.


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