mcp-agent-trace-inspector 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/db.js ADDED
@@ -0,0 +1,107 @@
1
+ // Uses the built-in node:sqlite module (Node.js >= 22.5.0)
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { homedir } from "node:os";
4
+ import { mkdirSync, existsSync } from "node:fs";
5
+ import { dirname, resolve } from "node:path";
6
+ export function expandPath(p) {
7
+ if (p.startsWith("~")) {
8
+ return resolve(homedir() + p.slice(1));
9
+ }
10
+ return resolve(p);
11
+ }
12
+ export function openDatabase(dbPath) {
13
+ const expanded = expandPath(dbPath);
14
+ const dir = dirname(expanded);
15
+ if (!existsSync(dir)) {
16
+ mkdirSync(dir, { recursive: true });
17
+ }
18
+ const db = new DatabaseSync(expanded);
19
+ initSchema(db);
20
+ return db;
21
+ }
22
+ function initSchema(db) {
23
+ db.exec(`
24
+ PRAGMA journal_mode = WAL;
25
+ PRAGMA foreign_keys = ON;
26
+
27
+ CREATE TABLE IF NOT EXISTS traces (
28
+ id TEXT PRIMARY KEY,
29
+ name TEXT NOT NULL,
30
+ status TEXT NOT NULL DEFAULT 'running',
31
+ started_at INTEGER NOT NULL,
32
+ ended_at INTEGER,
33
+ metadata TEXT
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS steps (
37
+ id TEXT PRIMARY KEY,
38
+ trace_id TEXT NOT NULL,
39
+ tool_name TEXT NOT NULL,
40
+ input_json TEXT NOT NULL,
41
+ output_json TEXT NOT NULL,
42
+ token_count INTEGER,
43
+ latency_ms INTEGER,
44
+ created_at INTEGER NOT NULL,
45
+ FOREIGN KEY (trace_id) REFERENCES traces(id)
46
+ );
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_steps_trace_id ON steps(trace_id);
49
+ CREATE INDEX IF NOT EXISTS idx_traces_started_at ON traces(started_at);
50
+ `);
51
+ }
52
+ export function insertTrace(db, id, name) {
53
+ db.prepare(`INSERT INTO traces (id, name, status, started_at) VALUES (?, ?, 'running', ?)`).run(id, name, Date.now());
54
+ }
55
+ export function endTrace(db, id) {
56
+ db.prepare(`UPDATE traces SET status = 'completed', ended_at = ? WHERE id = ?`).run(Date.now(), id);
57
+ }
58
+ export function insertStep(db, step) {
59
+ db.prepare(`
60
+ INSERT INTO steps (id, trace_id, tool_name, input_json, output_json, token_count, latency_ms, created_at)
61
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
62
+ `).run(step.id, step.trace_id, step.tool_name, step.input_json, step.output_json, step.token_count ?? null, step.latency_ms ?? null, Date.now());
63
+ }
64
+ export function getTrace(db, id) {
65
+ return db.prepare(`SELECT * FROM traces WHERE id = ?`).get(id);
66
+ }
67
+ export function getSteps(db, traceId) {
68
+ return db
69
+ .prepare(`SELECT * FROM steps WHERE trace_id = ? ORDER BY created_at ASC`)
70
+ .all(traceId);
71
+ }
72
+ export function listTraces(db, limit) {
73
+ if (limit && limit > 0) {
74
+ return db
75
+ .prepare(`SELECT * FROM traces ORDER BY started_at DESC LIMIT ?`)
76
+ .all(limit);
77
+ }
78
+ return db
79
+ .prepare(`SELECT * FROM traces ORDER BY started_at DESC`)
80
+ .all();
81
+ }
82
+ export function deleteOldTraces(db, retentionDays) {
83
+ if (retentionDays <= 0)
84
+ return 0;
85
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
86
+ // Delete steps for old traces first (foreign key safety)
87
+ db.prepare(`DELETE FROM steps WHERE trace_id IN (SELECT id FROM traces WHERE started_at < ?)`).run(cutoff);
88
+ const result = db
89
+ .prepare(`DELETE FROM traces WHERE started_at < ?`)
90
+ .run(cutoff);
91
+ return result.changes;
92
+ }
93
+ export function computeSummary(db, traceId) {
94
+ const trace = getTrace(db, traceId);
95
+ if (!trace)
96
+ return null;
97
+ const steps = getSteps(db, traceId);
98
+ const totalTokens = steps.reduce((acc, s) => acc + (s.token_count ?? 0), 0);
99
+ const totalLatencyMs = steps.reduce((acc, s) => acc + (s.latency_ms ?? 0), 0);
100
+ return {
101
+ trace,
102
+ stepCount: steps.length,
103
+ totalTokens,
104
+ totalLatencyMs,
105
+ steps,
106
+ };
107
+ }
@@ -0,0 +1 @@
1
+ export declare function startHttpServer(port: number, dbPath: string, noTokenCount: boolean): Promise<void>;
@@ -0,0 +1,34 @@
1
+ import express from "express";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { createServer } from "./server.js";
4
+ import { openDatabase } from "./db.js";
5
+ import { createAuthMiddleware } from "./auth.js";
6
+ import { createRateLimiter } from "./rate-limiter.js";
7
+ export async function startHttpServer(port, dbPath, noTokenCount) {
8
+ const app = express();
9
+ app.use(express.json());
10
+ const db = openDatabase(dbPath);
11
+ const server = createServer({ db, noTokenCount });
12
+ const auth = createAuthMiddleware();
13
+ const rateLimiter = createRateLimiter(60, 60000);
14
+ app.post("/mcp", auth, rateLimiter, async (req, res) => {
15
+ const transport = new StreamableHTTPServerTransport({
16
+ sessionIdGenerator: undefined,
17
+ });
18
+ await server.connect(transport);
19
+ await transport.handleRequest(req, res, req.body);
20
+ });
21
+ app.get("/mcp", auth, rateLimiter, async (req, res) => {
22
+ const transport = new StreamableHTTPServerTransport({
23
+ sessionIdGenerator: undefined,
24
+ });
25
+ await server.connect(transport);
26
+ await transport.handleRequest(req, res);
27
+ });
28
+ app.delete("/mcp", (_req, res) => {
29
+ res.status(405).json({ error: "Method not allowed" });
30
+ });
31
+ app.listen(port, () => {
32
+ console.error(`MCP Agent Trace Inspector HTTP server listening on port ${port}`);
33
+ });
34
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import yargs from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import { openDatabase, deleteOldTraces } from "./db.js";
6
+ import { createServer } from "./server.js";
7
+ import { loadPricingTable } from "./pricing.js";
8
+ import { startHttpServer } from "./http-server.js";
9
+ async function main() {
10
+ const argv = await yargs(hideBin(process.argv))
11
+ .option("db", {
12
+ alias: "db-path",
13
+ type: "string",
14
+ default: "~/.mcp/traces.db",
15
+ description: "Path to the SQLite database file",
16
+ })
17
+ .option("retention-days", {
18
+ type: "number",
19
+ default: 0,
20
+ description: "Auto-delete traces older than this many days. 0 = disabled.",
21
+ })
22
+ .option("no-token-count", {
23
+ type: "boolean",
24
+ default: false,
25
+ description: "Disable token counting",
26
+ })
27
+ .option("pricing-table", {
28
+ type: "string",
29
+ description: "Path to a custom JSON pricing table file",
30
+ })
31
+ .option("http-port", {
32
+ type: "number",
33
+ description: "Start in HTTP mode on the given port instead of stdio. Example: --http-port=3000",
34
+ })
35
+ .help()
36
+ .parseAsync();
37
+ // Load pricing table (for future use / extensions)
38
+ if (argv["pricing-table"]) {
39
+ try {
40
+ const customPricing = loadPricingTable(argv["pricing-table"]);
41
+ console.error(`[init] Loaded custom pricing table with ${Object.keys(customPricing).length} model(s)`);
42
+ }
43
+ catch (err) {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ console.error(`[warn] Failed to load pricing table: ${message}`);
46
+ }
47
+ }
48
+ // Open database (uses built-in node:sqlite)
49
+ const dbPath = argv.db;
50
+ const db = openDatabase(dbPath);
51
+ console.error(`[init] Database opened at: ${dbPath}`);
52
+ // Apply retention policy
53
+ const retentionDays = argv["retention-days"];
54
+ if (retentionDays > 0) {
55
+ const deleted = deleteOldTraces(db, retentionDays);
56
+ if (deleted > 0) {
57
+ console.error(`[retention] Deleted ${deleted} trace(s) older than ${retentionDays} day(s)`);
58
+ }
59
+ }
60
+ const noTokenCount = argv["no-token-count"];
61
+ const httpPort = argv["http-port"];
62
+ if (httpPort !== undefined) {
63
+ await startHttpServer(httpPort, dbPath, noTokenCount);
64
+ return;
65
+ }
66
+ const server = createServer({ db, noTokenCount });
67
+ const transport = new StdioServerTransport();
68
+ await server.connect(transport);
69
+ console.error("[init] MCP Agent Trace Inspector server running on stdio");
70
+ }
71
+ main().catch((err) => {
72
+ console.error("[fatal]", err);
73
+ process.exit(1);
74
+ });
@@ -0,0 +1,20 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ export interface OTLPSpan {
3
+ traceId: string;
4
+ spanId: string;
5
+ parentSpanId?: string;
6
+ name: string;
7
+ startTimeUnixNano: string;
8
+ endTimeUnixNano: string;
9
+ attributes: Record<string, string | number | boolean>;
10
+ status: {
11
+ code: number;
12
+ message?: string;
13
+ };
14
+ }
15
+ export interface OTLPTrace {
16
+ traceId: string;
17
+ spans: OTLPSpan[];
18
+ }
19
+ export declare function exportToOTLP(db: DatabaseSync, traceId: string): OTLPTrace;
20
+ export declare function exportAllOTLP(db: DatabaseSync): OTLPTrace[];
@@ -0,0 +1,98 @@
1
+ import { getTrace, getSteps, listTraces } from "./db.js";
2
+ /** Convert a UUID-like string to a 32-hex-char trace ID (no hyphens) */
3
+ function toTraceId(id) {
4
+ return id.replace(/-/g, "").padEnd(32, "0").slice(0, 32);
5
+ }
6
+ /** Convert a UUID-like string to a 16-hex-char span ID (no hyphens) */
7
+ function toSpanId(id) {
8
+ return id.replace(/-/g, "").padEnd(16, "0").slice(0, 16);
9
+ }
10
+ function msToNano(ms) {
11
+ return (BigInt(Math.floor(ms)) * 1000000n).toString();
12
+ }
13
+ function traceRowToRootSpan(trace) {
14
+ const endMs = trace.ended_at ?? trace.started_at;
15
+ const statusCode = trace.status === "completed" ? 1 : 2; // 1=OK, 2=ERROR
16
+ const attributes = {
17
+ "trace.name": trace.name,
18
+ "trace.status": trace.status,
19
+ };
20
+ if (trace.metadata) {
21
+ try {
22
+ const meta = JSON.parse(trace.metadata);
23
+ for (const [k, v] of Object.entries(meta)) {
24
+ if (typeof v === "string" ||
25
+ typeof v === "number" ||
26
+ typeof v === "boolean") {
27
+ attributes[`trace.metadata.${k}`] = v;
28
+ }
29
+ }
30
+ }
31
+ catch {
32
+ // ignore malformed metadata
33
+ }
34
+ }
35
+ return {
36
+ traceId: toTraceId(trace.id),
37
+ spanId: toSpanId(trace.id),
38
+ name: trace.name,
39
+ startTimeUnixNano: msToNano(trace.started_at),
40
+ endTimeUnixNano: msToNano(endMs),
41
+ attributes,
42
+ status: { code: statusCode, message: trace.status },
43
+ };
44
+ }
45
+ function stepRowToSpan(step, traceId, parentSpanId) {
46
+ const endMs = step.created_at + (step.latency_ms ?? 0);
47
+ const attributes = {
48
+ "step.tool_name": step.tool_name,
49
+ "step.trace_id": step.trace_id,
50
+ };
51
+ if (step.token_count !== null && step.token_count !== undefined) {
52
+ attributes["step.token_count"] = step.token_count;
53
+ }
54
+ if (step.latency_ms !== null && step.latency_ms !== undefined) {
55
+ attributes["step.latency_ms"] = step.latency_ms;
56
+ }
57
+ // Check for error in output
58
+ let statusCode = 1; // OK
59
+ try {
60
+ const output = JSON.parse(step.output_json);
61
+ if (output.error || output.isError === true) {
62
+ statusCode = 2; // ERROR
63
+ if (typeof output.error === "string") {
64
+ attributes["error.message"] = output.error;
65
+ }
66
+ }
67
+ }
68
+ catch {
69
+ // ignore
70
+ }
71
+ return {
72
+ traceId: toTraceId(traceId),
73
+ spanId: toSpanId(step.id),
74
+ parentSpanId,
75
+ name: step.tool_name,
76
+ startTimeUnixNano: msToNano(step.created_at),
77
+ endTimeUnixNano: msToNano(endMs),
78
+ attributes,
79
+ status: { code: statusCode },
80
+ };
81
+ }
82
+ export function exportToOTLP(db, traceId) {
83
+ const trace = getTrace(db, traceId);
84
+ if (!trace) {
85
+ throw new Error(`Unknown trace_id: ${traceId}`);
86
+ }
87
+ const steps = getSteps(db, traceId);
88
+ const rootSpan = traceRowToRootSpan(trace);
89
+ const stepSpans = steps.map((s) => stepRowToSpan(s, traceId, rootSpan.spanId));
90
+ return {
91
+ traceId: toTraceId(traceId),
92
+ spans: [rootSpan, ...stepSpans],
93
+ };
94
+ }
95
+ export function exportAllOTLP(db) {
96
+ const traces = listTraces(db);
97
+ return traces.map((t) => exportToOTLP(db, t.id));
98
+ }
@@ -0,0 +1,10 @@
1
+ export interface ModelPricing {
2
+ input: number;
3
+ output: number;
4
+ }
5
+ export interface PricingTable {
6
+ [modelName: string]: ModelPricing;
7
+ }
8
+ export declare const DEFAULT_PRICING: PricingTable;
9
+ export declare function loadPricingTable(filePath: string): PricingTable;
10
+ export declare function estimateCost(tokenCount: number, model: string, pricing: PricingTable): number | null;
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from "fs";
2
+ export const DEFAULT_PRICING = {
3
+ "claude-opus-4-6": { input: 0.015, output: 0.075 },
4
+ "claude-sonnet-4-6": { input: 0.003, output: 0.015 },
5
+ "claude-haiku-4-5": { input: 0.0008, output: 0.004 },
6
+ };
7
+ export function loadPricingTable(filePath) {
8
+ try {
9
+ const content = readFileSync(filePath, "utf-8");
10
+ const parsed = JSON.parse(content);
11
+ if (typeof parsed !== "object" ||
12
+ parsed === null ||
13
+ Array.isArray(parsed)) {
14
+ throw new Error("Pricing table must be a JSON object");
15
+ }
16
+ const table = parsed;
17
+ for (const [model, pricing] of Object.entries(table)) {
18
+ if (typeof pricing !== "object" ||
19
+ pricing === null ||
20
+ typeof pricing.input !== "number" ||
21
+ typeof pricing.output !== "number") {
22
+ throw new Error(`Invalid pricing entry for model "${model}": must have numeric input and output fields`);
23
+ }
24
+ }
25
+ return table;
26
+ }
27
+ catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ throw new Error(`Failed to load pricing table from "${filePath}": ${message}`);
30
+ }
31
+ }
32
+ export function estimateCost(tokenCount, model, pricing) {
33
+ const entry = pricing[model];
34
+ if (!entry)
35
+ return null;
36
+ // Treat all tokens as input tokens for simple estimation
37
+ return (tokenCount / 1000) * entry.input;
38
+ }
@@ -0,0 +1,21 @@
1
+ export declare function handleListPrompts(): {
2
+ prompts: Array<{
3
+ name: string;
4
+ description: string;
5
+ arguments: Array<{
6
+ name: string;
7
+ description: string;
8
+ required: boolean;
9
+ }>;
10
+ }>;
11
+ };
12
+ export declare function handleGetPrompt(name: string, args: Record<string, string> | undefined): {
13
+ description: string;
14
+ messages: Array<{
15
+ role: string;
16
+ content: {
17
+ type: string;
18
+ text: string;
19
+ };
20
+ }>;
21
+ };
@@ -0,0 +1,66 @@
1
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
2
+ const ANALYZE_TRACE_PROMPT = `You are an expert in analyzing AI agent workflow traces.
3
+
4
+ Given the trace data for trace_id: {{trace_id}}, please perform a thorough investigation:
5
+
6
+ 1. **Overview**: Summarize what the agent was trying to accomplish based on the tool names and inputs.
7
+
8
+ 2. **Performance Analysis**:
9
+ - Identify the slowest steps (highest latency_ms) and explain potential causes.
10
+ - Highlight any steps with unusually high token consumption.
11
+ - Calculate and comment on the total cost if pricing data is available.
12
+
13
+ 3. **Error Detection**:
14
+ - Flag any steps where the output contains an error field or isError flag.
15
+ - Suggest remediation for each error found.
16
+
17
+ 4. **Reasoning Chain**:
18
+ - Detect if the agent follows a prompt→reasoning→action pattern.
19
+ - Assess whether the reasoning steps appear logically sound given the inputs.
20
+
21
+ 5. **Optimization Opportunities**:
22
+ - Identify redundant steps (same tool called multiple times with similar inputs).
23
+ - Suggest steps that could be parallelized.
24
+ - Recommend caching opportunities.
25
+
26
+ 6. **Overall Assessment**:
27
+ - Rate the trace quality (1–5) on: efficiency, error handling, and goal completion.
28
+ - Provide 3 specific, actionable recommendations to improve this workflow.
29
+
30
+ Use the get_trace_summary tool with trace_id={{trace_id}} to fetch the data before beginning your analysis.`;
31
+ export function handleListPrompts() {
32
+ return {
33
+ prompts: [
34
+ {
35
+ name: "analyze-trace",
36
+ description: "Guide an investigation of a completed agent trace — surfaces performance bottlenecks, errors, reasoning chains, and optimization opportunities.",
37
+ arguments: [
38
+ {
39
+ name: "trace_id",
40
+ description: "The ID of the trace to analyze.",
41
+ required: true,
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ export function handleGetPrompt(name, args) {
49
+ if (name !== "analyze-trace") {
50
+ throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${name}`);
51
+ }
52
+ const traceId = args?.trace_id ?? "<trace_id>";
53
+ const promptText = ANALYZE_TRACE_PROMPT.replace(/{{trace_id}}/g, traceId);
54
+ return {
55
+ description: "Analyze an agent trace for performance, errors, reasoning chains, and optimization opportunities.",
56
+ messages: [
57
+ {
58
+ role: "user",
59
+ content: {
60
+ type: "text",
61
+ text: promptText,
62
+ },
63
+ },
64
+ ],
65
+ };
66
+ }
@@ -0,0 +1,2 @@
1
+ import type { RequestHandler } from "express";
2
+ export declare function createRateLimiter(maxRequests?: number, windowMs?: number): RequestHandler;
@@ -0,0 +1,34 @@
1
+ function getClientKey(req) {
2
+ const apiKey = req.headers["x-api-key"];
3
+ if (typeof apiKey === "string" && apiKey) {
4
+ return `apikey:${apiKey}`;
5
+ }
6
+ // Fall back to IP address
7
+ const ip = req.ip ?? req.socket?.remoteAddress ?? "unknown";
8
+ return `ip:${ip}`;
9
+ }
10
+ export function createRateLimiter(maxRequests = 60, windowMs = 60000) {
11
+ // Each limiter instance has its own window map (not module-level)
12
+ const windows = new Map();
13
+ return (req, res, next) => {
14
+ const key = getClientKey(req);
15
+ const now = Date.now();
16
+ const windowStart = now - windowMs;
17
+ let entry = windows.get(key);
18
+ if (!entry) {
19
+ entry = { timestamps: [] };
20
+ windows.set(key, entry);
21
+ }
22
+ // Slide the window: remove timestamps older than windowMs
23
+ entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
24
+ if (entry.timestamps.length >= maxRequests) {
25
+ res.status(429).json({
26
+ error: "Too Many Requests",
27
+ retryAfterMs: windowMs,
28
+ });
29
+ return;
30
+ }
31
+ entry.timestamps.push(now);
32
+ next();
33
+ };
34
+ }
@@ -0,0 +1,16 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ export declare function handleListResources(db: DatabaseSync): {
3
+ resources: Array<{
4
+ uri: string;
5
+ name: string;
6
+ description: string;
7
+ mimeType: string;
8
+ }>;
9
+ };
10
+ export declare function handleReadResource(db: DatabaseSync, uri: string): {
11
+ contents: Array<{
12
+ uri: string;
13
+ mimeType: string;
14
+ text: string;
15
+ }>;
16
+ };
@@ -0,0 +1,55 @@
1
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
2
+ import { listTraces, computeSummary } from "./db.js";
3
+ export function handleListResources(db) {
4
+ const traces = listTraces(db);
5
+ return {
6
+ resources: traces.map((t) => ({
7
+ uri: `trace://${t.id}`,
8
+ name: t.name,
9
+ description: `Agent trace "${t.name}" — status: ${t.status}, started: ${new Date(t.started_at).toISOString()}`,
10
+ mimeType: "application/json",
11
+ })),
12
+ };
13
+ }
14
+ export function handleReadResource(db, uri) {
15
+ const match = uri.match(/^trace:\/\/(.+)$/);
16
+ if (!match) {
17
+ throw new McpError(ErrorCode.InvalidParams, `Unsupported resource URI: ${uri}`);
18
+ }
19
+ const traceId = match[1];
20
+ const summary = computeSummary(db, traceId);
21
+ if (!summary) {
22
+ throw new McpError(ErrorCode.InvalidParams, `Unknown trace: ${traceId}`);
23
+ }
24
+ const { trace, stepCount, totalTokens, totalLatencyMs, steps } = summary;
25
+ const durationMs = trace.ended_at != null
26
+ ? trace.ended_at - trace.started_at
27
+ : Date.now() - trace.started_at;
28
+ const content = {
29
+ trace_id: trace.id,
30
+ name: trace.name,
31
+ status: trace.status,
32
+ started_at: trace.started_at,
33
+ ended_at: trace.ended_at,
34
+ duration_ms: durationMs,
35
+ step_count: stepCount,
36
+ total_tokens: totalTokens,
37
+ total_latency_ms: totalLatencyMs,
38
+ steps: steps.map((s) => ({
39
+ id: s.id,
40
+ tool_name: s.tool_name,
41
+ token_count: s.token_count,
42
+ latency_ms: s.latency_ms,
43
+ created_at: s.created_at,
44
+ })),
45
+ };
46
+ return {
47
+ contents: [
48
+ {
49
+ uri,
50
+ mimeType: "application/json",
51
+ text: JSON.stringify(content, null, 2),
52
+ },
53
+ ],
54
+ };
55
+ }
@@ -0,0 +1,13 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ export interface RetentionResult {
3
+ archived: number;
4
+ deleted: number;
5
+ }
6
+ /**
7
+ * Applies retention policy:
8
+ * - Marks traces older than `retentionDays` as archived (archived=1)
9
+ * - Deletes archived traces that are older than 2× retentionDays (plus their steps)
10
+ *
11
+ * Returns counts of archived and deleted traces.
12
+ */
13
+ export declare function applyRetentionPolicy(db: DatabaseSync, retentionDays: number): RetentionResult;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Ensures the traces table has an `archived` column.
3
+ * If it already exists, the ALTER TABLE is a no-op.
4
+ */
5
+ function ensureArchivedColumn(db) {
6
+ try {
7
+ db.exec("ALTER TABLE traces ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
8
+ }
9
+ catch {
10
+ // Column already exists — ignore
11
+ }
12
+ }
13
+ /**
14
+ * Applies retention policy:
15
+ * - Marks traces older than `retentionDays` as archived (archived=1)
16
+ * - Deletes archived traces that are older than 2× retentionDays (plus their steps)
17
+ *
18
+ * Returns counts of archived and deleted traces.
19
+ */
20
+ export function applyRetentionPolicy(db, retentionDays) {
21
+ if (retentionDays <= 0) {
22
+ return { archived: 0, deleted: 0 };
23
+ }
24
+ ensureArchivedColumn(db);
25
+ const now = Date.now();
26
+ const archiveCutoff = now - retentionDays * 24 * 60 * 60 * 1000;
27
+ const deleteCutoff = now - 2 * retentionDays * 24 * 60 * 60 * 1000;
28
+ // Archive traces older than retentionDays that aren't already archived
29
+ const archiveResult = db
30
+ .prepare(`UPDATE traces SET archived = 1 WHERE started_at < ? AND archived = 0`)
31
+ .run(archiveCutoff);
32
+ const archived = archiveResult.changes;
33
+ // Delete steps for archived traces past 2× threshold
34
+ db.prepare(`DELETE FROM steps WHERE trace_id IN (
35
+ SELECT id FROM traces WHERE archived = 1 AND started_at < ?
36
+ )`).run(deleteCutoff);
37
+ // Delete the archived traces past 2× threshold
38
+ const deleteResult = db
39
+ .prepare(`DELETE FROM traces WHERE archived = 1 AND started_at < ?`)
40
+ .run(deleteCutoff);
41
+ const deleted = deleteResult.changes;
42
+ return { archived, deleted };
43
+ }
@@ -0,0 +1,9 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ export declare function isRequestCancelled(requestId: string): boolean;
4
+ export declare function clearCancellation(requestId: string): void;
5
+ export interface ServerOptions {
6
+ db: DatabaseSync;
7
+ noTokenCount: boolean;
8
+ }
9
+ export declare function createServer(options: ServerOptions): Server;