Skip to main content
Glama

export_audit

Export audit logs as portable text (JSONL, JSON, or CSV) for compliance, archiving, or integration with external SIEMs. Filter events by time range.

Instructions

[audit] Export the audit log as a portable text artifact suitable for archiving or feeding into another SIEM/analyzer. Use for compliance exports, after-the-fact investigations, or to hand the trail to a non-MCP consumer; prefer audit_log for an in-conversation tail and verify_audit_chain to confirm integrity before exporting. Read-only. Returns the rendered text directly (no JSON wrapper). 'jsonl' is one event per line; 'json' is a single array; 'csv' is a header row plus events. Time filters are applied to the event timestamps before formatting.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
sinceNoInclusive lower bound on event timestamp, ISO 8601. Example: '2026-04-01T00:00:00Z'. Omit for no lower bound.
untilNoInclusive upper bound on event timestamp, ISO 8601. Omit for now/no upper bound.
formatNoOutput format. 'jsonl' (default) is most stream-friendly; 'json' is a single array; 'csv' is spreadsheet-friendly.jsonl

Implementation Reference

  • The core implementation of exportAudit — reads the audit.jsonl file, filters by time range (since/until), and formats output as jsonl (default), json, or csv.
    export function exportAudit(opts: ExportOptions = {}): string {
      const path = getAuditPath();
      if (!existsSync(path)) return opts.format === "json" ? "[]" : "";
    
      const lines = readFileSync(path, "utf8")
        .split("\n")
        .filter((l) => l.trim());
    
      let events: AuditEvent[] = lines
        .map((l) => {
          try {
            return JSON.parse(l) as AuditEvent;
          } catch {
            return null;
          }
        })
        .filter((e): e is AuditEvent => e !== null);
    
      if (opts.since) {
        const since = new Date(opts.since).getTime();
        events = events.filter((e) => new Date(e.timestamp).getTime() >= since);
      }
      if (opts.until) {
        const until = new Date(opts.until).getTime();
        events = events.filter((e) => new Date(e.timestamp).getTime() <= until);
      }
    
      if (opts.format === "json") {
        return JSON.stringify(events, null, 2);
      }
    
      if (opts.format === "csv") {
        const header = "timestamp,action,key,scope,env,source,pid,correlationId,detail";
        const rows = events.map(
          (e) =>
            `${e.timestamp},${e.action},${e.key ?? ""},${e.scope ?? ""},${e.env ?? ""},${e.source},${e.pid},${e.correlationId ?? ""},${(e.detail ?? "").replace(/,/g, ";")}`,
        );
        return [header, ...rows].join("\n");
      }
    
      return events.map((e) => JSON.stringify(e)).join("\n");
    }
  • ExportOptions interface defining the input parameters: since (ISO date), until (ISO date), format (jsonl|json|csv).
    export interface ExportOptions {
      since?: string;
      until?: string;
      format?: "jsonl" | "json" | "csv";
    }
  • MCP tool handler (server.tool) that registers the 'export_audit' tool with Zod schema validation for params, policy enforcement, and calls exportAudit from observer.ts.
    server.tool(
      "export_audit",
      [
        "[audit] Export the audit log as a portable text artifact suitable for archiving or feeding into another SIEM/analyzer.",
        "Use for compliance exports, after-the-fact investigations, or to hand the trail to a non-MCP consumer; prefer `audit_log` for an in-conversation tail and `verify_audit_chain` to confirm integrity before exporting.",
        "Read-only. Returns the rendered text directly (no JSON wrapper). 'jsonl' is one event per line; 'json' is a single array; 'csv' is a header row plus events. Time filters are applied to the event timestamps before formatting.",
      ].join(" "),
      {
        since: z
          .string()
          .optional()
          .describe(
            "Inclusive lower bound on event timestamp, ISO 8601. Example: '2026-04-01T00:00:00Z'. Omit for no lower bound.",
          ),
        until: z
          .string()
          .optional()
          .describe(
            "Inclusive upper bound on event timestamp, ISO 8601. Omit for now/no upper bound.",
          ),
        format: z
          .enum(["jsonl", "json", "csv"])
          .optional()
          .default("jsonl")
          .describe(
            "Output format. 'jsonl' (default) is most stream-friendly; 'json' is a single array; 'csv' is spreadsheet-friendly.",
          ),
      },
      async (params) => {
        const toolBlock = enforceToolPolicy("export_audit");
        if (toolBlock) return toolBlock;
    
        const output = exportAudit({
          since: params.since,
          until: params.until,
          format: params.format,
        });
        return text(output);
      },
    );
  • registerAuditTools function (exported) that registers the 'export_audit' tool (among others) on the McpServer.
    export function registerAuditTools(server: McpServer): void {
      server.tool(
        "audit_log",
        [
          "[audit] Query the q-ring audit log — a tamper-evident record of every read/write/delete touching a secret.",
          "Use to investigate 'who accessed KEY recently?' or to feed an agent the access timeline for a specific credential; prefer `detect_anomalies` for automated unusual-pattern detection and `health_check` for decay-state-plus-anomalies in one call.",
          "Read-only. Returns one line per event in chronological order, formatted `timestamp | action | key | [scope] | env:NAME | detail`. Returns 'No audit events found' when the filter matches nothing.",
        ].join(" "),
        {
          key: z
            .string()
            .optional()
            .describe(
              "Limit to events touching this exact key. Omit for the full log.",
            ),
          action: z
            .enum([
              "read",
              "write",
              "delete",
              "list",
              "export",
              "generate",
              "entangle",
              "tunnel",
              "teleport",
              "collapse",
            ])
            .optional()
            .describe(
              "Limit to a single action verb (e.g. 'read' to see only reads). Omit for all actions.",
            ),
          limit: z
            .number()
            .optional()
            .default(20)
            .describe(
              "Maximum events to return, newest first. Defaults to 20. Increase for deeper investigations.",
            ),
        },
        async (params) => {
          const toolBlock = enforceToolPolicy("audit_log");
          if (toolBlock) return toolBlock;
    
          const events = queryAudit({
            key: params.key,
            action: params.action,
            limit: params.limit,
          });
    
          if (events.length === 0) return text("No audit events found");
    
          const lines = events.map((e) => {
            const parts = [e.timestamp, e.action];
            if (e.key) parts.push(e.key);
            if (e.scope) parts.push(`[${e.scope}]`);
            if (e.env) parts.push(`env:${e.env}`);
            if (e.detail) parts.push(e.detail);
            return parts.join(" | ");
          });
    
          return text(lines.join("\n"));
        },
      );
    
      server.tool(
        "detect_anomalies",
        [
          "[audit] Scan the audit history for suspicious access patterns — burst reads of the same key, off-hours access, and other heuristics.",
          "Use as a quick triage signal when investigating a single key or before letting an agent rotate credentials; prefer `health_check` for a scope-wide decay+anomaly summary, and `agent_scan` for multi-project JSON reports with optional auto-rotation.",
          "Read-only; never mutates secrets or the audit log. Returns one line per finding formatted `[type] description`, or 'No anomalies detected' when the log looks clean.",
        ].join(" "),
        {
          key: z
            .string()
            .optional()
            .describe(
              "If provided, narrow the scan to this exact key. Omit to scan across every key in the audit log.",
            ),
        },
        async (params) => {
          const toolBlock = enforceToolPolicy("detect_anomalies");
          if (toolBlock) return toolBlock;
    
          const anomalies = detectAnomalies(params.key);
          if (anomalies.length === 0) return text("No anomalies detected");
    
          const lines = anomalies.map((a) => `[${a.type}] ${a.description}`);
          return text(lines.join("\n"));
        },
      );
    
      server.tool(
        "health_check",
        [
          "[health] Run a single read-only sweep over every secret in the requested scope and report counts of healthy/stale/expired secrets plus any current audit anomalies.",
          "Use as the default 'is everything OK?' command for an agent or operator; prefer `check_project` to validate manifest compliance specifically, `detect_anomalies` for audit-only triage, and `agent_scan` for multi-project JSON output or optional auto-rotation.",
          "Read-only — never writes. Returns a multi-line text summary: header counts (Total / Healthy / Stale / Expired / No decay / Anomalies), then per-secret `EXPIRED:` / `STALE:` issue lines, then per-anomaly `[type] description` lines.",
        ].join(" "),
        {
          scope,
          projectPath,
          teamId,
          orgId,
        },
        async (params) => {
          const toolBlock = enforceToolPolicy("health_check", params.projectPath);
          if (toolBlock) return toolBlock;
    
          const entries = listSecrets(opts(params));
          const anomalies = detectAnomalies();
    
          let healthy = 0;
          let stale = 0;
          let expired = 0;
          let noDecay = 0;
          const issues: string[] = [];
    
          for (const entry of entries) {
            if (!entry.decay?.timeRemaining) {
              noDecay++;
              continue;
            }
            if (entry.decay.isExpired) {
              expired++;
              issues.push(`EXPIRED: ${entry.key}`);
            } else if (entry.decay.isStale) {
              stale++;
              issues.push(
                `STALE: ${entry.key} (${entry.decay.lifetimePercent}%, ${entry.decay.timeRemaining} left)`,
              );
            } else {
              healthy++;
            }
          }
    
          const summary = [
            `Secrets: ${entries.length} total`,
            `Healthy: ${healthy} | Stale: ${stale} | Expired: ${expired} | No decay: ${noDecay}`,
            `Anomalies: ${anomalies.length}`,
          ];
    
          if (issues.length > 0) {
            summary.push("", "Issues:", ...issues);
          }
          if (anomalies.length > 0) {
            summary.push(
              "",
              "Anomalies:",
              ...anomalies.map((a) => `[${a.type}] ${a.description}`),
            );
          }
    
          return text(summary.join("\n"));
        },
      );
    
      server.tool(
        "verify_audit_chain",
        [
          "[audit] Recompute the SHA-256 hash chain over the audit log and confirm no event has been mutated, deleted, or reordered.",
          "Use periodically as a tamper-evidence check, or whenever you suspect the audit log has been touched outside q-ring; the result is informational — this tool does not repair the chain if it is broken.",
          "Read-only. Returns JSON `{ ok, valid, brokenAt? }` where `valid` is `true` for an intact chain and `brokenAt` (when present) names the first event whose hash did not match.",
        ].join(" "),
        {},
        async () => {
          const toolBlock = enforceToolPolicy("verify_audit_chain");
          if (toolBlock) return toolBlock;
    
          const result = verifyAuditChain();
          return text(JSON.stringify(result, null, 2));
        },
      );
    
      server.tool(
        "export_audit",
        [
          "[audit] Export the audit log as a portable text artifact suitable for archiving or feeding into another SIEM/analyzer.",
          "Use for compliance exports, after-the-fact investigations, or to hand the trail to a non-MCP consumer; prefer `audit_log` for an in-conversation tail and `verify_audit_chain` to confirm integrity before exporting.",
          "Read-only. Returns the rendered text directly (no JSON wrapper). 'jsonl' is one event per line; 'json' is a single array; 'csv' is a header row plus events. Time filters are applied to the event timestamps before formatting.",
        ].join(" "),
        {
          since: z
            .string()
            .optional()
            .describe(
              "Inclusive lower bound on event timestamp, ISO 8601. Example: '2026-04-01T00:00:00Z'. Omit for no lower bound.",
            ),
          until: z
            .string()
            .optional()
            .describe(
              "Inclusive upper bound on event timestamp, ISO 8601. Omit for now/no upper bound.",
            ),
          format: z
            .enum(["jsonl", "json", "csv"])
            .optional()
            .default("jsonl")
            .describe(
              "Output format. 'jsonl' (default) is most stream-friendly; 'json' is a single array; 'csv' is spreadsheet-friendly.",
            ),
        },
        async (params) => {
          const toolBlock = enforceToolPolicy("export_audit");
          if (toolBlock) return toolBlock;
    
          const output = exportAudit({
            since: params.since,
            until: params.until,
            format: params.format,
          });
          return text(output);
        },
      );
    }
  • CLI command 'audit:export' that also calls exportAudit, providing a non-MCP command-line alternative for exporting audit events.
    program
      .command("audit:export")
      .description("Export audit events in a portable format")
      .option("--since <date>", "Start date (ISO 8601)")
      .option("--until <date>", "End date (ISO 8601)")
      .option("--format <fmt>", "Output format: jsonl, json, csv", "jsonl")
      .option("-o, --output <file>", "Write to file instead of stdout")
      .action((cmd) => {
        const output = exportAudit({
          since: cmd.since,
          until: cmd.until,
          format: cmd.format,
        });
    
        if (cmd.output) {
          writeFileSync(cmd.output, output);
          console.log(`${SYMBOLS.check} Exported to ${cmd.output}`);
        } else {
          console.log(output);
        }
      });
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden. It states 'Read-only' to indicate safety, explains the return format ('Returns the rendered text directly (no JSON wrapper)'), and details time filtering behavior, all of which go beyond what annotations would typically convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient paragraph. Every sentence adds unique value—purpose, alternatives, behavioral notes, format details, and time filtering—without redundancy or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has no output schema, the description explicitly states the return format (rendered text directly) and covers all behavioral aspects. Input parameters are fully described via schema and supplemented by description. For a tool with three simple params and no nested objects, this is complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, providing a baseline of 3. The description adds value by explaining time filter application and giving concrete examples for the 'since' parameter. It also clarifies format differences beyond the enum labels, enriching the schema's meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description begins with 'Export the audit log as a portable text artifact', clearly specifying the verb and resource. It further distinguishes from siblings by naming 'audit_log' and 'verify_audit_chain' and describing when to use alternatives.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit guidance is provided: 'prefer `audit_log` for an in-conversation tail and `verify_audit_chain` to confirm integrity before exporting'. The description also lists concrete use cases (compliance exports, investigations) for using this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/I4cTime/quantum_ring'

If you have feedback or need assistance with the MCP directory API, please join our Discord server