API reference
Full reference for SpanlyClient, MonitorOptions, and the per-request hooks.
The TypeScript SDK exposes a single class – SpanlyClient – and a small
set of supporting types. Everything you need to instrument an MCP server
flows through client.monitor().
SpanlyClient
import { SpanlyClient } from '@spanly/sdk';
const client = new SpanlyClient({
apiKey: process.env.SPANLY_API_KEY,
});new SpanlyClient(options)
| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | yes | Your Spanly API key. Region (us / eu) is auto-detected from the prefix. |
region | SpanlyRegion | no | Override the auto-detected region. You almost never need this. |
endpoint | string | no | Override the ingest endpoint. Used internally for E2E tests. |
client.monitor(server, options?)
Attach the client to an MCP server. Pass the Server (or McpServer)
instance from @modelcontextprotocol/sdk. Returns the server unchanged
so you can chain.
const spanly = new SpanlyClient({ apiKey: process.env.SPANLY_API_KEY });
spanly.monitor(mcpServer);Call monitor() before server.connect(transport). If you call it
later, packets exchanged during the initialize handshake will be
missed.
client.flush()
Drain any buffered packets to ingest and resolve when the queue is empty. Useful in tests and short-lived scripts.
await spanly.flush();In long-running servers you do not need to call this – the SDK flushes on a fixed interval and on process exit.
MonitorOptions
Pass these as the second argument to monitor().
spanly.monitor(mcpServer, {
onCollect: (direction, context, packet) => { /* … */ },
onError: (err) => { /* … */ },
onWarning: (warning) => { /* … */ },
});onCollect(direction, context, packet)
Called on every captured packet before it is queued for ingest. Use this to attach custom context, redact fields, or drop traffic.
type CollectHook = (
direction: 'request' | 'response' | 'notification',
context: SpanlyPacketContext,
packet: McpPacket,
) => McpPacket | null;Returning null drops the packet entirely (it is not sent to Spanly).
Returning the packet (mutated or not) queues it.
The context object is mutable. Common writes:
context.environmentId = currentTenantId(); // multi-tenant tagging
context.userId = req.user.id; // join with your auth system
context.spanId = currentTrace().spanId; // correlate with OpenTelemetryonError(err)
Called when the SDK itself fails (failed flush, network error, malformed packet). The SDK never throws into your code path – it always logs through this hook instead.
spanly.monitor(mcpServer, {
onError: (err) => log.error({ err }, 'spanly capture failed'),
});onWarning(warning)
Called when the SDK observes something suspicious but recoverable – for example a packet that arrived without a matching request, or a malformed JSON-RPC frame. Useful during development; safe to ignore in production.
type CollectWarning = {
code: 'unmatched-response' | 'invalid-json' | 'queue-overflow';
message: string;
};Types
The package re-exports the types you'd want for typed callbacks:
import type {
SpanlyPacket,
SpanlyPacketContext,
SpanlyPacketTransportContext,
McpPacket,
CollectWarning,
SpanlyRegion,
} from '@spanly/sdk';SpanlyPacket– the envelope sent to ingest. You won't usually touch this directly; the hook receives the higher-levelMcpPacketandSpanlyPacketContext.McpPacket– the captured JSON-RPC request, response, or notification.SpanlyPacketContext– mutable context: server/client identity, transport, custom fields.SpanlyPacketTransportContext– HTTP path, status, method (when the transport is HTTP/SSE).SpanlyRegion = 'us' | 'eu'– region literal.
What the SDK does not do
- It does not modify the responses your server returns. The captured packet is a copy.
- It does not block your server's request handling on a network call to
ingest. Capture is queued asynchronously; if the queue is full, the
oldest packet is dropped and
onWarning('queue-overflow', …)fires. - It does not require any specific transport. stdio, HTTP, SSE, and
custom transports all work because
monitor()hooks the server's dispatch layer, not the wire.