MCP server (inbound)
Read-only MCP server that exposes a recorded Starling event log to AI assistants over stdio. Claude Desktop, Cursor, Claude Code.
This page is the inbound MCP server - AI assistants querying your event log. For the outbound client that lets agents call external MCP servers as tools, see MCP tools.
starling-mcp exposes a recorded event log to AI assistants over the
Model Context Protocol. Once installed, your AI client (Claude
Desktop, Cursor, Claude Code) can answer natural-language questions
about your agent's runs without you copy-pasting JSON.
The server is read-only by construction: the SQLite handle is opened
with eventlog.WithReadOnly, and only read tools are registered. It
runs as a stdio subprocess of the AI client; nothing opens a network
port.
Install
go install github.com/jerkeyray/starling/cmd/starling-mcp@latestOr build from source:
go build -o ~/bin/starling-mcp ./cmd/starling-mcpThe starling CLI also bundles it as a subcommand:
starling mcp <db> is identical.
Wire into an MCP client
Claude Desktop - ~/Library/Application Support/Claude/claude_desktop_config.json
on macOS:
{
"mcpServers": {
"starling": {
"command": "starling-mcp",
"args": ["/Users/me/.hearsay/saves/playbot.db"]
}
}
}Cursor - ~/.cursor/mcp.json (same shape).
Claude Code - add via claude mcp add starling starling-mcp /path/to/runs.db.
Restart the client. The server starts when the client first connects, exits when the client disconnects, and re-reads the DB file on every tool call so new events are visible immediately.
Tools
Seven read-only tools. All return JSON; arguments are typed.
| Tool | What it does |
|---|---|
list_runs | Enumerate recorded runs newest-first. Filters: status, query (substring match on run id + status), since (RFC3339), with_tool_calls. Page with limit (default 50, max 200) + offset. |
get_run | Return the run summary plus every event with hashes and decoded payloads. Caps at 1000 events; use offset to page when truncated. |
get_event | One event by (run_id, seq), with hash, prev_hash, kind, decoded payload. |
summarize_run | Turn count, tool-call count, in/out tokens, USD cost, duration, terminal kind, final assistant text. |
validate_run | Verify the BLAKE3 hash chain. Returns {ok: true} on a clean chain, otherwise {ok: false, reason: "..."}. |
diff_runs | Align two runs by sequence and classify each row as match, diff, only-a, or only-b. Includes the first-divergence sequence number. |
search_runs | Naive substring/kind scan across a bounded run page. Requires at least one of query or kind; cap hits with limit (default 50, max 500), scanned runs with run_limit (default 200, max 1000), and inspected events with max_examined_events (default 10000, max 50000). Returns {run_id, seq, kind, summary} per hit plus scan counters. |
Sample interactions
You: What runs are in the database?
Claude: (calls
list_runs) Five runs, newest first. Three completed, one failed (max_turnsat 14:32:07), one cancelled. Total cost $0.0089.
You: Summarize the failed one.
Claude: (calls
summarize_runwith the failed run's id) Run01HZ...failed after 5 turns and 2 tool calls, $0.0021, 1247 input / 38 output tokens, ran for 8.4 seconds. The final assistant text was empty - terminal kind is RunFailed with error_typemax_turns.
You: Was there a tool that errored?
Claude: (calls
search_runswithkind=ToolCallFailed) TwoToolCallFailedevents. Both in run01HZ..., both for thefetchtool witherror_type=tooland messageupstream 503: service unavailable. Same call idc4retried twice before final failure.
You: Show me event 5 of that run.
Claude: (calls
get_eventwithseq=5) AssistantMessageCompleted at 14:32:05.882, payload says it planned to callfetchwith{url: "https://..."}, stop_reasontool_use, 421 input + 18 output tokens, hash9038a81bea67….
The model picks which tool to call. You ask a normal question.
Read-only by construction
Three layers of defence:
- Storage layer.
eventlog.NewSQLite(path, eventlog.WithReadOnly())-AppendreturnsErrReadOnly. - Tool surface. The server registers no write tools. There is
no
add_event, noprune, nomigrate. Even a malicious client can't ask for what isn't there. - Construction.
mcpsrv.Newaccepts aneventlog.EventLog, not a path; the binary opens the read-only handle before handing it off.
Limits and gotchas
search_runsis naive. It walks every event of every run matching the run page; cost isO(runs × events). It requires eitherqueryorkind, pages runs withrun_limit/run_offset, caps hits withlimit, and stops oncemax_examined_eventsis reached. Responses includeruns_examined,total_matching_runs,runs_capped, andscan_capped. Real indexes are a future pass.get_runevent cap. A single run can have thousands of events with long assistant text. Default cap is 200 events per call; the response includestruncated: trueand atotal_eventscount so the model knows to page. Built-in log backends page events in storage; custom logs fall back toReadplus in-memory slicing.- No streaming. MCP tools are request/response. To watch a run unfold turn-by-turn, use the inspector - its SSE timeline is the right surface for that.
- One DB per server. Configure multiple
mcpServersentries in your client to query multiple databases.