SDKs
Rust SDK
The planned Rust SDK (svc-sdk): what it will look like, and what to use until it ships.
This page previews the planned API so you can see how a Rust agent will feel. Nothing below can be installed yet.
What it will be
| Item | Planned value |
|---|---|
| Crate | svc-sdk on crates.io (library name svc_sdk) |
| Runtime | tokio (other executors are not supported in the first release) |
| TLS | rustls by default; native-tls as an alternative feature |
| Cargo features | derive (default), rustls (default), native-tls, otel, telemetry, testing |
| Core dependencies | tokio, reqwest, serde, serde_json, schemars, futures-util, tokio-util, tracing |
Like every SDK, it connects out to the gateway (no inbound port), registers your tools, resources and prompts, runs your handlers when calls arrive, reconnects with backoff, and can report telemetry. See the SDK overview.
Install
cargo add svc-sdk
cargo add tokio --features macros,rt-multi-thread,signal
cargo add serde --features derive
cargo add schemars
cargo add svc-sdk --features otel,telemetry # optional
A minimal agent
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use svc_sdk::{tool, Agent, ToolError};
/// Arguments for `get_order`.
#[derive(Deserialize, JsonSchema)]
struct GetOrderArgs {
/// Order id, for example o_123
id: String,
}
#[derive(Serialize)]
struct Order {
id: String,
status: String,
}
/// Look up a single order by its id. Returns null if it does not exist.
#[tool(name = "get_order")]
async fn get_order(args: GetOrderArgs) -> Result<Option<Order>, ToolError> {
Ok(orders::find_by_id(&args.id).await?)
}
#[tokio::main]
async fn main() -> Result<(), svc_sdk::Error> {
let agent = Agent::builder()
.api_key(std::env::var("SPACEFIX_API_KEY").expect("SPACEFIX_API_KEY")) // URL from SPACEFIX_SERVICE_URL
.tool(get_order)
.build()?;
// Connects, reconnects, and returns after Ctrl-C or SIGTERM.
agent.run_until_signal().await
}
export SPACEFIX_API_KEY=agent_key_from_the_dashboard
export SPACEFIX_SERVICE_URL=https://api.spacefix.io # optional; defaults to the production URL
cargo run
# connected with 1 tool(s) (via tracing at INFO)
run_until_signal() is a convenience over connect(CancellationToken); use that when you own signal handling.
Defining a tool
The idiom is derive macros and attribute macros over plain structs and async fns, with schemars producing the JSON Schema and serde the (de)serialisation.
use schemars::JsonSchema;
use serde::Deserialize;
use svc_sdk::{tool, ToolError};
#[derive(Deserialize, JsonSchema)]
struct SearchArgs {
/// Customer id, for example cus_123
customer_id: String,
/// Order status
#[serde(default)]
status: Status,
/// Maximum orders to return
#[schemars(range(min = 1, max = 100))]
limit: Option<u32>,
}
#[derive(Deserialize, JsonSchema, Default)]
#[serde(rename_all = "lowercase")]
enum Status { #[default] Open, Shipped, Cancelled }
/// Search a customer's orders by status. Returns at most `limit` orders, newest first.
#[tool]
async fn search_orders(args: SearchArgs) -> Result<Vec<Order>, ToolError> {
Ok(orders::search(&args.customer_id, args.status, args.limit.unwrap_or(20)).await?)
}
The tool name is name = "..." or the function name and must match ^[a-zA-Z0-9_-]{1,64}$; a violation is a compile error. The description comes from description = "..." or the function's doc comment. The handler takes one argument type (or none), and returns Result<T, E> or a plain T. Resources and prompts use #[resource(uri = "...")] and #[prompt]. Without macros, ToolDefinition::new(...) builds a tool, and ToolDefinition::raw(...) takes raw JSON Schema.
Configuration
let agent = Agent::builder()
.api_key("...") // required
.url("https://api.spacefix.io") // optional
.otel(Otel::default().arg_keys(true)) // optional; Otel::off() disables
.build()?; // Err if the key is empty
| Option | Default | Meaning |
|---|---|---|
api_key | none, required | The agent key. build() returns Error::Config("api_key is required.") for an empty value |
url | SPACEFIX_SERVICE_URL, then https://api.spacefix.io | The gateway base URL. Set it per environment |
otel | on with the otel feature | Otel::off() disables the bridge. arg_keys(true) also records argument names, never values |
The design also proposes http_client, max_concurrent_calls, max_result_bytes, shutdown_grace and on_state_change. Logging goes through the tracing crate, so your application chooses the subscriber.
Running in production
Run the agent as an always-on process. Request-scoped serverless runtimes freeze the process between requests and break the connection; they are not supported.
- Standalone binary. Build a static binary (
x86_64-unknown-linux-musl) and ship it in ascratchor distroless container as its own deployment, scaling by replica. - Concurrency. Each call runs in its own tokio task. A handler that blocks the runtime thread starves other calls; use
tokio::task::spawn_blockingfor CPU-bound or blocking work. Handler futures must beSend + 'static. - Scaling. Run more processes. Each opens its own connection and the gateway spreads calls across them, so keep state in a database.
- Shutdown. Cancelling the
CancellationTokenstops the stream and waits for running handlers;run_until_signal()does this on Ctrl-C and SIGTERM. On Kubernetes, setterminationGracePeriodSecondsaboveshutdown_grace. - Health.
agent.state()returnsState::Connecting,State::Connected,State::RetryingorState::Stopped:
// axum readiness probe
async fn readyz(State(agent): State<AgentHandle>) -> StatusCode {
if agent.state() == State::Connected { StatusCode::OK } else { StatusCode::SERVICE_UNAVAILABLE }
}
Telemetry
An optional module, behind the telemetry feature, reports process health to the Agents page, the counterpart of @spacefix/telemetry:
use std::time::Duration;
use svc_sdk::telemetry::Telemetry;
let agent = Agent::builder()
.api_key(key)
.telemetry(Telemetry::default()) // every 10 seconds, over the agent's connection
// .telemetry(Telemetry::builder().interval(Duration::from_secs(30)).report(|m| statsd.gauge("heap", m.heap_used)).build())
.build()?;
| Option | Default | Meaning |
|---|---|---|
interval | 10 seconds | How often to sample and report |
report | built in | A function that receives each sample; replaces the gateway transport |
otel | true | Also publish through OpenTelemetry (feature otel) |
OpenTelemetry
Behind the otel feature, the bridge uses only the OpenTelemetry API crate. It never installs a provider or exporter: with your application's providers installed (opentelemetry_sdk, an OTLP exporter, or tracing-opentelemetry), spans, metrics and logs flow to your backend. Without the feature the bridge does not exist. Each call that reaches your agent produces a server span (tools/call <name>, resources/read <uri> or prompts/get <name>) that continues the caller's trace, plus the duration histogram mcp.server.operation.duration, the counter spacefix.agent.reconnects, the gauge spacefix.agent.connected and connection logs. Argument values and results are never recorded. The attributes and metric names are the same in every SDK; see OpenTelemetry.
Next steps
- Node.js SDK and Go SDK: available now
- The agent protocol: connect from Rust today without an SDK
- SDK overview: every language and its status