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

> Set up AgentCat in a Rust MCP server built on rmcp, with identity and every feature on.

> [!TIP]
>
> Your coding agent can do the setup for you with this prompt:
>
> ```text theme={null}
> Read and follow https://docs.agentcat.com/languages/rust.md to set up AgentCat in this MCP server. Follow its Guidance for AI agents.
> ```

## Supported MCP SDKs

| MCP library | Versions | Transports | Runtime |
| - | - | - | - |
| `rmcp` (official Rust MCP SDK) | 3.0.0 and later 3.x | stdio, streamable HTTP with sessions, stateless HTTP | Rust 1.88 or later, tokio with the time driver |

The `agentcat-sdk` crate tracks any `ServerHandler` from `rmcp`, whether the `#[tool_router]` macros or your own code write it. For a new server, we recommend stateless HTTP, because it matches the stateless MCP protocol of 2026-07-28.

## Installation

```bash Stateless HTTP theme={null}
cargo add agentcat-sdk
cargo add rmcp --features transport-streamable-http-server
cargo add axum
cargo add tokio --features macros,rt-multi-thread,net,signal
```

```bash Streamable HTTP with sessions theme={null}
cargo add agentcat-sdk
cargo add rmcp --features transport-streamable-http-server
cargo add axum
cargo add tokio --features macros,rt-multi-thread,net,signal
```

```bash stdio theme={null}
cargo add agentcat-sdk
cargo add rmcp --features transport-io
cargo add tokio --features macros,rt-multi-thread
```

