Delete Trace
delete_traceRemoves a trace and its spans by trace ID while preserving evaluation results for audit. Use to delete erroneous or sensitive traces for compliance or data cleanup.
Instructions
Remove a single trace by id. Cascades to spans; eval_results keep the score history with trace_id NULLed.
Sibling tools — log_trace creates traces, get_traces queries them, evaluate_output / evaluate_with_llm_judge / verify_citations score them. delete_rule handles custom-rule deletion (separate concern); list_rules / deploy_rule manage the custom-rule lifecycle. delete_trace is the DESTRUCTIVE single-row remove for traces; it does NOT touch eval_results (preserved for audit + drift analytics), spans cascade automatically.
Behavior. DESTRUCTIVE — SQL DELETE scoped to the caller's tenant_id. Cascades: spans belonging to this trace are deleted (FK ON DELETE CASCADE); eval_results that referenced this trace have their trace_id set to NULL (FK ON DELETE SET NULL) so aggregate dashboards + historical scores remain valid even after the trace is gone. Not idempotent: deleting an already-deleted trace returns deleted: false. Does not emit an audit log entry in v0.4 — traces are user-scope data, not policy changes. Rate-limited to 20 req/min on HTTP MCP.
Output shape. Returns JSON: { "deleted": boolean, "trace_id": string }. deleted=true if a row was removed; deleted=false if no trace with that id existed (or it belonged to a different tenant — cross-tenant deletes silently fail).
Use when a trace was captured in error, contains sensitive data that must be removed for compliance (e.g., a customer exercises GDPR right-to-erasure), or when cleaning up test data. Combine with get_traces to find candidates: query with filters → review → delete_trace(id) per target. For bulk time-window deletion, use deleteTracesOlderThan via the CLI / retention config — delete_trace is the single-row surgical path.
Don't use to clean up OLD data in bulk (use retention config with --retention-days). Don't use to PAUSE a trace — traces are immutable once stored; there's nothing to pause. Don't use to delete eval_results — eval_results survive their trace's deletion intentionally (for audit + drift analysis); they're pruned only by retention.
Parameters. trace_id is the only parameter; must match 32-char lowercase hex (Zod regex). The trace_id you pass is exactly what log_trace returned in its response, or what get_traces returned per row. Format mismatch fails Zod with 400 BEFORE the storage layer is touched. Cross-tenant trace_ids return deleted: false silently — they're invisible to the caller's tenant (prevents enumeration attacks; matches delete_rule's tenant-isolation contract).
Error modes. Throws 400 on malformed trace_id (wrong format: not 32-char lowercase hex). Returns {deleted: false} when the id doesn't exist in the caller's tenant (not an error — the trace may simply have been deleted already). Returns 429 on HTTP rate limit. Storage failures propagate as 500.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| trace_id | Yes | Trace id to delete (32-hex lowercase; obtained from log_trace response or get_traces) |
Implementation Reference
- src/tools/delete-trace.ts:57-67 (handler)The handler function that executes the delete_trace logic: calls storage.deleteTrace and returns the result as JSON.
async (args) => { const deleted = await storage.deleteTrace(LOCAL_TENANT, args.trace_id); return { content: [ { type: 'text' as const, text: JSON.stringify({ deleted, trace_id: args.trace_id }), }, ], }; }, - src/tools/delete-trace.ts:17-22 (schema)Input schema definition using Zod: trace_id must be a 32-character lowercase hex string.
const inputSchema = { trace_id: z .string() .regex(/^[a-f0-9]{32}$/) .describe('Trace id to delete (32-hex lowercase; obtained from log_trace response or get_traces)'), }; - src/tools/delete-trace.ts:24-69 (registration)Registration function that calls server.registerTool with name 'delete_trace', full description, input schema, annotations, and handler.
export function registerDeleteTraceTool( server: McpServer, storage: IStorageAdapter, ): void { server.registerTool( 'delete_trace', { title: 'Delete Trace', description: [ 'Remove a single trace by id. Cascades to spans; eval_results keep the score history with trace_id NULLed.', '', 'Sibling tools — log_trace creates traces, get_traces queries them, evaluate_output / evaluate_with_llm_judge / verify_citations score them. delete_rule handles custom-rule deletion (separate concern); list_rules / deploy_rule manage the custom-rule lifecycle. delete_trace is the DESTRUCTIVE single-row remove for traces; it does NOT touch eval_results (preserved for audit + drift analytics), spans cascade automatically.', '', 'Behavior. DESTRUCTIVE — SQL DELETE scoped to the caller\'s tenant_id. Cascades: spans belonging to this trace are deleted (FK ON DELETE CASCADE); eval_results that referenced this trace have their trace_id set to NULL (FK ON DELETE SET NULL) so aggregate dashboards + historical scores remain valid even after the trace is gone. Not idempotent: deleting an already-deleted trace returns `deleted: false`. Does not emit an audit log entry in v0.4 — traces are user-scope data, not policy changes. Rate-limited to 20 req/min on HTTP MCP.', '', 'Output shape. Returns JSON: `{ "deleted": boolean, "trace_id": string }`. `deleted=true` if a row was removed; `deleted=false` if no trace with that id existed (or it belonged to a different tenant — cross-tenant deletes silently fail).', '', "Use when a trace was captured in error, contains sensitive data that must be removed for compliance (e.g., a customer exercises GDPR right-to-erasure), or when cleaning up test data. Combine with get_traces to find candidates: query with filters → review → delete_trace(id) per target. For bulk time-window deletion, use `deleteTracesOlderThan` via the CLI / retention config — delete_trace is the single-row surgical path.", '', "Don't use to clean up OLD data in bulk (use retention config with --retention-days). Don't use to PAUSE a trace — traces are immutable once stored; there's nothing to pause. Don't use to delete eval_results — eval_results survive their trace's deletion intentionally (for audit + drift analysis); they're pruned only by retention.", '', 'Parameters. trace_id is the only parameter; must match 32-char lowercase hex (Zod regex). The trace_id you pass is exactly what log_trace returned in its response, or what get_traces returned per row. Format mismatch fails Zod with 400 BEFORE the storage layer is touched. Cross-tenant trace_ids return `deleted: false` silently — they\'re invisible to the caller\'s tenant (prevents enumeration attacks; matches delete_rule\'s tenant-isolation contract).', '', "Error modes. Throws 400 on malformed trace_id (wrong format: not 32-char lowercase hex). Returns `{deleted: false}` when the id doesn't exist in the caller's tenant (not an error — the trace may simply have been deleted already). Returns 429 on HTTP rate limit. Storage failures propagate as 500.", ].join('\n'), inputSchema, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, }, }, async (args) => { const deleted = await storage.deleteTrace(LOCAL_TENANT, args.trace_id); return { content: [ { type: 'text' as const, text: JSON.stringify({ deleted, trace_id: args.trace_id }), }, ], }; }, ); } - src/tools/index.ts:11-30 (registration)Import and registration call within registerAllTools(), wiring the tool into the MCP server.
import { registerDeleteTraceTool } from './delete-trace.js'; import { registerEvaluateWithLLMJudgeTool } from './evaluate-with-llm-judge.js'; import { registerVerifyCitationsTool } from './verify-citations.js'; export function registerAllTools( server: McpServer, storage: IStorageAdapter, evalEngine: EvalEngine, customRuleStore: CustomRuleStore, ): void { registerLogTraceTool(server, storage); registerEvaluateOutputTool(server, storage, evalEngine); registerGetTracesTool(server, storage); registerListRulesTool(server, customRuleStore); registerDeployRuleTool(server, customRuleStore); registerDeleteRuleTool(server, customRuleStore); registerDeleteTraceTool(server, storage); registerEvaluateWithLLMJudgeTool(server, storage); registerVerifyCitationsTool(server, storage); } - Storage-layer implementation: tenant-scoped SQL DELETE from traces table, returns boolean indicating if a row was deleted.
async deleteTrace(tenantId: TenantId, traceId: string): Promise<boolean> { assertTenant(tenantId); // Tenant-scoped: a trace id owned by a different tenant is // untouchable from this call. Cross-tenant deletions are not just // denied — they're invisible (no indication the id even exists). const result = this.db .prepare('DELETE FROM traces WHERE tenant_id = ? AND trace_id = ?') .run(tenantId, traceId); return result.changes > 0; }