Skip to content
AI

MCP server

A local MCP server that lets your coding agent read your Squally data: which runs happened, how each test behaves across them, and why a test failed.

squally-mcp is a local MCP server. It runs on your machine over stdio, started by your coding agent, and reads your Squally data through the read API over HTTPS - the same requests a curl would make, answered with the same JSON.

Read-only by construction. The server issues HTTP GET requests and nothing else: there is no code path that writes. Every tool is annotated readOnlyHint: true, which clients may use to skip the confirmation - Claude Desktop still asks for each call in its manual mode, and Always allow on a tool removes those prompts. It cannot reach past your organization either: the key is scoped to one organization, and scoping, plan checks and secret exclusion are enforced by the API, not by this client.

The package is squally-mcp on npm, MIT-licensed, with its source on GitHub.

  • A Standard or Pro plan. The read API is part of both; on Free, every call is answered tier_too_low.
  • An organization read key. An organization admin creates it under Organization › Read keys → Create read key. It starts with sqly_ro_ and is shown once. It is not the project ingest key your Playwright reporter uses - the server refuses an ingest key and says that it is one. More in Read keys.
  • Node 22 or newer. Your agent starts the server with npx.
  • A version that matches the read API. app.squally.dev moved to read API 1.0.0-beta.18 on 23.10.2026; which version it serves now is in the API changelog. squally-mcp 0.8.0 and later work against beta.17 and beta.18; against beta.18 its test and error tools count CI and local runs unless you pass source. Versions before 0.7.0 expect a project’s stable branch, which beta.17 removed: a client that validates the tools’ structured output - the MCP SDK’s does - rejects the answers of squally-list-projects and squally-find-run, so update to the latest version.

Worth knowing before you hand a key to an assistant. Through this server an agent can read:

  • your projects and their names;
  • your runs - CI and local ones, each marked - with commit SHA and message, branch, author, pull request, timing, the run’s status and counts, and how a run was cancelled; since read API 1.0.0-beta.15 also its job key, its attempt, the run it re-ran and the link to its CI run;
  • per-test results for a run, including which shard ran what and whether an attempt ran into its time limit;
  • for a failing attempt: the error message and stack, the error snippet - verbatim source lines from your test file - and the page snapshot at the moment of failure: an ARIA snapshot of your application, whatever was on screen, truncated to 3000 characters. All three are part of the Copy-for-AI prompt, which the agent gets too;
  • per-test metrics, counted per run - a sharded run once, when all its shards have finished; CI and local runs by default, or one of them when asked: how often each test passed on the first try, passed only after a retry, or failed, and how much time its retries and failures cost - see How Squally measures tests;
  • error signatures: failures grouped across runs and branches, with how many runs each one hit and how those runs ended for its tests.

It cannot see screenshots, videos or traces - artifacts are deliberately out of scope - and it cannot see any key, secret or webhook URL.

Each client starts the server with npx -y squally-mcp and hands it your key as SQUALLY_API_KEY. Replace sqly_ro_your_key_here with your read key. If the client’s config file already has an mcpServers section, add the squally entry to it.

For one project, a .mcp.json at the project’s root - it can be committed, because it holds a reference to the key, not the key:

{
"mcpServers": {
"squally": {
"command": "npx",
"args": ["-y", "squally-mcp"],
"env": { "SQUALLY_API_KEY": "${SQUALLY_API_KEY}" }
}
}
}

Claude Code fills in ${SQUALLY_API_KEY} from the environment it starts in, so set the variable there - in your shell profile or from a secrets manager. The key then stays out of every file and out of your shell history. The same entry from the command line, in single quotes so your shell writes the reference and not the key:

claude mcp add squally --scope project \
--env 'SQUALLY_API_KEY=${SQUALLY_API_KEY}' \
-- npx -y squally-mcp

Claude Code asks once before it starts a server from a project’s .mcp.json. For all your projects instead, use --scope user: the entry then lives in ~/.claude.json, outside any repository. claude mcp list shows whether it connected; /mcp shows it inside a session.

On Windows (not WSL), Claude Code starts npx through cmd: write "command": "cmd", "args": ["/c", "npx", "-y", "squally-mcp"] in .mcp.json, or end the command above with -- cmd /c npx -y squally-mcp.

In ~/.codex/config.toml - the Codex CLI and the IDE extension read the same file:

[mcp_servers.squally]
command = "npx"
args = ["-y", "squally-mcp"]
[mcp_servers.squally.env]
SQUALLY_API_KEY = "sqly_ro_your_key_here"

Or the same from the command line:

codex mcp add squally \
--env SQUALLY_API_KEY=sqly_ro_your_key_here \
-- npx -y squally-mcp

codex mcp list shows the configured servers; /mcp in the Codex TUI shows the active ones.

In ~/.cursor/mcp.json, for all your projects:

{
"mcpServers": {
"squally": {
"type": "stdio",
"command": "npx",
"args": ["-y", "squally-mcp"],
"env": { "SQUALLY_API_KEY": "sqly_ro_your_key_here" }
}
}
}

A project’s .cursor/mcp.json works too, but it is meant to be committed - so write "SQUALLY_API_KEY": "${env:SQUALLY_API_KEY}" there instead of the key, and set the variable in the environment Cursor starts from. Save the file and restart Cursor; the server appears under Customize → MCPs, where it can be switched on and off. The Cursor CLI reads the same files.

In Claude Desktop, open Settings → Developer and click Edit Config to find claude_desktop_config.json - on macOS in ~/Library/Application Support/Claude/, on Windows in %APPDATA%\Claude\. Quit Claude Desktop completely, including from the system tray, before you edit it - a running app overwrites your edit. Then add:

{
"mcpServers": {
"squally": {
"command": "npx",
"args": ["-y", "squally-mcp"],
"env": { "SQUALLY_API_KEY": "sqly_ro_your_key_here" }
}
}
}

Then start Claude Desktop again - it loads the configuration when it starts.

On Windows, npx can fail to start when npm is not installed globally. If %APPDATA%\npm does not exist, run npm install -g npm once.

As of squally-mcp 0.8.4, read from the package when these docs were built. Each tool is one call on the read API; the path links to its reference.

ToolWhat it answersCost
squally-list-projectsGET /projects
Which projects exist. Start here — every other tool needs a projectId from it.cheap
Which runs happened — latest, or by branch, commit SHA, status or source. Counters only.cheap
One run with its per-test rows, across all shards. Which test is red.cheap
Every attempt of one test in one run: error, stack, full-context prompt.cheap
One test's stability, flaky rate and failure rate over a period.cheap
Every test with a run in the period, ranked by stability, flaky rate or failures.cheap
Error signatures in a period: what keeps failing, grouped, and how its runs ended.cheap
What the server tells your agent first
Start with squally-list-projects; every other tool needs a projectId from it. For one test, use squally-get-test-metrics, not squally-list-tests. Test metrics count the finished runs of the period, CI and local - a sharded run counts once - without runs that were a mass failure as a whole; pass source=ci or source=local to count only one of them. A run still in progress counts once all its shards have finished, or once the run timeout has passed, with the results its shards reported - also when a shard was stopped by the monthly result limit. stability = runs that passed on the first try / runs; flakyRate = runs that passed only after a retry / runs; failureRate = failed runs / runs. There is no verdict: the tools return numbers, and you judge them. If a test name is ambiguous, repeat with filePath from the error. A run's status is passed, failed, cancelled, incomplete or running; a run that is not settled can still change. Each job of a CI run that sets a job key is its own run (jobKey). squally-get-run reports each test's final attempt: one that passed only after a retry is passed with attempts above 1, and squally-debug-failure has the errors of its failed attempts. Errors carry a code and an action; follow the action.

squally-list-tests, squally-get-test-metrics and squally-list-errors take a source argument (since 0.4.0):

  • all - both, the default since squally-mcp 0.8.0 (read API 1.0.0-beta.18); before, the default was ci;
  • ci - CI runs only: send it to read your CI on its own;
  • local - local runs, runs no shard reported a CI provider for.

Every answer names the runs it counted in population. The test tools leave out runs that were a mass failure as a whole; squally-list-errors counts them. A run still in progress counts once all its shards have finished - or, once the run timeout has passed, with the results its shards reported, also when a shard hit the monthly result limit (read API 1.0.0-beta.10 and beta.11). The values: Conventions.

squally-find-run lists runs of every source by default (since 0.5.0): it counts nothing, it says what ran. Its source argument narrows it to ci or local. Every run it returns, and the run of squally-get-run, says:

  • status - the run page’s word: passed, failed, cancelled, incomplete or running. The status filter of squally-find-run takes all five; a cancelled run matches cancelled only. See Run summary.
  • local - true when no shard of the run reported a CI provider.
  • the counts - every test once, however many shards reported it, by its final attempt across all of them; the tests a cancel cut off count nowhere.

The cancelled boolean of 0.4.0 is gone: it repeated status === "cancelled". cancellation says where a cancel came from and when.

Since read API 1.0.0-beta.15 the same runs carry five more fields, which squally-mcp 0.6.0’s tool descriptions and output schemas name:

  • settled - true when the run’s status and counts are final and it counts in the dashboard’s figures. An incomplete run that is not settled still waits for a shard within the run timeout and can still change - see Incomplete: waiting or settled.
  • jobKey - the job key the reporter sent (SQUALLY_CI_JOB_KEY, squally-reporter 0.11.0), or null. Each job with a key is its own run.
  • attempt - which attempt of its CI run this is, 2 after GitHub’s Re-run failed jobs; null when it is not known.
  • rerunOf - { id, runNumber } of the run this one re-ran, or null.
  • ciRunUrl - the CI run’s page, the run page’s View in CI link, or null.

A tool’s answer is the read API’s JSON, unchanged. The answers below are the API’s own examples, with invented data.

The three least stable tests of the last 30 days - the default period and sort:

{ "projectId": "3f7c2a91-5b8e-4d0a-9c61-2e4b7f0d8a15", "perPage": 3 }

The first of the three rows in the answer:

{
"testName": "chromium > checkout.spec.ts > Checkout > applies a coupon code",
"filePath": "tests/checkout.spec.ts",
"browser": "chromium",
"runs": 8,
"stableRuns": 4,
"flakyRuns": 4,
"failedRuns": 0,
"stability": 0.5,
"flakyRate": 0.5,
"failureRate": 0,
"timeLostMs": 7361,
"recentResults": [
{
"runId": "5c8e2a17-9d4b-4f60-b3a1-7e2d9c0f4b58",
"runNumber": 1261,
"startedAt": "2026-09-12T07:30:41.000Z",
"branch": "main",
"result": "stable"
},
{
"runId": "c2e7a940-5d1b-4f8e-9a36-0b4d7e1c5f92",
"runNumber": 1264,
"startedAt": "2026-09-14T15:12:09.000Z",
"branch": "feature/saved-carts",
"result": "flaky"
},
{
"runId": "a3f19c62-0e7d-4b85-9c24-6d1e8b3f7a09",
"runNumber": 1266,
"startedAt": "2026-09-15T07:31:03.000Z",
"branch": "main",
"result": "flaky"
},
{
"runId": "d07b4e95-6a2c-4e13-8f5d-2b9c1a7e6f30",
"runNumber": 1270,
"startedAt": "2026-09-17T07:29:58.000Z",
"branch": "main",
"result": "flaky"
},
{
"runId": "6b1f8d23-9e4a-4c07-b5d2-3a8e0f6c1d74",
"runNumber": 1273,
"startedAt": "2026-09-18T11:47:35.000Z",
"branch": "feature/saved-carts",
"result": "stable"
},
{
"runId": "8f6a3d21-4c9e-4a7b-b052-e1d7c3a9f864",
"runNumber": 1277,
"startedAt": "2026-09-20T07:32:16.000Z",
"branch": "main",
"result": "stable"
},
{
"runId": "7d2f4b19-3c8e-4a61-9e07-5b1d8c2a6f34",
"runNumber": 1283,
"startedAt": "2026-09-23T16:05:40.000Z",
"branch": "feature/saved-carts",
"result": "stable"
},
{
"runId": "b1e0c6d2-7a43-4f19-8d5e-0c3a9e6f2b71",
"runNumber": 1284,
"startedAt": "2026-09-24T07:31:12.000Z",
"branch": "main",
"result": "flaky"
}
],
"branches": [
"feature/saved-carts",
"main"
],
"topBranch": {
"branch": "main",
"kind": "retries",
"count": 3
}
}

Around the rows, the answer names the period, the branch and browser filters, the sort and the page. List tests describes every field.

One test, by the name Squally shows for it:

{
"projectId": "3f7c2a91-5b8e-4d0a-9c61-2e4b7f0d8a15",
"testName": "chromium > checkout.spec.ts > Checkout > applies a coupon code"
}

The answer - the numbers of that test’s row, beside the period they cover:

{
"project": {
"id": "3f7c2a91-5b8e-4d0a-9c61-2e4b7f0d8a15",
"name": "storefront-web"
},
"population": "ci_groups_finished_not_mass_failure",
"days": 30,
"since": "2026-08-25T08:00:00.000Z",
"until": "2026-09-24T08:00:00.000Z",
"branch": null,
"testName": "chromium > checkout.spec.ts > Checkout > applies a coupon code",
"filePath": "tests/checkout.spec.ts",
"browser": "chromium",
"runs": 8,
"stableRuns": 4,
"flakyRuns": 4,
"failedRuns": 0,
"stability": 0.5,
"flakyRate": 0.5,
"failureRate": 0,
"timeLostMs": 7361,
"recentResults": [
{
"runId": "5c8e2a17-9d4b-4f60-b3a1-7e2d9c0f4b58",
"runNumber": 1261,
"startedAt": "2026-09-12T07:30:41.000Z",
"branch": "main",
"result": "stable"
},
{
"runId": "c2e7a940-5d1b-4f8e-9a36-0b4d7e1c5f92",
"runNumber": 1264,
"startedAt": "2026-09-14T15:12:09.000Z",
"branch": "feature/saved-carts",
"result": "flaky"
},
{
"runId": "a3f19c62-0e7d-4b85-9c24-6d1e8b3f7a09",
"runNumber": 1266,
"startedAt": "2026-09-15T07:31:03.000Z",
"branch": "main",
"result": "flaky"
},
{
"runId": "d07b4e95-6a2c-4e13-8f5d-2b9c1a7e6f30",
"runNumber": 1270,
"startedAt": "2026-09-17T07:29:58.000Z",
"branch": "main",
"result": "flaky"
},
{
"runId": "6b1f8d23-9e4a-4c07-b5d2-3a8e0f6c1d74",
"runNumber": 1273,
"startedAt": "2026-09-18T11:47:35.000Z",
"branch": "feature/saved-carts",
"result": "stable"
},
{
"runId": "8f6a3d21-4c9e-4a7b-b052-e1d7c3a9f864",
"runNumber": 1277,
"startedAt": "2026-09-20T07:32:16.000Z",
"branch": "main",
"result": "stable"
},
{
"runId": "7d2f4b19-3c8e-4a61-9e07-5b1d8c2a6f34",
"runNumber": 1283,
"startedAt": "2026-09-23T16:05:40.000Z",
"branch": "feature/saved-carts",
"result": "stable"
},
{
"runId": "b1e0c6d2-7a43-4f19-8d5e-0c3a9e6f2b71",
"runNumber": 1284,
"startedAt": "2026-09-24T07:31:12.000Z",
"branch": "main",
"result": "flaky"
}
],
"branches": [
"feature/saved-carts",
"main"
],
"topBranch": {
"branch": "main",
"kind": "retries",
"count": 3
}
}

A test with results but no counted run of the source in the period answers runs: 0 and null rates - an answer, not an error. When the name exists in several spec files, the tool answers with the filePath values to repeat the call with. What each number means: How Squally measures tests.

The errors of the last 30 days, in CI runs and local runs together:

{ "projectId": "3f7c2a91-5b8e-4d0a-9c61-2e4b7f0d8a15", "source": "all" }

One error from the answer:

{
"fingerprint": "9c1e5a7f3b2d8e4c6a0f1b9d7e3c5a2f8b4d6e0c1a9f7b3e5d2c8a4f6e0b1d3c",
"title": "toHaveText · getByTestId('order-total')",
"message": "Error: expect(locator).toHaveText(expected) failed Locator: getByTestId('order-total') Expected: \"€ 45.00\" Received: \"€ 50.00\" Timeout: 5000ms",
"category": "Assertion Failures",
"filePath": "tests/checkout.spec.ts",
"fileCount": 1,
"testCount": 1,
"testNames": [
"chromium > checkout.spec.ts > Checkout > applies a coupon code"
],
"occurrenceCount": 9,
"runCount": 7,
"failedCount": 5,
"flakyCount": 2,
"failedRunCount": 5,
"flakyRunCount": 2,
"massFailureRunCount": 0,
"firstSeen": "2026-09-02T11:20:44.000Z",
"lastSeen": "2026-09-24T07:34:05.000Z"
}
  • runCount - the runs the error appeared in, a sharded run once (since 0.4.0; it counted every shard).
  • failedCount and flakyCount - of the runs and tests the error hit, how many ended failed and how many passed after a retry, by the test’s final attempt across the run’s shards.
  • massFailureRunCount - how many of those runs were a mass failure.

Around the errors, the answer gives population, the period, and unfinishedRuns - the runs that have not finished and are in no figure. isNew is gone since 0.4.0: compare firstSeen with window.from instead. List errors describes every field.