The SDK needs your project ID, which you can find in the [AgentCat dashboard](https://agentcat.com/app/settings/projects/). We recommend the `AGENTCAT_PROJECT_ID` environment variable, which keeps the ID out of your code. `AgentCat::from_env` reads it for you.

## Adding AgentCat to your server

`AgentCat::from_env(options)` builds one handle for the whole process, and `track()` wraps a `ServerHandler` with AgentCat. In the snippets, `Calculator` stands for your own `ServerHandler`.

```rust Stateless HTTP 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. It reads AGENTCAT_PROJECT_ID.
    let agentcat = AgentCat::from_env(Options::default());

    // rmcp calls this closure for each request, so each new server gets tracked.
    let handle = agentcat.clone();
    let service = StreamableHttpService::new(
        move || Ok(handle.track(Calculator)), // Calculator is your own ServerHandler
        Arc::new(LocalSessionManager::default()),
        // No MCP sessions, and JSON responses instead of server-sent events.
        StreamableHttpServerConfig::default()
            .with_legacy_session_mode(false)
            .with_json_response(true),
    );
    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?,
        () = stop_signal() => {}
    }

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

/// Resolves on SIGTERM (Unix) or Ctrl-C.
async fn stop_signal() {
    #[cfg(unix)]
    {
        use tokio::signal::unix::{SignalKind, signal};
        if let Ok(mut terminate) = signal(SignalKind::terminate()) {
            tokio::select! {
                _ = terminate.recv() => {}
                _ = tokio::signal::ctrl_c() => {}
            }
            return;
        }
    }
    let _ = tokio::signal::ctrl_c().await;
}
```

```rust Streamable HTTP with sessions 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. It reads AGENTCAT_PROJECT_ID.
    let agentcat = AgentCat::from_env(Options::default());

    // rmcp calls this closure for each new MCP session, so each session gets a tracked server.
    let handle = agentcat.clone();
    let service = StreamableHttpService::new(
        move || Ok(handle.track(Calculator)), // Calculator is your own ServerHandler
        Arc::new(LocalSessionManager::default()),
        // MCP sessions and server-sent events, the defaults of rmcp (for clients on protocol versions before 2026-07-28).
        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?,
        () = stop_signal() => {}
    }

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

/// Resolves on SIGTERM (Unix) or Ctrl-C.
async fn stop_signal() {
    #[cfg(unix)]
    {
        use tokio::signal::unix::{SignalKind, signal};
        if let Ok(mut terminate) = signal(SignalKind::terminate()) {
            tokio::select! {
                _ = terminate.recv() => {}
                _ = tokio::signal::ctrl_c() => {}
            }
            return;
        }
    }
    let _ = tokio::signal::ctrl_c().await;
}
```

```rust stdio theme={null}
use agentcat_sdk::{AgentCat, Options};
use rmcp::ServiceExt;
use rmcp::transport::stdio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // The handle reads AGENTCAT_PROJECT_ID.
    let server = AgentCat::from_env(Options::default()).track(Calculator); // Calculator is your own ServerHandler

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

A stateless server builds a new handler for each request. Each `track()` call wraps one handler, so the call goes inside the closure that `rmcp` calls.

The HTTP tabs wait for a stop signal and then call `shutdown`, because the SDK installs no signal handler and containers stop a server with SIGTERM. A stdio server needs no `shutdown` call.

## Identifying users

> [!NOTE]
>
> We strongly recommend that you identify your users. Without identity, the dashboard cannot show who uses your server, and each session stays anonymous.

The `identify` hook tells AgentCat which user made each tool call. We recommend the first of these two levels that your server can give.

### Full identity (recommended)

Full identity gives each session a real user ID and name. The hook receives a `ToolCall`, and `call.headers()` holds the HTTP headers with lower-case names. This snippet takes the place of the `AgentCat::from_env` line of your tab:

```rust HTTP (both tabs) theme={null}
use agentcat_sdk::{AgentCat, HookError, Options, ToolCall, UserIdentity};

let options = Options::default().identify(|call: ToolCall| async move {
    let token = call
        .headers()
        .and_then(|headers| headers.get("authorization"))
        .and_then(|value| value.as_str())
        .map(|value| value.trim_start_matches("Bearer ").to_owned());
    let Some(token) = token else {
        return Ok::<_, HookError>(None);
    };
    let Some(user) = get_user_by_token(&token).await else { // your own lookup
        return Ok(None);
    };
    Ok(Some(UserIdentity::new(user.id).user_name(user.name)))
});
let agentcat = AgentCat::from_env(options);
```

```rust stdio theme={null}
use agentcat_sdk::{AgentCat, HookError, Options, ToolCall, UserIdentity};

let options = Options::default().identify(|_call: ToolCall| async move {
    // A stdio server has no HTTP headers, so read the key that your server already uses.
    let Ok(key) = std::env::var("MY_SERVER_API_KEY") else {
        return Ok::<_, HookError>(None);
    };
    let Some(user) = get_user_by_key(&key).await else { // your own lookup
        return Ok(None);
    };
    Ok(Some(UserIdentity::new(user.id).user_name(user.name)))
});
let server = AgentCat::from_env(options).track(Calculator);
```

### Anonymized identity (the minimum)

If you prefer not to send user details, a stable ID works too, for example a hash of the token or API key. The same user always gets the same ID. The snippets need the `sha2` crate (`cargo add sha2`):

```rust HTTP (both tabs) theme={null}
use agentcat_sdk::{AgentCat, HookError, Options, ToolCall, UserIdentity};
use sha2::{Digest, Sha256};

let options = Options::default().identify(|call: ToolCall| async move {
    let token = call
        .headers()
        .and_then(|headers| headers.get("authorization"))
        .and_then(|value| value.as_str())
        .map(|value| value.trim_start_matches("Bearer ").to_owned());
    let Some(token) = token else {
        return Ok::<_, HookError>(None);
    };
    let hash: String = Sha256::digest(token.as_bytes())
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect();
    Ok(Some(UserIdentity::new(format!("anon-{}", &hash[..16]))))
});
let agentcat = AgentCat::from_env(options);
```

```rust stdio theme={null}
use agentcat_sdk::{AgentCat, HookError, Options, ToolCall, UserIdentity};
use sha2::{Digest, Sha256};

let options = Options::default().identify(|_call: ToolCall| async move {
    let Ok(key) = std::env::var("MY_SERVER_API_KEY") else {
        return Ok::<_, HookError>(None);
    };
    let hash: String = Sha256::digest(key.as_bytes())
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect();
    Ok(Some(UserIdentity::new(format!("anon-{}", &hash[..16]))))
});
let server = AgentCat::from_env(options).track(Calculator);
```

If the hook returns an error or takes more than 30 seconds, the event still goes out, without identity. Without an `identify` hook, every user is anonymous.

## Recommended settings

`Options::default()` turns on every feature, and we recommend that you keep them that way. Each one adds a different view of how agents use your server:

* **Intent** (`enable_tool_call_context`): agents tell you why they call each tool, which powers session goals.
* **Agent feedback** (`enable_report_missing`): agents can report tools that they looked for and did not find, through the `get_more_tools` tool.
* **Sessions** (`enable_tracing`): AgentCat records each tool call and groups the calls of one task into one session, so you can replay it.

If you need to turn one off, the [reference page](https://docs.agentcat.com/languages/rust/reference.md#options) explains each option.

## Testing your setup

The Rust SDK always writes its log to `~/agentcat.log`, so it needs no debug option.

1. Watch the log:

```bash theme={null}
tail -f ~/agentcat.log
```

2. Start your real server with `AGENTCAT_PROJECT_ID` set, as you start it in production.
3. Call one tool with an MCP client, for example the MCP Inspector CLI:

```bash theme={null}
npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http \
  --header "Authorization: Bearer <token>" \
  --method tools/call --tool-name <tool> --tool-arg <name>=<value> \
  --tool-arg session_id=start --tool-arg "context=Testing that AgentCat records this tool call."
```

For a stdio server, you can replace the URL and `--transport http` with the command that starts your server, for example `-e AGENTCAT_PROJECT_ID=<project ID> <path to your binary>`, and leave out `--header`.

4. Look for `Successfully sent event ... | mcp:tools/call | ...` in the log. The line ends with the user ID from `identify`, or `anonymous`. In [Sessions](https://agentcat.com/app/sessions/), the new session shows the user name from `identify`.

A coding agent can confirm the event with the [AgentCat MCP server](https://docs.agentcat.com/agent/connect.md). If no event arrives, the log shows whether a send failed.

## Guidance for AI agents

1. Keep `identify` in place, because without it every user is anonymous; if the developer does not want to send user details, offer the anonymized ID.
2. Return the same ID for the same user on every call, because a random value or a timestamp splits one user into many.
3. Keep raw tokens, API keys, and passwords out of `user_id`, `user_name`, and `user_data`, because AgentCat stores and shows them.
4. Leave `enable_tool_call_context`, `enable_report_missing`, and `enable_tracing` at their defaults, because each one feeds a part of the dashboard.
5. Read the project ID from `AGENTCAT_PROJECT_ID`, so that it stays out of committed code.
6. Call `track()` inside the closure that `rmcp` calls on an HTTP server, because that closure makes a new handler for each request or session.
7. Call `shutdown` after `axum::serve` returns on an HTTP server, because the SDK installs no signal handler and the queued events would be lost.
8. Send your own logs to stderr in a stdio server, because stdout carries JSON-RPC.
9. Run `cargo search agentcat-sdk` before you pin a version, because a version from memory can be old.
10. Test through the real server start, because a separate script can skip the `track()` call.

## Next steps

> [card] [Rust reference](https://docs.agentcat.com/languages/rust/reference.md)
>
> Options, runtimes, shutdown, and limits

> [card] [Custom events](https://docs.agentcat.com/sdk/custom-events.md)
>
> Send your own events

> [card] [Tags and properties](https://docs.agentcat.com/sdk/event-tags-properties.md)
>
> Add data that you can filter on

> [card] [Redaction](https://docs.agentcat.com/privacy-security/client-side-redaction.md)
>
> Remove sensitive data in your server


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