agentcat_sdk/lib.rs
1//! Analytics for MCP servers built on [`rmcp`], the official Rust SDK of the Model Context
2//! Protocol (MCP).
3//!
4//! [AgentCat](https://agentcat.com) shows the maintainers of an MCP server what agents use the
5//! server for and where the agents get stuck. This crate records the tool calls of an `rmcp`
6//! server and sends them to your `AgentCat` project.
7//!
8//! To start, wrap your server handler with [`track`] and serve the [`Tracked`] wrapper in its
9//! place. The wrapper forwards every `ServerHandler` method to your handler.
10//!
11//! # What the SDK records
12//!
13//! The SDK publishes one event for each `tools/call` request. The event holds:
14//!
15//! - The tool name, the request with its arguments, the result, and how long the call took.
16//! - The intent of the call: the text that the agent sends in the `context` parameter (see
17//! [What agents see](#what-agents-see)).
18//! - The session: an ID that ties together the calls of one task.
19//! - The actor: the user that your [`identify`](Options::identify) hook names.
20//! - The error, when the call fails: an error result, a JSON-RPC error, a panic of the tool, or a
21//! call that was cancelled before it completed.
22//! - Token estimates of the arguments and of the result.
23//! - The names and versions of the MCP client and of your server.
24//! - The tags and the properties that your [`event_tags`](Options::event_tags) and
25//! [`event_properties`](Options::event_properties) hooks return.
26//!
27//! The SDK records no other request: tool listings, resources and prompts give no events. Your
28//! code can also publish custom events of its own ([`CustomEvent`]).
29//!
30//! When a tool panics, the SDK records the panic as the error of the call and then lets the panic
31//! go on unchanged. When a call is cancelled before it completes (its future is dropped), the event
32//! gets the error type `cancelled`. The SDK never changes the result or the error of a tool, apart
33//! from the session data that [What agents see](#what-agents-see) describes.
34//!
35//! ## Errors and custom events
36//!
37//! `rmcp` turns an error that a tool returns into the text of an error result, so the event gets
38//! only that text. Call [`capture_error`] before the tool returns the error. The event then also
39//! gets the type of the error and the texts of its `source()` chain.
40//!
41//! A tool that returns `Err(ErrorData)` gives the client a JSON-RPC error, as `rmcp` sends it.
42//! The event gets the name of the JSON-RPC error code as `error.type`. The `error` that the SDK
43//! records for a failed tool call has the platform `rust` and no parsed stack frames.
44//!
45//! The SDK gives each tool call a [`CallContext`], which a tool reads as an
46//! `Extension<CallContext>` parameter. With it, a tool can read its session ID and publish a
47//! custom event with [`CallContext::publish_custom_event`]:
48//!
49//! ```no_run
50//! use agentcat_sdk::{CallContext, CustomEvent, capture_error};
51//! use rmcp::handler::server::common::Extension;
52//! use rmcp::{tool, tool_router};
53//!
54//! #[derive(Clone)]
55//! struct Files;
56//!
57//! #[tool_router]
58//! impl Files {
59//! #[tool(description = "Reads the configuration file")]
60//! async fn read_config(
61//! &self,
62//! Extension(call): Extension<CallContext>,
63//! ) -> Result<String, String> {
64//! // A custom event in the session of this call.
65//! let mut event = CustomEvent::new().resource_name("config-read");
66//! if let Some(session) = call.session_id() {
67//! event = event.session_id(session);
68//! }
69//! let _ = call.publish_custom_event("proj_...", event);
70//!
71//! std::fs::read_to_string("config.toml").map_err(|error| {
72//! // The event gets the type and the cause chain of the error.
73//! capture_error(&error);
74//! error.to_string()
75//! })
76//! }
77//! }
78//! ```
79//!
80//! Outside a tool call, publish a custom event with [`AgentCat::publish_custom_event`].
81//!
82//! # Quickstart: stdio
83//!
84//! Add the crate, `rmcp` with its stdio transport, tokio, and what the `#[tool]` macros need:
85//!
86//! ```toml
87//! [dependencies]
88//! agentcat-sdk = "1"
89//! rmcp = { version = "3", features = ["transport-io"] }
90//! tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
91//! schemars = "1"
92//! serde = { version = "1", features = ["derive"] }
93//! ```
94//!
95//! Then wrap your server with [`track`], and serve the wrapper where you served your server:
96//!
97//! ```no_run
98//! use agentcat_sdk::Options;
99//! use rmcp::handler::server::wrapper::Parameters;
100//! use rmcp::model::{InitializeResult, ServerCapabilities};
101//! use rmcp::{ServerHandler, ServiceExt, tool, tool_handler, tool_router};
102//! use schemars::JsonSchema;
103//! use serde::Deserialize;
104//!
105//! #[derive(Deserialize, JsonSchema)]
106//! struct AddArgs {
107//! a: f64,
108//! b: f64,
109//! }
110//!
111//! #[derive(Clone)]
112//! struct Calculator;
113//!
114//! #[tool_router]
115//! impl Calculator {
116//! #[tool(description = "Adds two numbers")]
117//! async fn add(&self, Parameters(args): Parameters<AddArgs>) -> String {
118//! (args.a + args.b).to_string()
119//! }
120//! }
121//!
122//! #[tool_handler]
123//! impl ServerHandler for Calculator {
124//! fn get_info(&self) -> InitializeResult {
125//! InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
126//! }
127//! }
128//!
129//! #[tokio::main]
130//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
131//! // One line: wrap the server with AgentCat.
132//! let server = agentcat_sdk::track(Calculator, "proj_...", Options::default());
133//! let running = server.serve(rmcp::transport::stdio()).await?;
134//! running.waiting().await?;
135//! Ok(())
136//! }
137//! ```
138//!
139//! Replace `proj_...` with the ID of your project. To read the project ID from the environment
140//! variable [`AGENTCAT_PROJECT_ID`] instead, wrap the server with
141//! `AgentCat::from_env(Options::default()).track(Calculator)` ([`AgentCat::from_env`]).
142//!
143//! A stdio server talks JSON-RPC on stdout. The SDK writes nothing to stdout or stderr. Send your
144//! own logs to stderr (see [Logging](#logging)).
145//!
146//! The server ends when the client closes stdin. Then the last clone of the handle drops, and the
147//! drop delivers the queued events, for up to 5 s (see [Delivery and
148//! shutdown](#delivery-and-shutdown)). A stdio server needs no `shutdown` call.
149//!
150//! # Quickstart: streamable HTTP
151//!
152//! A streamable HTTP server builds a new server handler for each MCP session. Build one
153//! [`AgentCat`] handle for the process, and wrap each new handler with [`AgentCat::track`]. A
154//! clone of the handle is cheap, and `track` does no other work, so the handle can serve every
155//! session.
156//!
157//! This server needs the streamable HTTP server of `rmcp`, more tokio features, and `axum`:
158//!
159//! ```toml
160//! [dependencies]
161//! agentcat-sdk = "1"
162//! rmcp = { version = "3", features = ["transport-streamable-http-server"] }
163//! tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] }
164//! axum = "0.8"
165//! schemars = "1"
166//! serde = { version = "1", features = ["derive"] }
167//! ```
168//!
169//! ```no_run
170//! # use rmcp::handler::server::wrapper::Parameters;
171//! # use rmcp::{ServerHandler, tool, tool_handler, tool_router};
172//! # #[derive(serde::Deserialize, schemars::JsonSchema)]
173//! # struct AddArgs { a: f64, b: f64 }
174//! # #[derive(Clone)]
175//! # struct Calculator;
176//! # #[tool_router]
177//! # impl Calculator {
178//! # #[tool(description = "Adds two numbers")]
179//! # async fn add(&self, Parameters(args): Parameters<AddArgs>) -> String {
180//! # (args.a + args.b).to_string()
181//! # }
182//! # }
183//! # #[tool_handler]
184//! # impl ServerHandler for Calculator {}
185//! use std::sync::Arc;
186//! use std::time::Duration;
187//!
188//! use agentcat_sdk::{AgentCat, Options};
189//! use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
190//! use rmcp::transport::streamable_http_server::{StreamableHttpServerConfig, StreamableHttpService};
191//!
192//! #[tokio::main]
193//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
194//! // One handle for the whole process.
195//! let agentcat = AgentCat::new("proj_...", Options::default());
196//!
197//! // rmcp calls this closure for each new MCP session. Wrap each new server.
198//! let handle = agentcat.clone();
199//! let service = StreamableHttpService::new(
200//! move || Ok(handle.track(Calculator)),
201//! Arc::new(LocalSessionManager::default()),
202//! StreamableHttpServerConfig::default(),
203//! );
204//! let router = axum::Router::new().route_service("/mcp", service);
205//!
206//! let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await?;
207//! tokio::select! {
208//! served = axum::serve(listener, router) => served?,
209//! _ = tokio::signal::ctrl_c() => {}
210//! }
211//!
212//! // Deliver the queued events before the process ends.
213//! agentcat.shutdown(Duration::from_secs(5)).await;
214//! Ok(())
215//! }
216//! ```
217//!
218//! An HTTP server does not end when a client goes away, and the SDK installs no signal handler.
219//! So call [`AgentCat::shutdown`] after your server stops: the queued events then go out before
220//! the process ends.
221//!
222//! This quickstart stops only on Ctrl-C. Containers and process managers stop a server with
223//! SIGTERM, and a server that does not catch it skips `shutdown` and loses the queued events. The
224//! [`streamable_http.rs`](https://github.com/agentcathq/agentcat-rust-sdk/blob/main/agentcat-sdk/examples/streamable_http.rs)
225//! example stops on SIGTERM (Unix) or Ctrl-C: copy its `stop_signal` function in place of
226//! `tokio::signal::ctrl_c()`.
227//!
228//! ## Stateless HTTP
229//!
230//! In stateless mode, `rmcp` builds a new server handler for each request. The setup is the same
231//! as above. Only the configuration of the service changes:
232//!
233//! ```
234//! use rmcp::transport::streamable_http_server::StreamableHttpServerConfig;
235//!
236//! // No MCP sessions, and JSON responses instead of server-sent events.
237//! let config = StreamableHttpServerConfig::default()
238//! .with_legacy_session_mode(false)
239//! .with_json_response(true);
240//! # let _ = config;
241//! ```
242//!
243//! The SDK never uses the MCP session of the transport (the `mcp-session-id` header) to group
244//! calls. The `session_id` parameter groups them on every transport, so a stateless server loses
245//! no sessions. On a stateless server, an event names the client only when the request names it
246//! in its `_meta`.
247//!
248//! # What agents see
249//!
250//! The SDK changes the tool list that `tools/list` returns:
251//!
252//! - Each tool gets a `context` parameter, in which the agent says why it calls the tool. The SDK
253//! records this text as the intent of the call.
254//! - Each tool gets a `session_id` parameter, which ties the calls of one task together.
255//! - With [`enable_agent_tracking`](Options::enable_agent_tracking) on, each tool gets an
256//! `agent_id` parameter, in which the agent names itself.
257//! - The list gets one more tool, `get_more_tools`, through which agents report a tool that they
258//! miss.
259//!
260//! A tool that declares its own parameter with one of these names keeps it, and the SDK does not
261//! add its own. Before your tool runs, the SDK removes the parameters that it added from the
262//! arguments, so your tool never sees them. The table of [Options](#options) names the setter of
263//! each change.
264//!
265//! ## The first call of a task
266//!
267//! The first call of a task sends `session_id` as `start`, or sends no `session_id`. The SDK then
268//! issues a new session ID, which starts with `ses_`. It puts a text block before the content of
269//! the tool's own result. The block begins `[session_id issued` and holds the line
270//! `session_id: ses_...`. The agent sends that ID in every later call of the task, and the SDK
271//! records those calls in the same session.
272//!
273//! A client or a test that reads the first content block of a first call gets this block, not
274//! the content of the tool. An error result gets the block too. A JSON-RPC error
275//! (`Err(ErrorData)`) has no content, so it gets no block.
276//!
277//! A `session_id` that is neither `start` nor a valid `ses_` ID gets a text block that begins
278//! `[session_id unrecognized`. The call then has no session.
279//!
280//! When a tool declares an output schema, the SDK adds an `mcp_session` property to the schema. A
281//! structured result of that tool then gets `mcp_session` as its first key. It holds the session
282//! ID, the agent ID when the agent sent one, and a status.
283//!
284//! To turn this off, set a [`resolve_session_id`](Options::resolve_session_id) hook. The hook
285//! then decides the session of each call, the tools get no `session_id` parameter, and the
286//! results get no text block. The SDK derives the session ID from the value of the hook and your
287//! project ID, so the same value always gives the same session. When the hook returns `None`, the
288//! call gets a new session ID that the agent never learns.
289//!
290//! # Options
291//!
292//! [`Options::default`] has the defaults in the table below. Each setter consumes the options and
293//! returns them changed:
294//!
295//! ```
296//! use agentcat_sdk::{HookError, Options, ToolCall, UserIdentity};
297//!
298//! let options = Options::default()
299//! .enable_agent_tracking(true)
300//! .identify(|call: ToolCall| async move {
301//! // Read your own data about the caller, for example an HTTP header.
302//! let user = call
303//! .headers()
304//! .and_then(|headers| headers.get("x-user-id"))
305//! .and_then(|value| value.as_str())
306//! .map(UserIdentity::new);
307//! Ok::<_, HookError>(user)
308//! });
309//! # let _ = options;
310//! ```
311//!
312//! | Setter | Default |
313//! |---|---|
314//! | [`enable_tool_call_context`](Options::enable_tool_call_context) | `true`: tools get `context` |
315//! | [`custom_context_description`](Options::custom_context_description) | empty: the default description of `context` |
316//! | [`enable_tracing`](Options::enable_tracing) | `true`: the SDK records calls, and tools get `session_id` |
317//! | [`enable_agent_tracking`](Options::enable_agent_tracking) | `false`: tools get no `agent_id` |
318//! | [`enable_report_missing`](Options::enable_report_missing) | `true`: the list gets `get_more_tools` |
319//! | [`resolve_session_id`](Options::resolve_session_id) | none: the SDK issues the session IDs |
320//! | [`identify`](Options::identify) | none: calls have no actor |
321//! | [`event_tags`](Options::event_tags) | none |
322//! | [`event_properties`](Options::event_properties) | none |
323//! | [`redact_event`](Options::redact_event) | none |
324//! | [`redact_sensitive_information`](Options::redact_sensitive_information) | none |
325//! | [`api_base_url`](Options::api_base_url) | empty: [`AGENTCAT_API_URL`], then [`MCPCAT_API_URL`], then `https://api.agentcat.com` |
326//! | [`log_destination`](Options::log_destination) | [`LogDestination::File`]: `~/agentcat.log` |
327//!
328//! This release has no exporters (OpenTelemetry, Datadog, Sentry, `PostHog`) and no SDK
329//! diagnostics.
330//!
331//! The hooks are async closures. The [`identify`](Options::identify),
332//! [`event_tags`](Options::event_tags), [`event_properties`](Options::event_properties) and
333//! [`resolve_session_id`](Options::resolve_session_id) hooks get a [`ToolCall`]. It holds the tool
334//! name, the arguments, the `_meta` of the request, the HTTP headers, the client, and the `rmcp`
335//! request context. The SDK starts these hooks on the tokio runtime of the call. The call never
336//! waits for them, so a slow hook never delays a tool call. When the event is ready to go out, the
337//! SDK waits at most 30 s for each hook. It stops a hook that has not completed by then, and the
338//! event goes out without the value of that hook. A hook that returns an error gives no value, and
339//! the SDK logs the error.
340//!
341//! The redaction hooks run before an event leaves the process:
342//! [`redact_event`](Options::redact_event) on the whole [`Event`], then
343//! [`redact_sensitive_information`](Options::redact_sensitive_information) on each string of the
344//! event. When a redaction hook fails, the SDK drops the event and does not send it unredacted.
345//!
346//! # Environment variables
347//!
348//! | Variable | Effect |
349//! |---|---|
350//! | [`AGENTCAT_PROJECT_ID`] | The project ID that [`AgentCat::from_env`] reads. When it is unset or empty, the handle is telemetry only. |
351//! | [`AGENTCAT_API_URL`] | The base URL of the `AgentCat` API, when the options set none. |
352//! | [`MCPCAT_API_URL`] | The former name of `AGENTCAT_API_URL`, read when that one is unset or empty. |
353//! | [`AGENTCAT_ENABLED`] | `false`, `0`, `no` or `off`, in any case, turns the SDK off. Any other value, or none, leaves it on. |
354//!
355//! A telemetry-only handle (an empty project ID) builds each event and runs the hooks, but sends
356//! nothing to `AgentCat`.
357//!
358//! When `AGENTCAT_ENABLED` turns the SDK off, a handle starts no thread, and its [`Tracked`]
359//! servers forward every request to your handler untouched. A tool still gets a
360//! [`CallContext`], with no session. Custom events publish nothing, and `flush` and `shutdown`
361//! return `true`. The SDK has no test detection: it stays on in tests unless you set this
362//! variable.
363//!
364//! # Features
365//!
366//! | Feature | Default | Effect |
367//! |---|---|---|
368//! | `rustls` | on | TLS with rustls for the connection to the `AgentCat` API. |
369//! | `native-tls` | off | TLS with the TLS library of the platform. |
370//! | `test-util` | off | The [`testing`] module with [`testing::Fake`]. |
371//!
372//! Turn on one TLS feature. To use `native-tls`, turn off the default features:
373//!
374//! ```toml
375//! [dependencies]
376//! agentcat-sdk = { version = "1", default-features = false, features = ["native-tls"] }
377//! ```
378//!
379//! Without a TLS feature, the SDK can send events only to an `http:` URL. It logs a warning when
380//! the URL is an `https:` URL.
381//!
382//! # Delivery and shutdown
383//!
384//! Each [`AgentCat`] handle delivers its events from its own thread, `agentcat-delivery`, with its
385//! own queue and its own API URL. The first event or log line of the handle starts the thread. A
386//! tool call only puts its event in the queue of the handle, so the call never waits for the
387//! network. The delivery has these limits:
388//!
389//! - The queue holds at most 10 000 events. When it is full, the SDK drops the oldest event.
390//! - At most 5 events of a handle are in progress at a time.
391//! - The SDK tries a failed send 3 more times, after 1, 2 and 4 s. Each attempt has a timeout of
392//! 10 s.
393//!
394//! Your code can wait for the delivery:
395//!
396//! - [`AgentCat::flush`] waits until the handle delivered every event it took, for at most a
397//! timeout. It returns `true` when that happened in time.
398//! - [`AgentCat::shutdown`] does the same, then stops the handle for good. The handle drops every
399//! later event.
400//! - [`AgentCat::flush_blocking`] and [`AgentCat::shutdown_blocking`] block the calling thread
401//! instead. They need no tokio runtime.
402//!
403//! The SDK installs no signal or exit handler. These calls and the drain of the last drop (below)
404//! deliver the queued events.
405//!
406//! ```no_run
407//! # async fn run(agentcat: agentcat_sdk::AgentCat) {
408//! use std::time::Duration;
409//!
410//! // ... serve until the server stops ...
411//! let delivered = agentcat.shutdown(Duration::from_secs(5)).await;
412//! if !delivered {
413//! // Some events did not go out within 5 s.
414//! }
415//! # }
416//! ```
417//!
418//! When the last clone of a handle drops, the SDK runs `shutdown` with a timeout of 5 s. The drop
419//! blocks the dropping thread until the shutdown ends. The [`Tracked`] servers hold clones of
420//! the handle too. This drain does not run after `std::process::exit`, or after a panic with
421//! `panic = "abort"`.
422//!
423//! On a current-thread runtime, a drop on the thread of the runtime blocks its only thread. Then
424//! no hook can run during the wait, and the SDK drops an event that still needs a hook after 5 s.
425//! On such a runtime, call `shutdown` before the runtime ends. See [dropping the last
426//! handle](AgentCat#dropping-the-last-handle).
427//!
428//! # Logging
429//!
430//! The SDK never writes to stdout or stderr. It writes its own log lines to the file `agentcat.log`
431//! in your home directory (`~/agentcat.log`). Each line holds the time, the versions of the SDK, of
432//! Rust and of `rmcp`, and the message:
433//!
434//! ```text
435//! [2026-10-28T09:15:02.137Z] [sdk=1.0.0 rust=1.88.0 rmcp=3.6.0] Successfully sent event ...
436//! ```
437//!
438//! When the SDK finds no home directory, it writes no log. It ignores the errors of the file.
439//!
440//! To send the lines to `tracing` instead, set [`LogDestination::Tracing`]:
441//!
442//! ```
443//! use agentcat_sdk::{LogDestination, Options};
444//!
445//! let options = Options::default().log_destination(LogDestination::Tracing);
446//! # let _ = options;
447//! ```
448//!
449//! The `tracing` events have the target `agentcat`. A line about a failure or an error is an
450//! `ERROR` event. A warning is a `WARN` event, and any other line is an `INFO` event. The delivery
451//! thread of the handle emits the events, so set a global subscriber, for example with
452//! `tracing::subscriber::set_global_default`. A subscriber that `with_default` sets on another
453//! thread does not see them.
454//!
455//! The log destination applies to the whole process. The first handle built in the process
456//! decides it.
457//!
458//! ## Stdio servers
459//!
460//! A stdio server talks JSON-RPC on stdout, so any other text on stdout breaks the connection to
461//! the client. Send your own logs and your `tracing` output to stderr. The `fmt` subscriber of
462//! `tracing-subscriber` writes to stdout unless you give it another writer, for example
463//! `.with_writer(std::io::stderr)`. This also holds when you send the lines of the SDK to
464//! `tracing`.
465//!
466//! # Testing
467//!
468//! ## A redaction hook
469//!
470//! To unit-test a [`redact_event`](Options::redact_event) hook, call it with an event from
471//! [`Event::default`]. Set the fields that the hook reads:
472//!
473//! ```
474//! use agentcat_sdk::{Event, HookError};
475//!
476//! # #[tokio::main(flavor = "current_thread")]
477//! # async fn main() -> Result<(), HookError> {
478//! // The closure that you pass to `Options::redact_event`.
479//! let hook = |mut event: Event| async move {
480//! event.response = None;
481//! Ok::<_, HookError>(Some(event))
482//! };
483//!
484//! let mut event = Event::default();
485//! event.response = Some(serde_json::json!({"token": "fake-secret-42"}));
486//! let redacted = hook(event).await?;
487//! assert_eq!(redacted.and_then(|event| event.response), None);
488//! # Ok(())
489//! # }
490//! ```
491//!
492//! ## A tracked server
493//!
494//! [`testing::Fake`] builds handles whose events stay in the process. It records each event as a
495//! real handle sends it, after the hooks and the redaction.
496//!
497//! The test that follows serves the tracked server to an `rmcp` client in memory, calls a tool,
498//! and makes sure that the fake recorded the call. It needs these dev-dependencies:
499//!
500//! - The `test-util` feature of the SDK, for `Fake`.
501//! - The `client` feature of `rmcp`, for the client.
502//! - The `io-util` feature of tokio, for `tokio::io::duplex`.
503//! - The `macros` and `rt` features of tokio, for `#[tokio::test]`.
504//!
505//! ```toml
506//! [dev-dependencies]
507//! agentcat-sdk = { version = "1", features = ["test-util"] }
508//! rmcp = { version = "3", features = ["client"] }
509//! tokio = { version = "1", features = ["io-util", "macros", "rt"] }
510//! ```
511//!
512//! In a `#[tokio::test]`:
513//!
514//! ```
515//! # use rmcp::handler::server::wrapper::Parameters;
516//! # use rmcp::{ServerHandler, tool, tool_handler, tool_router};
517//! # #[derive(serde::Deserialize, schemars::JsonSchema)]
518//! # struct AddArgs { a: f64, b: f64 }
519//! # #[derive(Clone)]
520//! # struct Calculator;
521//! # #[tool_router]
522//! # impl Calculator {
523//! # #[tool(description = "Adds two numbers")]
524//! # async fn add(&self, Parameters(args): Parameters<AddArgs>) -> String {
525//! # (args.a + args.b).to_string()
526//! # }
527//! # }
528//! # #[tool_handler]
529//! # impl ServerHandler for Calculator {}
530//! # #[cfg(feature = "test-util")]
531//! # #[tokio::main(flavor = "current_thread")]
532//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
533//! use agentcat_sdk::Options;
534//! use agentcat_sdk::testing::Fake;
535//! use rmcp::ServiceExt;
536//! use rmcp::model::{
537//! CallToolRequestParams, ClientCapabilities, Implementation, InitializeRequestParams,
538//! };
539//!
540//! let fake = Fake::new();
541//! let server = fake.agentcat("proj_test", Options::default()).track(Calculator);
542//!
543//! // The server on one end of an in-memory stream, an rmcp client on the other.
544//! let (server_end, client_end) = tokio::io::duplex(64 * 1024);
545//! tokio::spawn(async move {
546//! if let Ok(running) = server.serve(server_end).await {
547//! let _ = running.waiting().await;
548//! }
549//! });
550//! let client = InitializeRequestParams::new(
551//! ClientCapabilities::default(),
552//! Implementation::new("test", "1.0.0"),
553//! )
554//! .serve(client_end)
555//! .await?;
556//!
557//! let mut arguments = rmcp::model::JsonObject::new();
558//! arguments.insert("a".into(), 1.into());
559//! arguments.insert("b".into(), 2.into());
560//! client
561//! .call_tool(CallToolRequestParams::new("add").with_arguments(arguments))
562//! .await?;
563//!
564//! fake.assert_tracked("add").await;
565//! # Ok(())
566//! # }
567//! # #[cfg(not(feature = "test-util"))]
568//! # fn main() {}
569//! ```
570//!
571//! [`Fake::events`] returns the recorded events, for checks of your own.
572//!
573//! # Minimum supported Rust version
574//!
575//! The minimum supported Rust version (MSRV) is 1.88, the MSRV of `rmcp` 3. The MSRV is never
576//! higher than the MSRV of the `rmcp` versions that this crate supports. The MSRV is always a Rust
577//! release that is at least 6 months old. An MSRV increase is a minor release and has its own
578//! CHANGELOG section.
579//!
580//! The crate supports `rmcp` 3, from 3.0.0. CI tests the latest patch of each 3.x minor release.
581//!
582//! # Known limits
583//!
584//! - A tracked server needs a tokio runtime with the time driver: the SDK times a rebuild of its
585//! tool registry with `tokio::time::timeout`, and tokio panics at such a timer on a runtime
586//! without the driver. `rmcp` uses timers too. `#[tokio::main]`, `#[tokio::test]` and
587//! `tokio::runtime::Runtime::new` turn the time driver on. A runtime from
588//! `tokio::runtime::Builder` needs `enable_time` or `enable_all`.
589//! - The SDK does not support the `local` feature of `rmcp`. It spawns the hooks as `Send` tasks
590//! on the tokio runtime of the call.
591//! - The SDK turns on the `preserve_order` feature of `serde_json`, so that `mcp_session` stays
592//! the first key of a result. Cargo turns a feature on for the whole build, so every
593//! `serde_json::Map` in your program keeps the order of insertion and does not sort its keys.
594//! - When another crate in your build turns on the `arbitrary_precision` or the `float_roundtrip`
595//! feature of `serde_json`, Cargo turns it on for the SDK too. These features change how
596//! `serde_json` reads and writes numbers. Without them, `serde_json` can read some decimals (for
597//! example `7.038531e-26`, and some of 16 or 17 significant digits) as a neighboring `f64`, and
598//! the SDK counts each number as the shortest text that reads back as the same `f64`. With either
599//! feature, the text of some numbers in the events, the token estimates and the size checks can
600//! change. The SDK logs a warning that names the feature each time its delivery thread starts.
601//! - `parameters.extra.requestInfo.url` of an event is the URL that the `rmcp` service sees.
602//! `axum::Router::nest_service` removes the path prefix of the route before that, so the event
603//! records the URL without the prefix. `route_service` keeps the full path.
604//! - The SDK keeps a registry of what the tool listings tell it, for each handle and each type of
605//! server handler, by tool name. It never removes an entry, so a server whose tool names differ
606//! per tenant grows the registry with each new name. A hand-written `ServerHandler` without
607//! `get_tool` depends on that registry (`#[tool_handler]` writes `get_tool` for you). On such a
608//! server, the listing of one session decides which arguments the SDK removes from the calls of
609//! another session.
610//! - A hook that holds a clone of its [`AgentCat`] handle makes a reference cycle. The handle then
611//! never drops, so the drain of the last drop never runs. Let the hook capture only what it
612//! needs, or call [`AgentCat::shutdown`] before the process ends. A tool reaches its handle
613//! through [`CallContext::agentcat`] and needs no clone.
614//! - The SDK gives the redaction hooks no time limit. A redaction hook that never completes holds
615//! one of the 5 delivery slots of its handle for good. Five such hooks stop the delivery of the
616//! handle.
617//!
618//! # Examples
619//!
620//! The repository has four examples. Each one serves the tools `add` and `echo`:
621//!
622//! - [`stdio.rs`](https://github.com/agentcathq/agentcat-rust-sdk/blob/main/agentcat-sdk/examples/stdio.rs):
623//! a stdio server.
624//! - [`streamable_http.rs`](https://github.com/agentcathq/agentcat-rust-sdk/blob/main/agentcat-sdk/examples/streamable_http.rs):
625//! a streamable HTTP server with MCP sessions and server-sent events.
626//! - [`stateless_http.rs`](https://github.com/agentcathq/agentcat-rust-sdk/blob/main/agentcat-sdk/examples/stateless_http.rs):
627//! a stateless streamable HTTP server with JSON responses.
628//! - [`hooks.rs`](https://github.com/agentcathq/agentcat-rust-sdk/blob/main/agentcat-sdk/examples/hooks.rs):
629//! a stdio server that uses every hook except `resolve_session_id`, a custom event and
630//! [`capture_error`].
631//!
632// The `testing` module exists only with the `test-util` feature. Without the feature, the links
633// of the guide to the module go to docs.rs, so that a doc build with the default features has no
634// broken links.
635#![cfg_attr(feature = "test-util", doc = "[`testing`]: testing")]
636#![cfg_attr(feature = "test-util", doc = "[`testing::Fake`]: testing::Fake")]
637#![cfg_attr(feature = "test-util", doc = "[`Fake::events`]: testing::Fake::events")]
638#![cfg_attr(
639 not(feature = "test-util"),
640 doc = "[`testing`]: https://docs.rs/agentcat-sdk/latest/agentcat_sdk/testing/index.html"
641)]
642#![cfg_attr(
643 not(feature = "test-util"),
644 doc = "[`testing::Fake`]: https://docs.rs/agentcat-sdk/latest/agentcat_sdk/testing/struct.Fake.html"
645)]
646#![cfg_attr(
647 not(feature = "test-util"),
648 doc = "[`Fake::events`]: https://docs.rs/agentcat-sdk/latest/agentcat_sdk/testing/struct.Fake.html#method.events"
649)]
650#![forbid(unsafe_code)]
651#![cfg_attr(docsrs, feature(doc_cfg))]
652
653mod call;
654mod capture;
655mod client;
656mod config;
657mod constants;
658mod context;
659mod custom;
660mod delivery;
661mod error;
662mod event;
663mod handles;
664mod hooks;
665mod inject;
666mod logging;
667mod pipeline;
668mod registry;
669mod sink;
670#[cfg(test)]
671mod test_support;
672#[cfg(feature = "test-util")]
673#[cfg_attr(docsrs, doc(cfg(feature = "test-util")))]
674pub mod testing;
675mod tokens;
676mod tracked;
677mod wire;
678
679pub use capture::capture_error;
680pub use client::{AgentCat, track};
681pub use config::env::{AGENTCAT_API_URL, AGENTCAT_ENABLED, AGENTCAT_PROJECT_ID, MCPCAT_API_URL};
682pub use config::{HookError, LogDestination, Options, UserIdentity};
683pub use context::CallContext;
684pub use custom::CustomEvent;
685pub use error::Error;
686pub use event::Event;
687pub use hooks::ToolCall;
688pub use tracked::Tracked;
689
690/// The code blocks of `README.md`, compiled as doctests. In the repository the file is a symlink
691/// to the root README, which GitHub shows; in the package it is a copy, which crates.io shows.
692/// Only `cargo test --doc` builds it.
693#[cfg(doctest)]
694#[doc = include_str!("../README.md")]
695pub struct ReadmeDoctests;