Skip to main content
This page describes the agentcat-sdk crate, version 1.x, for rmcp 3.x. For the first setup, see Rust setup. The crate guide 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).
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: 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 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).
  • 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:
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:
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.