Skip to Content
MCP server

MCP server

The CLI includes a Model Context Protocol (opens in a new tab) server, so an AI agent calls the snippets as tools instead of reading their source or writing its own measurement code. It opens the page in a headless Chromium, runs the published snippet and returns the object the snippet returns, which follows the result contract (opens in a new tab).

npx -y webperf-snippets mcp

The server runs over stdio: the client starts this command and talks to it through its standard input and output. It is listed in the MCP Registry (opens in a new tab) as io.github.nucliweb/webperf-snippets.

Install

The server needs Node.js 20.12 or later and the Chromium of Playwright, which npx does not download:

npx playwright install chromium

Without it, the tools answer with that same command, and an agent with a terminal can run it and call the tool again.

Claude Code

claude mcp add webperf -- npx -y webperf-snippets mcp

Or install the webperf plugin, which adds the same server with the major version of the CLI pinned, so a breaking release does not reach it:

/plugin install webperf --marketplace nucliweb/webperf-snippets

On Claude Code versions before 2.1.275, add the marketplace first with /plugin marketplace add nucliweb/webperf-snippets, then run /plugin install webperf@webperf-snippets.

Cursor, Claude Desktop, Gemini CLI and Antigravity

These clients read a JSON file with an mcpServers object:

ClientFile
Cursor.cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project
Claude Desktopclaude_desktop_config.json, opened from Settings → Developer → Edit Config
Gemini CLI.gemini/settings.json in the project, or ~/.gemini/settings.json
Antigravity.agents/mcp_config.json in the workspace, or ~/.gemini/config/mcp_config.json; in the editor, MCP Servers → Manage MCP Servers → View raw config
{ "mcpServers": { "webperf": { "command": "npx", "args": ["-y", "webperf-snippets", "mcp"] } } }

VS Code

.vscode/mcp.json in the workspace, or the mcp.json of the user profile, names the object servers and requires the transport:

{ "servers": { "webperf": { "type": "stdio", "command": "npx", "args": ["-y", "webperf-snippets", "mcp"] } } }

Tools

ToolWhat it does
list_snippetsLists the snippets with their category, title, short aliases and whether they track interactions. Takes an optional category.
run_snippetOpens a URL in a headless Chromium, runs one snippet and returns a summary of its result. Takes url, snippet (name, alias or Category/Name), and optional viewport, waitMs, cpu, network, interactions, runs, baseline and thresholds.
run_workflowRuns a workflow (core-web-vitals, audit, loading, interaction) and the follow-ups of its decision tree, such as LCP-Subparts when LCP is over 2.5 s. Takes url, workflow, and optional viewport, waitMs, cpu, network, interactions, runs, baseline and thresholds.
get_cruxReturns the CrUX p75 of LCP, CLS and INP for a URL, or for its origin. Takes url, an optional formFactor (phone, desktop, tablet) and optional weeks (1 to 40), which adds the trend of each metric week by week from the CrUX History API. With them come the p75 of the sub-parts of an image LCP, the share of loads whose LCP is an image, the round trip time and the navigation types.
open_pageOpens a URL in a page that stays open between calls and installs tracking snippets (track, such as INP). Takes optional viewport, waitMs, cpu and network. Returns a sessionId.
interactRuns steps on an open page: click, type, hover, scroll or wait.
read_resultsReturns what the tracking snippets of an open page have recorded.
get_detailsReturns the whole object of one snippet of a run, by the runId that run_snippet, run_workflow or read_results returned, without measuring again. Takes runId and snippet.
close_pageCloses an open page.

The tools that measure return the whole object of each result when the results are small, and a summary of each (status, metric, rating and issues) when they are large, around 2,000 tokens or more; detail in the answer says which. An answer with summaries has a runId: the details, items and thresholds a summary leaves out come from get_details, with the values of the same run. run_snippet and run_workflow always return it, as the id of a baseline. The server keeps the last 20 runs of a connection. With detail: "summary" or detail: "full", run_snippet, run_workflow and read_results return one of the two whatever the size.

When the results call for another measurement, run_snippet and run_workflow add nextSteps: the calls to make next, with their arguments and their reason, such as run_snippet with LCP-Subparts when LCP is over 2.5 s. For the steps of a workflow that need clicks and key presses, it offers the workflow again with interactions, whose selector the agent replaces with an element of the page, and then open_page with those steps.

