Skip to content

feat(cli): add mcp command — stdio MCP server for AgentMeter spend data - #3

Merged
adamhenson merged 8 commits into
mainfrom
cursor/mcp-command-395b
Jul 24, 2026
Merged

feat(cli): add mcp command — stdio MCP server for AgentMeter spend data#3
adamhenson merged 8 commits into
mainfrom
cursor/mcp-command-395b

Conversation

@adamhenson

@adamhenson adamhenson commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds agentmeter mcp — a stdio MCP server that lets any MCP-compatible AI agent (Claude Code, Cursor, etc.) query session history and spend data without leaving their workflow.

Zero setup for local users. Add two lines of JSON to your MCP client config. Done. The server scans Claude Code and Cursor data on demand — no account, no sync, no background service required.


Architecture

Local tools — no account needed

list_recent_sessions and get_session call the Claude/Cursor scanners directly at tool-call time. Results are cached in memory for 60 seconds per MCP server process (one per client connection), so the first call does the scan and subsequent calls within the window are instant.

Sessions from the last 12 months are returned, including full token counts (input/output/cache), duration, model, engine, and repo. costCents is null without an account — it gets populated automatically when sessions are submitted to the AgentMeter API and written back to sync-state.

API tools — require account

get_my_spend and get_team_spend call the AgentMeter API directly. If no key is configured they return a structured { error, hint } with a sign-up link — the server keeps running, tools degrade gracefully.

get_session — hybrid

Searches the live scan first (no API needed). Falls back to GET /api/runs/<id> only if the session isn't found locally and a key is configured.

sync-state.json — role clarified

Stays as an API deduplication cache only. The MCP tools no longer depend on it for local session data. costCents enrichment reads from it opportunistically when present.


Files changed

src/commands/mcp.ts

  • getLocalSessions() — runs ClaudeScanner + CursorScanner with in-memory TTL cache
  • Handler functions exported for testing: handleListRecentSessions, handleGetSession, handleGetMySpend, handleGetTeamSpend, fetchJson
  • Removed startup config gate — server starts for everyone

src/schemas/sync-state.ts

  • Added optional tokens field — persists token counts when syncing to API

src/commands/sync.ts

  • Writes tokens to sync-state after each successful API submission

src/services/sync-state.ts

  • trimSyncState() — count cap at 5,000 completed entries (no age cutoff)

__tests__/commands/mcp.test.ts (22 tests)

  • Scanner mocks, in-memory cache hit, 12-month filter, costCents enrichment, API fallback paths

README.md

  • Rewritten: MCP config first (2 lines of JSON), no prerequisites
  • AgentMeter.app section frames the upsell: cost data, dashboards, team visibility
  • CLI reference moved to bottom as optional
Open in Web Open in Cursor 

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The overall design is solid — fetchJson is a clean, defensive helper with proper Zod validation on every API response, error paths return human-readable messages instead of throwing, and stdout is never touched directly (MCP protocol owns it). Good use of encodeURIComponent on sessionId in the URL.

Two issues need to be addressed before merge:

1. readSyncState() not wrapped in try/catch (both tool handlers)

list_recent_sessions (line 162) and get_session (line ~244) call readSyncState() bare. If the sync-state file is absent, locked, or malformed, the throw propagates out of the MCP tool handler. Project convention: "the CLI should never crash with an unhandled exception — catch, log, continue." Wrap in try/catch and return textResult({ error: ... }).

2. list_recent_sessions description says "Works without an API key" — but it doesn't

startMcpServer() exits before registering any tools if getEffectiveConfig() returns nothing. The description is factually incorrect. Either relax the startup check (check only when a tool that needs the API is called) or remove that sentence. Details in the inline comment.

Missing tests

No tests were added for the new command. At minimum, test:

  • fetchJson with mocked fetch (success, network error, non-2xx, bad JSON, Zod mismatch)
  • list_recent_sessions session-sorting and limit slicing logic against a fixture sync-state
  • get_session local-first / API-fallback branching

The __tests__/ directory mirrors src/ — add __tests__/commands/mcp.test.ts.

Generated by Agent: Code Review for issue #3 · ● 151.2K

Comment thread packages/cli/src/commands/mcp.ts Outdated
Comment thread packages/cli/src/commands/mcp.ts Outdated
…I key

- list_recent_sessions and get_session local path require no API key
- API-requiring tools (get_my_spend, get_session fallback, get_team_spend)
  return a structured error + sign-up hint instead of exiting
- Wrap readSyncState() in try/catch in all callers
- Export fetchJson and handler functions for unit testing
… setup

Also adds __tests__/commands/mcp.test.ts (16 tests covering fetchJson,
list_recent_sessions, get_session, get_my_spend, get_team_spend)
- Add tokens field to SyncedSessionSchema (optional, backwards-compat)
- Write session.tokens to sync-state during submitAll
- Include tokens in list_recent_sessions and get_session local output
- Update README: token questions are answerable locally without an account
- Add trimSyncState() — drops completed sessions older than 90 days and
  caps completed entries at 5,000 (most recent kept); running sessions are
  always preserved for vanished-session detection
- writeSyncState calls trimSyncState before persisting
- Trimmed sessions re-submitted on next scan receive 409 (handled gracefully)
- 3 new tests covering age cutoff, running-session preservation, count cap
Age cutoff caused Claude Code users to re-submit their full session history
on the next sync after a trim, since the scanner reads all JSONL files on
every run. A 5,000-entry count cap is sufficient: 2.5 MB max, ~8 months of
heavy usage before it activates, and re-submissions are safe (API is
idempotent for known session IDs).

Also correct the 409 claim in comments — internal docs confirm the API
returns 200/201 (update) for duplicate session IDs, not 409.
- list_recent_sessions and get_session now scan Claude/Cursor data live at
  call time instead of reading sync-state.json
- 60-second in-memory cache (per MCP server process) keeps subsequent calls
  within the TTL window instant
- list_recent_sessions filters to last 12 months; get_session searches full
  scan with no time limit
- costCents is enriched from sync-state when available (AgentMeter account)
  but the tools work fully without it
- No CLI setup or background service required for local-only MCP use —
  adding the MCP config JSON is the only step
- 22 tests: scanner mocks, cache hit, 12-month filter, costCents merge,
  API fallback paths
@adamhenson
adamhenson marked this pull request as ready for review July 24, 2026 20:23
@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@adamhenson
adamhenson merged commit 0d15075 into main Jul 24, 2026
7 checks passed
@adamhenson
adamhenson deleted the cursor/mcp-command-395b branch July 24, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants