agent-activity
by patrickdaj
README.md
# agent-activity
A read-only MCP server that exposes your local coding-agent session logs
(the JSONL transcripts written by Claude Code at
`~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`) as three tools, so
you — or an agent — can introspect recent work, debug tool failures, and
track token/estimated cost without hand-parsing log files.
## Guarantees
- **Read-only.** The server only ever reads log files; it never writes to,
deletes, or otherwise mutates anything under the log root.
- **Local.** All data comes from the filesystem on the machine the server
runs on.
- **No network.** The server never makes an outbound network call — not
for logs, not for pricing data, not for telemetry. Pricing is a local,
configurable table (see [Cost is an estimate](#cost-is-an-estimate-not-a-bill)).
- **stdio transport.** Standard MCP over stdio, via the official `mcp`
Python SDK (`FastMCP`).
## Tools
### `list_recent_sessions(limit=20, repo=None)`
Recent sessions, most-recently-active first.
- `limit` — max sessions to return (default 20, max 200).
- `repo` — optional substring filter against the session's repo/cwd.
Returns, per session: `session_id`, `repo`, `cwd`, `git_branch`, `start`/
`end` timestamps, `message_count`, `schema_status` (`"verified"` or
`"unverified"` — whether the log's shape matched what this server
expects; degrades gracefully rather than failing on drift), and a
`usage` block (`total_tokens`, `total_cost`, `cost_status`, and a
`main`/`sidechain` breakdown — see [Sidechains](#sidechains-and-subagents)).
### `tail_agent_log(session, limit=100, include_current=False, cwd=None)`
The tail of one session's transcript, oldest-first.
- `session` — a full session UUID, a unique UUID prefix, or `"latest"`
(see [Session resolution](#how-session-resolution-works)).
- `limit` — max entries to return (default 100, max 1000).
- `include_current` — when resolving `"latest"`, whether to allow it to
resolve to the session currently calling this tool (default `False`).
- `cwd` — optional exact working-directory path; scopes `"latest"`
resolution and current-session detection to sessions from that
directory.
Returns `session_id`, `repo`, `schema_status` (as in
`list_recent_sessions`), and a list of `entries`, each with `type`,
`timestamp`, any `tool_calls` (`tool_use_id`, `tool_name`, and `outcome`
— `ok`/`error`/`pending`, joined against the whole session so a call's
outcome is known even when its result entry falls outside the returned
tail) and `tool_results` (`tool_use_id`, `is_error`), and — for
assistant entries that carry usage — a `usage` block with `model`,
per-entry `tokens`, and estimated `cost`.
### `summarize_tool_calls(session, cwd=None)`
Per-tool aggregate for a session: invocation counts, error counts, which
calls failed, and total usage.
- `session` — a full session UUID, a unique UUID prefix, or `"latest"`
(resolved *including* the current session — summarizing your own live
session is a valid use case here).
- `cwd` — optional exact working-directory path; scopes `"latest"`
resolution and current-session detection to sessions from that
directory.
Returns `session_id`, `repo`, `schema_status` (as in
`list_recent_sessions`), `is_live_session`, `per_tool` (per tool
name: `ok`/`error`/`pending`/`in_progress` counts), `totals` (the same
breakdown across all tools, plus `rejected`), `by_source` (`main` vs
`sidechain` totals), `failing_calls` (each labeled `error` or
`rejected`), and a session-level `usage` block (tokens + estimated cost).
All three tools return a structured `{"error": ...}` dict (never raise)
when a `session` argument is ambiguous (`"ambiguous_session"`, with
candidates) or matches nothing (`"session_not_found"`).
## Cost is an estimate, not a bill
Session logs record token counts (`message.usage`) but **not** dollar
cost. Cost is computed by multiplying token counts by a per-model rate
table (`pricing.py`) — order-of-magnitude, hand-maintained figures that
will drift as providers change pricing. Treat any `cost` field as a
*ballpark to sanity-check spend*, not an authoritative bill. Unknown
models degrade to `cost_status: "unknown"` (tokens are still reported)
rather than raising.
Override the table with your own by pointing `AGENT_ACTIVITY_PRICING_FILE`
at a JSON file shaped like:
```json
{
"claude-sonnet-5": {
"input": 3.00,
"output": 15.00,
"cache_write": 3.75,
"cache_read": 0.30
}
}
```
Rates are USD per 1,000,000 tokens. A model listed in the override file
fully replaces that model's default entry; models you don't mention keep
their built-in defaults.
## Sidechains and subagents
Subagent ("sidechain") activity lives in separate sibling files
(`<session-dir>/subagents/agent-*.jsonl`), not inline in the main session
file. This server associates those files with their parent session and
includes their tool calls and token usage in `summarize_tool_calls` and
`list_recent_sessions`, but keeps every count and token total tagged
`main` vs `sidechain` so totals stay decomposable rather than blended.
Sidechain usage is summed **only** from the sidechain files' own
`message.usage` entries. The parent log's separate rollup fields
(`toolUseResult.totalTokens` / the `<usage>...</usage>` text tag on the
spawning `Agent` tool's result) are a second, independently-computed
aggregate of the same work — summing both would double-count, so those
fields are never added into the totals here.
## Config
| Env var | Default | Purpose |
|---|---|---|
| `AGENT_ACTIVITY_LOG_ROOT` | `~/.claude/projects/` | Root directory to scan for `*/*.jsonl` session logs. |
| `AGENT_ACTIVITY_PRICING_FILE` | (none — built-in table) | Path to a JSON pricing-table override (see above). |
| `AGENT_ACTIVITY_CALLER_SESSION_ID` | (none — falls back to a heuristic) | The calling session's own id, if your MCP client can supply it. Used to reliably identify "the current session" for `"latest"` resolution; without it, the server falls back to a newest-mtime heuristic (documented limitation: can misfire with concurrent sessions). |
## Install
This project uses [uv](https://docs.astral.sh/uv/). With uv installed
(`curl -LsSf https://astral.sh/uv/install.sh | sh`), from the repo root:
```bash
uv sync
```
This creates a locked virtual environment (`.venv/`) from the committed
`uv.lock`, pins the interpreter via `.python-version`, and installs the
`agent-activity` package plus its one runtime dependency (`mcp`).
> **Fallback (no uv):** `pip install -e .` still works and installs the
> `agent-activity` console script and its `mcp` dependency — it just isn't
> locked or interpreter-pinned.
## Run
Any of the following start the same stdio server (via uv, no manual
activation needed):
```bash
uv run agent-activity
uv run python -m agent_activity
uv run python -m agent_activity.server
```
If you've activated the environment (or installed with `pip install -e .`),
you can drop the `uv run` prefix and invoke `agent-activity` /
`python -m agent_activity` directly.
The process speaks MCP over stdio (stdin/stdout) — it's meant to be
launched by an MCP client, not run interactively.
### Register with an MCP client
An example client registration is checked in at [`.mcp.json`](.mcp.json):
```json
{
"mcpServers": {
"agent-activity": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/agent-activity-mcp-server", "agent-activity"],
"env": {
"AGENT_ACTIVITY_LOG_ROOT": "~/.claude/projects"
}
}
}
}
```
`uv run` resolves the project (and its locked environment) from a working
directory, so set `--directory` to the absolute path of this repo — MCP
clients launch the command from an arbitrary cwd. This form doesn't require
the console script to be on `PATH`.
Alternatively, if you've installed the package into the environment your MCP
client uses (e.g. via `pip install -e .`), point `command` directly at the
`agent-activity` console script (with `args: []`), or use `python` with
`args: ["-m", "agent_activity"]` to invoke the module.
### Example tool calls
Once registered, a client can call e.g.:
```
list_recent_sessions(limit=5)
tail_agent_log(session="latest", limit=50)
summarize_tool_calls(session="latest")
```
or target a specific session by its full UUID or an unambiguous prefix:
```
tail_agent_log(session="4140dfe3", limit=200)
```
## How session resolution works
The `session` argument to `tail_agent_log` and `summarize_tool_calls`
accepts:
- a **full session UUID** — exact match against the log filename stem;
- a **unique UUID prefix** — resolves if exactly one discovered session
id starts with it; an ambiguous prefix (matches more than one) returns
a structured error listing the candidates rather than guessing;
- **`"latest"`** — the most-recently-active session (newest log file
mtime). By default this **excludes the session currently calling the
tool**, so `tail_agent_log(session="latest")` doesn't tail its own
still-being-written log; pass `include_current=True` to opt back in.
(`summarize_tool_calls` resolves `"latest"` including the current
session by default, since summarizing your own live session is a
normal thing to want.)
Identifying "the current session" is inherently a heuristic — an MCP
server isn't told its caller's session id by the protocol. If the
`AGENT_ACTIVITY_CALLER_SESSION_ID` env var is set, it's used directly
(authoritative). Otherwise the server falls back to "the session with
the newest file mtime" (optionally scoped to the caller's cwd), which
can misfire if two sessions are active at once.
## Development
`uv sync` installs the dev dependencies (the `dev` dependency group, which
includes `pytest`) alongside the runtime deps. Run the tests with:
```bash
uv sync
uv run pytest tests/ -q
```
An end-to-end stdio smoke test (spawns the real server as a subprocess,
performs the MCP handshake, and calls all three tools against real logs)
lives at `scripts/smoke_test.py`:
```bash
uv run python scripts/smoke_test.py
```
TDQS
A4.6/5.0
Scored across 3 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: listing sessions, summarizing tool calls within a session, and tailing session transcripts. There is no overlap or ambiguity.
Naming Consistency5/5
All tool names follow a consistent verb_noun snake_case pattern (list_recent_sessions, summarize_tool_calls, tail_agent_log), making them predictable and clear.
Tool Count4/5
Three tools is slightly low but appropriate for the narrow domain of inspecting agent sessions. The set covers the essential operations without superfluous tools.
Completeness4/5
The tool surface covers listing sessions, inspecting tool calls, and viewing transcripts. A minor gap is the lack of a dedicated 'get session details' tool, but the list tool returns metadata, and summary/tail provide depth.
Maintenance
ActivityMaintained
ResponsivenessSyncing