The tools take http and https URLs, local servers such as http://localhost:3000 included, and refuse any other protocol, such as file://. The agent chooses the URLs, so it can reach what your machine reaches, an internal address too. Everything a tool returns comes from the measured page, and the server tells the agent, in its instructions and in the description of each tool, to read it as data and never follow what it says.

The snippets that record interactions (INP, Interactions, Input-Latency-Breakdown) need clicks or key presses. run_snippet and run_workflow take them in interactions, which run after the load of each run. A page opened with open_page is for exploring: the agent installs the snippets, clicks or types with interact and reads them with read_results, one call each. At most three pages are open at a time, a page closes after 10 minutes without use, and every page and Chromium close when the client disconnects.

Throttle the CPU and the network

run_snippet, run_workflow and open_page take cpu and network, as --cpu and --network of the CLI. cpu slows the CPU of the page down by a factor of at least 1, such as 4 for a mid-range phone, and network takes a DevTools preset: fast-4g, slow-4g or 3g. Without them the page runs at the speed of the machine, with no throttling. The throttling applies before the page loads, and on a page of open_page it stays for every interact.

The answer adds a throttling field, the one of the JSON of the CLI, such as { "cpu": 4, "network": "slow-4g" }, and the conditions of the run name cpu and network too. A baseline measured under other throttling, or without it, gives a warning, as in the CLI. The calls in nextSteps carry the throttling of the run that suggests them.

Measure several runs

run_snippet and run_workflow take runs, from 1 to 9, as --runs of the CLI: the page loads that many times, one after the other, and the answer is the median run plus a runs field, the one of the JSON of the CLI, with the median, min, max, the value of each run and the agreement of each metric. A run that fails counts in failed and is left out; when every run fails, the answer is a tool error. The server keeps the results of the median run only, as the JSON of the CLI does, so get_details answers from that run and names it in medianRun; a metric other than the one that picked it (medianBy) can differ there from its median in runs.

Each run is a page load plus waitMs. With the default wait of 3 seconds, five runs of a workflow take about 20 seconds on a local page, and about a minute on a page that takes a few seconds to load. When the client asks for progress, the server reports the three stages of each run, numbered across the runs. A client sets its own limit on a tool call: the MCP SDK stops waiting after 60 seconds unless it is given a longer timeout or resetTimeoutOnProgress, and in Claude Code MCP_TOOL_TIMEOUT sets the limit. When the client gives up, the run in progress ends and no other starts.

Several calls at once

An agent often sends several calls in one turn, and the client runs them at the same time. The server takes them in turns: run_snippet, run_workflow, open_page and interact run one at a time, in the order they arrive, because a page load or a click competes for the CPU with any other, which inflates what the page reports. The answers are the ones of calls sent one by one, and the turn saves no time.

A call keeps its turn only while it runs, so nothing waits for a later call of the agent. A page left open with open_page holds up no measurement; if it keeps working, close it before you measure, as it still uses the CPU. read_results, close_page and the tools that load no page answer at once, and so does a call with a wrong argument.

Waiting counts towards the limit of the client on a tool call. When the client asks for progress, a waiting call reports how many calls are ahead, when the queue moves and every 5 seconds, so a client with resetTimeoutOnProgress keeps waiting. A call the client gives up on leaves the queue and its page never loads; if it was the one measuring, its Chromium closes and the next call starts. A call that fails passes the turn. A page that stops answering fails its call after 2 minutes per run, plus the wait steps of interact, and closes, so it does not hold up the rest.

Each connection has its own queue: two clients at the same time, or a client and the CLI, still compete for the CPU.

Compare with a baseline

run_snippet and run_workflow compare a run with an earlier one, with the verdict of --baseline. baseline takes the runId of an earlier run of the connection, which every run of these two tools returns, or the path of a page the CLI wrote with --json, absolute or relative to the working directory of the server. The answer adds a baseline field: the one of the JSON of the CLI, with the runId or the file it compared with, plus regressions, the same lines --fail-on-regression prints. thresholds takes the values of --threshold, such as ["LCP=15%", "CLS=0.1"]. A baseline that cannot be read or a threshold the server does not understand is a tool error, before the page loads, and a baseline of a matrix is refused, since a tool measures one page.