The server stops at start: SQUALLY_API_KEY is not set. Before anything else, the server checks for the key and, without one, exits with a single line naming the variable. Your client shows that line in the server’s log - Claude Desktop in mcp-server-squally.log (see Claude Desktop below for the folder), Cursor in the Output panel under MCP Logs. Add the key to the client’s config (Setup) and restart the server.

It refuses the key as an ingest key. A key that starts with sqly_ but not sqly_ro_ is a project ingest key - the one your Playwright reporter uses to upload results. The server refuses it at start, before any request, with a message that SQUALLY_API_KEY looks like a project ingest key. Create an organization read key under Organization › Read keys.

fetch failed. The request never reached the API. Since 0.1.3 the tool error names the cause - for example UNABLE_TO_VERIFY_LEAF_SIGNATURE, a certificate that could not be verified, or ENOTFOUND, a host name that did not resolve - and the MCP log gets one line per failed call with the same cause. The log’s first lines say which Node runs the server and whether it trusts the operating system’s certificate store (Certificates).

tier_too_low. The organization is on Free, and the read API is part of Standard and Pro. The agent gets the API’s sentence as a tool error and passes it on, rather than reporting Squally as unreachable. Keys are not revoked on a downgrade; they work again after an upgrade.

Rate limited. Over the limit, the tool answers: wait 60 seconds, then retry. The API’s 429 says the same with Retry-After: 60 and the error rate_limited; a 429 from the edge firewall, without either, means the same wait. The numbers are on Rate limit.

Answers are rejected after an API update. When the client says a tool’s structured output does not match its schema - for example squally-find-run after the API moved to 1.0.0-beta.14 - the package is older than the API it talks to. Update it: npx -y squally-mcp@latest, or npm install -g squally-mcp for a global install. See Requirements.

The agent calls squally-list-flaky-tests or squally-get-test-status. Both were removed in 0.2.0, together with the API endpoints they read; squally-list-tests and squally-get-test-metrics take their places. An agent that still offers the old two runs an older copy of the package - a global install keeps its version until you run npm install -g squally-mcp again.

A note that the key expires. From 7 days before your key expires, every successful tool result carries one extra line with the date. Keys are not renewed: create a new key, put it in the client’s config, and revoke the old one. After the date, calls are answered key_expired.

Every other error reaches the agent as the API’s sentence, its code and what to do about it. All codes: the error table on Conventions.

The server does not start in time. The first start through npx downloads the package, which can take longer than Claude Desktop’s 60-second limit for a server to start. Run npm install -g squally-mcp once beforehand, and again after each update: npx then uses that global copy instead of downloading.

Your config edit is gone. A running Claude Desktop overwrites claude_desktop_config.json. Edit it only while the app is fully closed - quit it from the system tray too.

Where the log is. mcp-server-squally.log is in ~/Library/Logs/Claude on macOS and %APPDATA%\Claude\logs on Windows. The Microsoft Store build also writes to %LOCALAPPDATA%\Claude\logs.

Set SQUALLY_API_URL next to SQUALLY_API_KEY to point the server at another Squally, such as a local one: http://localhost:3000. It takes the base only - the server appends /api/v1 itself, and tolerates a trailing / or /api/v1. Without it, the server talks to https://app.squally.dev. Use a read key created in that app: a key is only known to the app that issued it.

If the address is wrong, the tool answers that it could not reach the Squally API there.

Certificates. squally-mcp trusts the operating system’s certificate store in addition to Node’s own, like a browser does, so it works behind antivirus HTTPS scanning and corporate TLS-inspecting proxies - on Node 22.19+ or 24.5+, while an older Node works as before; SQUALLY_USE_SYSTEM_CA=0 turns it off.

The key stays on your machine, in your agent’s local config. The server reads it from its environment, sends it only to the Squally API as the Authorization header, and never prints it. Keep it out of anything you commit: the user-level configs above keep it out of the repository.

The key reaches every project in the organization. Create one per machine, so a single revocation does not lock out everything, and revoke it in Organization › Read keys when you no longer need it. The list there shows when each key was created, last used, and when it expires.

Provenance. Every version from 0.1.1 on is published from the package’s GitHub Actions workflow with an npm provenance attestation: a signed statement linking the tarball to the workflow run and the commit it was built from. To check the latest version:

npm view squally-mcp dist.attestations

A version with provenance answers with the attestation’s url and predicateType: 'https://slsa.dev/provenance/v1'; one without answers nothing. 0.1.0, published by hand before trusted publishing was set up, is the only version without.