@keepsnappy/mcp
v0.1.0-alpha.18
Published
KeepSnappy MCP server (stdio, read-only): performance findings, compare, window history and commits for coding agents.
Downloads
780
Readme
@keepsnappy/mcp
KeepSnappy's MCP server for coding agents (Claude Code and others): it answers "what got slower, where, how sure, why, and
what do I do" from the performance findings KeepSnappy measured on real phones. It never starts a measurement. It reads, with two
exceptions: submit_hypotheses stores open predictions about a finding (no verdict reads them), and recompute_with_new_rules starts the
server-side recompute with a run key (plan 48 §4, plan 82, plan 86).
It gives the same answers as the keepsnappy CLI (keepsnappy findings | finding | compare | history | commits | layers):
both use this package's API client and answer layout, so a person and an agent read the same words.
Which question needs which tool, and how to read the answers: docs/agents.md ("KeepSnappy for coding agents").
Set it up in an app repo (TapIn)
- Ask the KeepSnappy owner for a project key with read scope (
bun run create-key -- <org> <app> readinapps/api; it refuses an org or app that does not exist). Setup → Keys on the dashboard lists read keys and revokes them. A CI key (runscope) works too; an SDK key (ingest) does not. - Put the key in your shell environment, not in the repo:
export KEEPSNAPPY_KEY=pl_rk_…(for example in~/.zshenv). - Add
.mcp.jsonat the repo root:
{
"mcpServers": {
"keepsnappy": {
"command": "npx",
"args": ["-y", "@keepsnappy/[email protected]"],
"env": {
"KEEPSNAPPY_KEY": "${KEEPSNAPPY_KEY}",
"KEEPSNAPPY_API": "https://keepsnappy-api-922918702810.europe-west1.run.app"
}
}
}
}Claude Code expands ${KEEPSNAPPY_KEY} from your environment. KEEPSNAPPY_API is optional (the hosted API is the default).
Use the first release after 0.1.0-alpha.18 (all seventeen tools; alpha.18 has fifteen, without get_investigation and submit_hypotheses; alpha.17 has thirteen, without rules_status and recompute_with_new_rules; alpha.16 has eleven, without list_problems and main_changes; alpha.15 ten, alpha.13 six). From a KeepSnappy checkout instead of npm: "command": "bun", "args": ["<keepsnappy>/packages/mcp/src/bin.ts"].
Tools
| Tool | Answers | API |
|---|---|---|
| pr_status | Is this branch ok, in one call: blocked, nothing blocks with results to look at, or ok; per journey × phone the deciding word, whether it blocks, the worst present findings (ids for get_finding) and the dashboard link. A branch never measured, whose newest run set was not judged, or that could not be compared yet is not ok: the answer says which, with its reason. | GET /v1/branches/status |
| list_problems | The single problems of a branch's own runs: "Over budget" and "Far over budget" (from twice the budget), each with the value and the budget, the runs it was seen in, the heaviest components and the tag ("New in this PR", "Also on main", or "Not compared with main" when the comparison could not tell). Not a regression and never blocking; pr_status counts them. Filters: branch or main, journey, phone, limit (≤ 200), offset. | GET /v1/problems |
| list_findings | Whether and when the branch was measured (measured: run sets, the newest set of each journey × phone with its commit, word and runs), then what got worse (Worse / Warning) or better, worst first. Filters: branch or main, journey, phone, state (present default, not-judged, resolved, all), limit (≤ 200), offset (the answer gives next_offset). | GET /v1/findings |
| get_finding | One finding by id: what changed, where (journey, phone, branch, commit title and author, PR), how sure (runs per side, session spread, measured differently / not blocking, setup notes), why (components that rendered longer with their cause, counts that moved), what to do (a prompt for a coding agent), lifecycle (first and last seen; present, resolved, or not judged when the newest run set did not judge the measure; seen in N PRs). The prompt ends with the finding's id and page. | GET /v1/findings/:id |
| compare_runs | Two runs side by side, window by window, and what differs in how they were measured. No statistics. | GET /v1/compare?a=&b= |
| window_history | One window (screen, tap, scroll) of a journey on main over time, run by run, with commit titles, plus the findings on it. | GET /v1/history |
| session_places | Every place (screen, tap, scroll) of one session (a run set of one journey on one phone) from its stored statistics: per measure the median, MAD, p10/p90 and range, and the components with the most own time. Not a verdict, not a comparison. Pick with run_set, or journey (+ phone, branch; default: the app's baseline branch (main)) for the newest closed run set with a valid run; rank by measure (default render_ms) and sort (median, p90, mad), narrow with window, page with limit / offset (the answer gives next_offset), top_components (default 5). A session with no stored statistics says not_computed, one with another version's stale: never zero. | GET /v1/sessions/places |
| commits_between | The commits KeepSnappy measured between two points (run set ids or commit hashes), one entry per commit with its sessions. | GET /v1/commits |
| main_changes | Main's confirmed level shifts in one lane (journey, phone), newest first: place, measure, before → after, the commit range from the last main session before the change to its first session, and the PRs merged in it, oldest first. The PRs are what the agent stored with the main run set at ingest (git log --first-parent); null ("PRs not known") when no stored list reaches the session before. Main's own history, never a PR verdict. limit ≤ 200. CLI: keepsnappy main --journey KEY --phone MODEL --changes. | GET /v1/main/changes |
| get_investigation | The investigation package of a finding, from stored results only: the lane's verdict as stored, the worse places with main and PR values, the components to check, the problems, the measured commits between main and the PR, a main finding's change with its PRs. Lists say their total when cut; digest names what was read. form: "short" keeps the counts, the verdict and the digest. The structured result is the whole package. The skill docs/skills/keepsnappy-investigate/SKILL.md uses it. | GET /v1/findings/:id/investigation |
| submit_hypotheses | Stores up to ten hypotheses about a finding: predictions with a measurable expected change, never a verdict. The submission (with the package's package_digest) is checked against the one hypothesis schema (@keepsnappy/dashboard-contract/hypotheses) before anything is sent. A read or run key; the same submission_id stores nothing twice, and the same id with other content is a conflict. The prose (claim, evidence, risk notes, moved cost, prior conflict, magnitude hint) may not contain a word of the verdict (Worse, Slightly worse, Better, No change); Can’t tell is allowed. | POST /v1/findings/:id/hypotheses |
| explain_layers | Which of KeepSnappy's layers answers which question, and each layer's certificate (none issued yet). | GET /v1/layers |
Besides measures, a run in which the phone heated up (iOS thermal state nominal or fair at the start, serious or worse at
its peak, or critical from a lower state) is a finding of its own (kind: "heat", layer 3), invalid runs included
(REQ-MET-20). The comparison warning "Phone heats up" is a measure finding whose values are shares of cool-started runs.
An empty list is a clean result only when measured.newest exists and was judged: the answer's first line says "No
KeepSnappy measurement on branch X" or "the newest run set was not judged" otherwise. Query parameters the API does not know
are dropped, not refused (there is no ?commit= filter).
Every tool returns the text the CLI prints and the API's JSON as structured content. The answer tools (pr_status, list_findings, list_problems, get_finding, session_places, main_sessions, main_vs_previous,
place_trend, window_history) take form: full (the default, the whole text) or short (the CLI's --short). Short is
the full text with lines left out, in the same order, and ends with one line of its own that says what it left out (with
counts); nothing left out, no line. Each tool's description says what its short form leaves out. The JSON is the same in
both forms. Every finding names its layer
(4 = PR vs main, 5 = main over time), its certificate (null until plan 51 certifies the layer) and its limits.
Words
The one word table of packages/present/src/states.ts (REQ-VER-24): Worse / Slightly worse / Better / No change /
Can’t tell, and "not blocking" as a badge on a Worse measured differently from main ("Worse (not blocking)"). The evidence ranks likely causes; it does not prove them.
Develop
bun install
bunx vitest run # the server against a fake API serving findings built by the API's own finding module
bunx tsc --noEmit
KEEPSNAPPY_KEY=… bun src/bin.ts # stdio