Examples
Recipes for common TypeScript SDK integrations.
Claude Desktop / Cursor / Windsurf
These IDEs and chat clients launch your MCP server as a child process
over stdio. The SDK works unchanged – just wire it into the server
before connect().
import { SpanlyClient } from '@spanly/sdk';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const mcpServer = new McpServer({ name: 'my-server', version: '1.0.0' });
// … register tools, prompts, resources …
new SpanlyClient({ apiKey: process.env.SPANLY_API_KEY }).monitor(mcpServer);
await mcpServer.connect(new StdioServerTransport());Pass the API key through the host config:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["./dist/server.js"],
"env": {
"SPANLY_API_KEY": "spanly_us_xxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}Multi-tenant tagging
When the same MCP server handles requests from many tenants, attach the tenant id per request so the dashboard can slice by it.
import { AsyncLocalStorage } from 'node:async_hooks';
const tenantStore = new AsyncLocalStorage<{ tenantId: string }>();
app.use((req, _res, next) => {
const tenantId = extractTenantFromAuth(req);
tenantStore.run({ tenantId }, next);
});
spanly.monitor(mcpServer, {
onCollect: (_direction, context) => {
const scope = tenantStore.getStore();
if (scope) {
context.environmentId = scope.tenantId;
}
return null; // unchanged – onCollect can return the packet or null
},
});The dashboard's filter bar picks up environmentId automatically.
Filtering sensitive tools
Drop a packet entirely by returning null from onCollect:
spanly.monitor(mcpServer, {
onCollect: (_direction, _context, packet) => {
if (packet.method === 'tools/call' &&
packet.params?.name === 'internal-debug-tool') {
return null; // never sent to Spanly
}
return packet;
},
});You can also redact fields in place:
spanly.monitor(mcpServer, {
onCollect: (_direction, _context, packet) => {
if (packet.method === 'tools/call' && packet.params?.arguments?.apiKey) {
packet.params.arguments.apiKey = '[REDACTED]';
}
return packet;
},
});Tests – flush before assertions
import { SpanlyClient } from '@spanly/sdk';
test('captures a tool call', async () => {
const spanly = new SpanlyClient({ apiKey: process.env.SPANLY_API_KEY });
spanly.monitor(server);
await callTool(server, 'echo', { input: 'hi' });
await spanly.flush();
// assertions against the dashboard / capture mock here
});For unit tests against the capture layer, pass a fake endpoint to the
client and run a local HTTP server that records the payloads.
Correlate with OpenTelemetry / Sentry
SpanlyPacketContext is plain JSON, so any trace id you have in scope
can ride along:
import { trace } from '@opentelemetry/api';
spanly.monitor(mcpServer, {
onCollect: (_direction, context) => {
const span = trace.getActiveSpan();
if (span) {
const { traceId, spanId } = span.spanContext();
context.traceId = traceId;
context.spanId = spanId;
}
return null;
},
});Click into a request in the Spanly dashboard and the traceId field is
linkable straight to your APM tool's trace view.