@spanlens/mcp-server
Advanced tools
+16
-0
@@ -16,2 +16,18 @@ /** | ||
| import type { SpanlensClient } from './client.js'; | ||
| /** | ||
| * Translate the MCP-friendly timeframe enum into hours. | ||
| * | ||
| * The REST API has no `window` param: /stats/overview takes `from`/`to` | ||
| * ISO dates and /stats/models takes `hours`. (v0.2.0 sent `window`, which | ||
| * the server silently ignored — stats came back for the wrong period.) | ||
| */ | ||
| export declare function timeframeToHours(tf?: string): number; | ||
| /** ISO timestamp `hours` ago — the `from` bound for /stats/overview. */ | ||
| export declare function hoursAgoIso(hours: number): string; | ||
| /** | ||
| * Map an ISO `since` bound onto /api/v1/anomalies' `observationHours` | ||
| * param (the server compares the last N hours against a reference window; | ||
| * it has no `from` param). Clamped to the server's accepted 0.25–72 range. | ||
| */ | ||
| export declare function sinceToObservationHours(since: string): number | undefined; | ||
| export declare function registerTools(server: McpServer, client: SpanlensClient): void; |
+56
-19
@@ -9,16 +9,38 @@ import { z } from 'zod'; | ||
| }; | ||
| /** Translate the MCP-friendly enum into the REST API's window param shape. */ | ||
| function timeframeToWindow(tf) { | ||
| /** | ||
| * Translate the MCP-friendly timeframe enum into hours. | ||
| * | ||
| * The REST API has no `window` param: /stats/overview takes `from`/`to` | ||
| * ISO dates and /stats/models takes `hours`. (v0.2.0 sent `window`, which | ||
| * the server silently ignored — stats came back for the wrong period.) | ||
| */ | ||
| export function timeframeToHours(tf) { | ||
| switch (tf) { | ||
| case '1h': | ||
| return '1h'; | ||
| return 1; | ||
| case '24h': | ||
| return '24h'; | ||
| return 24; | ||
| case '30d': | ||
| return '30d'; | ||
| return 30 * 24; | ||
| case '7d': | ||
| default: | ||
| return '7d'; | ||
| return 7 * 24; | ||
| } | ||
| } | ||
| /** ISO timestamp `hours` ago — the `from` bound for /stats/overview. */ | ||
| export function hoursAgoIso(hours) { | ||
| return new Date(Date.now() - hours * 3_600_000).toISOString(); | ||
| } | ||
| /** | ||
| * Map an ISO `since` bound onto /api/v1/anomalies' `observationHours` | ||
| * param (the server compares the last N hours against a reference window; | ||
| * it has no `from` param). Clamped to the server's accepted 0.25–72 range. | ||
| */ | ||
| export function sinceToObservationHours(since) { | ||
| const t = Date.parse(since); | ||
| if (Number.isNaN(t)) | ||
| return undefined; | ||
| const hours = (Date.now() - t) / 3_600_000; | ||
| return Math.min(72, Math.max(0.25, hours)); | ||
| } | ||
| export function registerTools(server, client) { | ||
@@ -37,10 +59,9 @@ // ── 1. get_stats ──────────────────────────────────────────────────────── | ||
| try { | ||
| const hours = timeframeToHours(timeframe); | ||
| if (groupBy === 'model' || groupBy === 'provider') { | ||
| const data = await client.get('/api/v1/stats/models', { | ||
| window: timeframeToWindow(timeframe), | ||
| }); | ||
| const data = await client.get('/api/v1/stats/models', { hours }); | ||
| return formatJson(data); | ||
| } | ||
| const data = await client.get('/api/v1/stats/overview', { | ||
| window: timeframeToWindow(timeframe), | ||
| from: hoursAgoIso(hours), | ||
| }); | ||
@@ -64,3 +85,14 @@ return formatJson(data); | ||
| provider: z | ||
| .enum(['openai', 'anthropic', 'gemini', 'azure']) | ||
| .enum([ | ||
| 'openai', | ||
| 'anthropic', | ||
| 'gemini', | ||
| 'azure', | ||
| 'mistral', | ||
| 'openrouter', | ||
| 'groq', | ||
| 'deepseek', | ||
| 'xai', | ||
| 'cohere', | ||
| ]) | ||
| .optional() | ||
@@ -148,14 +180,19 @@ .describe('Filter to a specific provider.'), | ||
| // ── 5. get_anomalies ──────────────────────────────────────────────────── | ||
| server.tool('get_anomalies', 'List unacknowledged cost / latency / error-rate anomalies the platform has detected. Use when the user asks "anything weird going on?", "any spikes?", or wants a quick health check.', { | ||
| severity: z | ||
| .enum(['low', 'medium', 'high']) | ||
| .optional() | ||
| .describe('Filter to anomalies at or above this severity.'), | ||
| server.tool('get_anomalies', 'List unacknowledged cost / latency / error-rate anomalies the platform has detected. Each anomaly carries a `deviations` field (how many sigmas off baseline). Use when the user asks "anything weird going on?", "any spikes?", or wants a quick health check.', { | ||
| since: z | ||
| .string() | ||
| .optional() | ||
| .describe('ISO 8601 timestamp lower bound. Only return anomalies first seen at or after this time.'), | ||
| }, async ({ severity, since }) => { | ||
| .describe('ISO 8601 timestamp. Sets the observation window: behaviour since this time is compared against the preceding baseline. Clamped to the last 15 minutes – 72 hours; default is the last hour.'), | ||
| sigma: z | ||
| .number() | ||
| .min(1) | ||
| .max(10) | ||
| .optional() | ||
| .describe('Minimum deviations (in sigmas) to flag. Default 3. Lower = more sensitive.'), | ||
| }, async ({ since, sigma }) => { | ||
| try { | ||
| const data = await client.get('/api/v1/anomalies', { severity, from: since }); | ||
| const data = await client.get('/api/v1/anomalies', { | ||
| observationHours: since ? sinceToObservationHours(since) : undefined, | ||
| sigma, | ||
| }); | ||
| return formatJson(data); | ||
@@ -162,0 +199,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| export declare const SERVER_VERSION = "0.2.0"; | ||
| export declare const SERVER_VERSION = "0.2.1"; |
+1
-1
| // Kept in a separate module so package.json doesn't have to be read at runtime | ||
| // (avoids resolveJsonModule pulling the whole manifest into the bundle). | ||
| // Bumped manually with each release; package.json stays canonical for npm. | ||
| export const SERVER_VERSION = '0.2.0'; | ||
| export const SERVER_VERSION = '0.2.1'; |
+1
-1
| { | ||
| "name": "@spanlens/mcp-server", | ||
| "version": "0.2.0", | ||
| "version": "0.2.1", | ||
| "mcpName": "io.github.spanlens/mcp-server", | ||
@@ -5,0 +5,0 @@ "description": "MCP server for Spanlens — query LLM cost, anomalies, traces, and per-user analytics from inside Cursor, Continue, or Claude Desktop.", |
+1
-1
@@ -90,3 +90,3 @@ # @spanlens/mcp-server | ||
| | `get_trace` | Full agent span tree for a trace ID — every LLM/tool/retrieval span with timing, tokens, cost. | | ||
| | `get_anomalies` | Cost / latency / error-rate anomalies the platform has detected. Optional `severity`. | | ||
| | `get_anomalies` | Cost / latency / error-rate anomalies the platform has detected. Optional `since` (observation window) and `sigma` (sensitivity). | | ||
| | `get_savings` | Model-swap recommendations with projected monthly savings and adoption status. | | ||
@@ -93,0 +93,0 @@ | `get_user_analytics` | Per-end-user usage breakdown — total cost, request count, models touched, recent calls. | |
27767
8.56%478
12.47%