With five runs or more a side, a regression needs a Mann-Whitney U p-value under 0.05 on top of its threshold, as in the CLI, and each metric adds stats with the p-value, the shift and its 95 % interval. With fewer, a metric compares by its threshold and, against a baseline of several runs, by the range of those runs. A single run can be off by more than a threshold on a page that did not change, so read warnings, which also names each metric left without statistics and the values on each side after failed runs, and measure both sides with runs: 5, or --runs 5 in the CLI, when the verdict matters. An earlier run of the connection works for a change made in the same session, and a file measured with the CLI for a comparison with main. What agents get wrong lists the mistakes that change a verdict, such as deciding on one run or changing a wait, and what to do instead.

Measure interactions in one call

run_snippet and run_workflow take interactions, the steps of interact (click, type, hover, scroll and wait, up to 50), as --interact-script of the CLI. The steps run after the load of each run, so runs, baseline and thresholds work with them: one call gives the median run of five, under the throttling of cpu, and the verdict against a baseline.

With a click or a key press, run_snippet runs INP, Interactions and Input-Latency-Breakdown, and the interaction workflow runs its steps that need input instead of skipping them. Without interactions, run_snippet answers a tracking snippet with an error that names both ways to measure it, and the interaction workflow scrolls the page and skips those steps.

The INP of a product page, measured five times with a click on its buy button, with the CPU 4 times slower, against an earlier run of the same connection:

{ "url": "http://localhost:3000/product", "snippet": "INP", "interactions": [ { "action": "click", "selector": "#buy" }, { "action": "wait", "ms": 300 } ], "runs": 5, "cpu": 4, "baseline": "<runId of the run before the change>" }

The answer has the INP of the median run, runs with the INP of each run, throttling, and baseline with the verdict of INP and its p-value. The baseline is the runId of the same call without baseline, made before the change, or a page the CLI wrote with the same flags and --json. open_page, interact and read_results stay for exploring a page step by step, such as finding the element whose click is slow.

Resources and prompts

The resources are documents a client can attach to the context:

ResourceWhat it has
webperf://schemaThe contract every snippet result follows: base shape, statuses, how to read a result, the rules of the issue codes.
webperf://schema/{snippet}The fields one snippet returns, with an example, and the codes of its issues.
webperf://workflows/{category}The workflows and the decision tree of a category.
webperf://runs/{runId}A run of run_snippet or run_workflow of the connection, in the shape of the JSON of the CLI. The list names each run with its snippet or workflow, its URL, its viewport, how many runs it measured when they are several, and its time, to choose a baseline; saved to a file, it is a baseline of --baseline too.

The prompts chain the tools for a common task, each with a url argument: audit_lcp, debug_inp and audit_page. In Claude Code they run as /mcp__webperf__audit_lcp.

CrUX field data

get_crux needs a CrUX API key (opens in a new tab), free in the Google Cloud console, in the CRUX_API_KEY variable of the server. It goes in the configuration of the client, never in a tool argument:

claude mcp add webperf -e CRUX_API_KEY=<key> -- npx -y webperf-snippets mcp

The server of the plugin reads CRUX_API_KEY from the environment of Claude Code: export it in your shell before you start Claude Code.

{ "mcpServers": { "webperf": { "command": "npx", "args": ["-y", "webperf-snippets", "mcp"], "env": { "CRUX_API_KEY": "<key>" } } } }

In VS Code, an input keeps the key out of .vscode/mcp.json: VS Code asks for it the first time the server starts and stores it.

{ "inputs": [ { "type": "promptString", "id": "crux-api-key", "description": "CrUX API key", "password": true } ], "servers": { "webperf": { "type": "stdio", "command": "npx", "args": ["-y", "webperf-snippets", "mcp"], "env": { "CRUX_API_KEY": "${input:crux-api-key}" } } } }

An app opened from the Dock or the Start menu does not get the variables of your shell. If CRUX_API_KEY is only exported in your shell profile, set it in env. Keep a file with the key out of version control.

The other tools work without the key.

MCP server or Chrome DevTools MCP

The server measures a page in its own headless Chromium, with a fresh profile: no login, no extensions and no cache from earlier visits. To measure a page as you see it in your browser, with your session, use the Agent Skills with Chrome DevTools MCP (opens in a new tab), which run the same snippets in your Chrome.

Further reading

Last updated on