Findings
The audit log was already a product feature, unread
Every tool execution has written an audit_log row since the audit logger
landed: packages/server/src/mcp/meta-tools.ts logs at six points in one
execution — tool-not-found, not-connected, invalid-args, safeparse-error,
success, and thrown error — with integration, tool, success, error
and duration_ms, indexed on (user_id, created_at).
Nothing read it. There was no endpoint, so the portal could not show a human what their agents had actually been doing, and the only way to see a failure was to have been watching the server's stdout when it happened. Adding the read side was two queries and a route; the data had been accumulating the whole time.
Two details are worth carrying forward.
An empty table is ambiguous. AUDIT_LOG_DEST may be sqlite, stdout
or kafka. Under the latter two the table stays empty forever, which looks
identical to "you have not run any tools yet" and would have sent someone
hunting for a bug in the logger. Both endpoints return an explicit
stored: false instead, and the UI says which situation it is in.
Keyset paging has to spell out the tiebreak. Rows share a created_at
routinely — a batch of tool calls lands inside the same second — so paging on
the timestamp alone silently drops or repeats rows at a page boundary. The
predicate needs the id as a tiebreak, and it has to be written longhand as
created_at < ? OR (created_at = ? AND id < ?): the row-value form
(created_at, id) < (?, ?) is not portable across the two SQL backends this
project supports. See
postgres dialect gotchas.