@perf-labs/mcp
v0.1.0-alpha.11
Published
KeepSnappy MCP server (stdio, read-only): performance findings, compare, window history and commits for coding agents.
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. Read-only: it never starts a measurement, writes or recomputes anything (plan 48 §4).
It gives the same answers as the perflab CLI (perflab 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.
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 PERFLAB_KEY=pl_rk_…(for example in~/.zshenv). - Add
.mcp.jsonat the repo root:
{
"mcpServers": {
"perflab": {
"command": "npx",
"args": ["-y", "@keepsnappy/mcp"],
"env": {
"PERFLAB_KEY": "${PERFLAB_KEY}",
"PERFLAB_API": "https://perflab-api-864539138788.europe-west1.run.app"
}
}
}
}Claude Code expands ${PERFLAB_KEY} from your environment. PERFLAB_API is optional (the hosted API is the default).
From a KeepSnappy checkout instead of npm: "command": "bun", "args": ["<perflab>/packages/mcp/src/bin.ts"].
Tools
| Tool | Answers | API |
|---|---|---|
| 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 |
| 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 |
| 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. 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/verdicts/src/headline.ts (REQ-VER-24): Worse / Warning / Better / No change /
Can't compare / Not enough data yet, 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
PERFLAB_KEY=… bun src/bin.ts # stdio