SDKs
C# SDK
The planned .NET SDK (Spacefix.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 .NET agent will feel. Nothing below can be installed yet.
What it will be
| Item | Planned value |
|---|---|
| NuGet package | Spacefix.Sdk (namespace Spacefix) |
| Companion packages | Spacefix.Sdk.Hosting (Generic Host and dependency injection), Spacefix.Sdk.Generators (source generator), Spacefix.Sdk.Testing (fake gateway) |
| Target frameworks | net8.0 and newer |
| Trimming and AOT | Trim-safe and Native AOT compatible when tools are registered through the source generator |
| Dependencies | HttpClient and System.Text.Json from the runtime, plus Microsoft.Extensions.Logging.Abstractions |
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
dotnet add package Spacefix.Sdk
dotnet add package Spacefix.Sdk.Hosting # optional: Generic Host integration
A minimal agent
using Spacefix;
var agent = new Agent(new AgentOptions
{
ApiKey = Environment.GetEnvironmentVariable("SPACEFIX_API_KEY")!, // URL comes from SPACEFIX_SERVICE_URL
});
agent.Tool("get_order",
description: "Look up a single order by its id. Returns null if it does not exist.",
handler: async (GetOrderArgs args, CancellationToken ct) => await orders.FindByIdAsync(args.Id, ct));
using var stop = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; stop.Cancel(); };
AppDomain.CurrentDomain.ProcessExit += (_, _) => stop.Cancel();
await agent.ConnectAsync(stop.Token); // returns when the token is cancelled or DisconnectAsync is called
public sealed record GetOrderArgs(string Id);
export SPACEFIX_API_KEY=agent_key_from_the_dashboard
export SPACEFIX_SERVICE_URL=https://api.spacefix.io # optional; defaults to the production URL
dotnet run
# connected with 1 tool(s)
In a real service you will use the Generic Host (see Running in production), where this becomes a few lines.
Defining a tool
The idiom is attributes on methods and records for arguments, with System.Text.Json source generators so schema generation needs no reflection at run time.
using System.ComponentModel;
using Spacefix;
public sealed class OrderTools(IOrderService orders)
{
[McpTool("get_order", Description = "Look up a single order by id. Returns null if it does not exist.")]
public Task<Order?> GetOrder(
[Description("Order id, for example o_123")] string id,
CancellationToken ct)
=> orders.FindByIdAsync(id, ct);
[McpTool("search_orders",
Description = "Search a customer's orders by status. Returns at most `limit` orders, newest first.")]
public Task<IReadOnlyList<Order>> Search(SearchArgs args, CancellationToken ct)
=> orders.SearchAsync(args.CustomerId, args.Status, args.Limit, ct);
}
public sealed record SearchArgs(
[property: Description("Customer id, for example cus_123")] string CustomerId,
[property: Description("Order status")] OrderStatus Status = OrderStatus.Open,
[property: Range(1, 100), Description("Maximum orders to return")] int Limit = 20);
public enum OrderStatus { Open, Shipped, Cancelled }
A tool method takes either one record or class parameter (the argument object) or several simple parameters, plus optional CancellationToken and CallContext parameters that are not part of the schema. It returns T, Task<T>, ValueTask<T> or void/Task. Names default to the attribute argument, then to the method name in snake_case, and must match ^[a-zA-Z0-9_-]{1,64}$. Resources and prompts use [McpResource] and [McpPrompt]; prompt arguments are always strings.
Configuration
var options = new AgentOptions
{
ApiKey = "...", // required
Url = "https://api.spacefix.io", // optional
Otel = new OtelOptions { ArgKeys = true } // optional; Otel = null disables; default is on
};
| Property | Default | Meaning |
|---|---|---|
ApiKey | none, required | The agent key. A null or empty value throws ArgumentException("ApiKey is required.") |
Url | SPACEFIX_SERVICE_URL, then https://api.spacefix.io | The gateway base URL. Set it per environment |
Otel | on | null turns the bridge off. ArgKeys = true also records argument names, never values |
In the Generic Host these bind from configuration (Svc:ApiKey, Svc:Url, Svc:Otel:ArgKeys), and the environment variables SPACEFIX_API_KEY and SPACEFIX_SERVICE_URL are honoured as fallbacks. The design also proposes HttpClient, MaxConcurrentCalls, MaxResultBytes, Logger, ShutdownGrace and StateChanged.
Running in production
Run the agent as an always-on process. Request-scoped hosting (for example Azure Functions consumption plans) freezes the process between requests and breaks the connection; it is not supported.
Spacefix.Sdk.Hosting registers the agent as a BackgroundService, so it starts and stops with the host, in a web app or a worker service:
var builder = Host.CreateApplicationBuilder(args); // or WebApplication.CreateBuilder(args)
builder.Services.AddSvcAgent(builder.Configuration.GetSection("Svc")) // Svc:ApiKey, Svc:Url
.AddTools<OrderTools>()
.AddResources<OrderResources>()
.AddPrompts<OrderPrompts>()
.AddTelemetry();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddHealthChecks().AddSvcAgentHealthCheck();
await builder.Build().RunAsync();
- Scoped services. With the Hosting package, each call runs in its own dependency injection scope, so scoped services (such as a
DbContext) are created per call. - Scaling. Run more instances. Each opens its own connection and the gateway spreads calls across them, so keep state in a database. For isolation from your web tier, run the same registration in a separate worker service.
- Shutdown.
DisconnectAsyncis idempotent and waits for running handlers. In the Generic Host, SIGTERM and Ctrl-C triggerStopAsync; setHostOptions.ShutdownTimeoutabove the agent'sShutdownGrace. - Health.
agent.StateisConnecting,Connected,RetryingorStopped, andAddSvcAgentHealthCheck()is unhealthy when the agent is not connected. Use it for readiness, not liveness: a gateway outage should not restart your pods. - Secrets. Keep
ApiKeyout ofappsettings.json; use user secrets, environment variables (Svc__ApiKey) or a secret store.
Telemetry
An optional package reports process health to the Agents page, the counterpart of @spacefix/telemetry:
agent.Use(new Telemetry()); // every 10 seconds over the agent's connection
agent.Use(new Telemetry(new TelemetryOptions {
Interval = TimeSpan.FromSeconds(30),
Report = m => { statsd.Gauge("heap", m.HeapUsed); return ValueTask.CompletedTask; } }));
| Option | Default | Meaning |
|---|---|---|
Interval | 10 seconds | How often to sample and report |
Report | built in | A Func<Metrics, ValueTask>; replaces the gateway transport |
Otel | true | Also publish through OpenTelemetry |
OpenTelemetry
.NET's OpenTelemetry API is built into the runtime, so the bridge needs no extra package: it uses an ActivitySource, a Meter and ILogger. These are no-ops until your application registers listeners. Unlike other languages, your application must subscribe by name:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("Spacefix.Sdk"))
.WithMetrics(m => m.AddMeter("Spacefix.Sdk", "Spacefix.Telemetry"))
.WithLogging(); // ILogger records flow through the logging provider
// or, with the Hosting package: builder.Services.AddOpenTelemetry().AddSvcSdkInstrumentation();
Each call that reaches your agent produces a server activity (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 .NET today without an SDK
- SDK overview: every language and its status