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

ItemPlanned value
Cratesvc-sdk on crates.io (library name svc_sdk)
Runtimetokio (other executors are not supported in the first release)
TLSrustls by default; native-tls as an alternative feature
Cargo featuresderive (default), rustls (default), native-tls, otel, telemetry, testing
Core dependenciestokio, 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

bash
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

rust
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
}
bash
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.

rust
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

rust
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
OptionDefaultMeaning
api_keynone, requiredThe agent key. build() returns Error::Config("api_key is required.") for an empty value
urlSPACEFIX_SERVICE_URL, then https://api.spacefix.ioThe gateway base URL. Set it per environment
otelon with the otel featureOtel::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 a scratch or 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_blocking for CPU-bound or blocking work. Handler futures must be Send + '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 CancellationToken stops the stream and waits for running handlers; run_until_signal() does this on Ctrl-C and SIGTERM. On Kubernetes, set terminationGracePeriodSeconds above shutdown_grace.
  • Health. agent.state() returns State::Connecting, State::Connected, State::Retrying or State::Stopped:
rust
// 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:

rust
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()?;
OptionDefaultMeaning
interval10 secondsHow often to sample and report
reportbuilt inA function that receives each sample; replaces the gateway transport
oteltrueAlso 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