MCP server
Read your organization's real-user data from an AI client you run yourself (Claude, Cursor) over the Model Context Protocol. Read-only, opt-in per organization, connected with OAuth.
The Model Context Protocol (MCP) lets an AI client you run yourself ask questions about your fastmon data. Connect Claude, Claude Code, or Cursor once, and the model reads the same numbers as the dashboard: Web Vitals, traffic, errors, Server-Timing phases, Lighthouse results.
The model is yours. Fastmon answers from its own data, but the provider behind your client runs the model, and we neither pick that provider nor have a contract with it. That is why an owner has to open the server first.
Every tool is read-only. Nothing here changes a setting, deletes anything, or starts a run.
Turning it on
Organization settings → MCP server → Access from your own AI clients. Owner only (org:update), and the audit log records it as a consent change.
While it is off, the organization cannot be approved on the consent screen, and a token approved earlier gets 403 mcp_disabled before any query runs.
A consent switch, not a kill switch
Switching MCP off closes the MCP endpoint and nothing else. An app that already holds a connection keeps reading this organization through the ordinary API, which is the only access it ever asked for. A connection ends through the person who approved it: they disconnect it under Account → Access, their role narrows, or they leave. If somebody's access has to be gone, offboard them.
Connecting a client
One address, the same for everybody:
https://api.fastmon.eu/mcpClaude Code adds it from the terminal:
claude mcp add --transport http fastmon https://api.fastmon.eu/mcpEvery other client takes the same address as a remote MCP server or connector (Claude under Settings → Connectors, Cursor under MCP). The client then sends you to fastmon to sign in and approve. No key to paste, no secret to store.
The consent screen asks for:
- One organization, from those you can reach. The connection reads that one and nothing else, so two organizations mean two connections. One with MCP switched off is marked and cannot be approved.
analytics:read, required to connect at all.synthetic:read, optional. Three tools read Lighthouse reports and need it; the other 17 work without.
The screen labels the connection as an MCP one, next to the address it returns you to, because that is the fact worth seeing: what you release here can reach an AI model.
The connection belongs to you. Its permissions come from your current role on every request, so it narrows when you are demoted and ends with your membership or your account. An organization-owned connection is refused on purpose: a credential nobody holds could not end when its owner leaves.
Manage your connections under Account → Access. Disconnecting is immediate, and access tokens already issued expire within minutes.
Copy the address exactly, with no trailing slash. Your client names it when it asks for a token, and the endpoint only accepts a token addressed to it in that spelling. Nothing is served below /mcp.
What the model can read
20 tools, all read-only, all scoped to the organization the connection was approved for:
| Area | Tools |
|---|---|
| Discovery | list_sites, get_data_freshness |
| Traffic and Web Vitals | get_overview, get_timeseries, get_breakdown, get_experience_score |
| Change over time | compare_periods, detect_changes |
| Errors | get_errors, get_failed_resources, get_error_detail |
| Performance detail | get_cwv_distribution, get_loaf_attribution, get_resource_breakdown, get_third_party_impact, get_fetch_xhr, get_server_timing |
| Synthetic (lab) | list_synthetic_pages, get_synthetic_lighthouse, get_synthetic_lighthouse_audits |
Answers are aggregates and ranked lists, the same ones behind the dashboard. Nothing returns raw beacon rows, and no tool takes an organization: it comes off the connection.
A call without a site covers the whole organization, since an MCP connection has no "current site" the way a dashboard tab does. For a question about one site, a model should call list_sites first.
list_sites also reports what each site collects: whether fetch/XHR collection, page-type detection, checkout tracking, session collection, and error messages and stack frames are on, plus which visitor identifier its counts use. Without that, a model reads the empty result of a switched-off feature as "no problem". Visitor counts follow each site's own settings, exactly as the dashboard does, and a count spanning sites with different identifiers says so rather than quietly mixing them.
The three synthetic tools need synthetic:read plus Synthetic Monitoring enabled for the organization. Missing either one is a refusal with a reason, never an empty report.
How answers are qualified. Every p75 carries the measurement count behind it (samples), which is not the pageview count: the vital columns are nullable, so an LCP p75 can rest on a handful of measurements. It also makes a null legible, since inp_p75_ms: null beside samples.inp: 0 reads as "nothing measured here" rather than as a disabled feature. Where a tool applies a sample floor it reports the floor it used, so an empty result under a floor is a different finding from an empty dimension. Whether a number is too thin to quote stays the caller's judgement.
How filters match. A url written as a path (/checkout*) matches the path, and text filter values match case-insensitively, so browser: ["chrome"] and device_type: ["Desktop"] find the stored Chrome and desktop. country takes codes, not names, and repairs the ones ISO does not assign (UK and EN to GB, EL to GR). A tag no site carries is refused rather than answered as a scope of zero sites. There is no per-site timezone: a period without an offset is read as UTC.
Limits
- 90 days per call at most, and 50 rows per breakdown.
- 120 requests per minute, counted per person rather than per organization, so one client in a loop cannot lock out a colleague. Over the limit: 429 with the number of seconds to wait.
- Minute buckets span at most 6 hours, because minute resolution reads the raw beacon table. Longer periods use hourly buckets or coarser.
- 30 seconds per query, then it is cancelled and the tool says so. Usually a high-cardinality breakdown (URL, browser version, origin host) over a long period.
What leaves fastmon
The answers do: page URLs, referrers and UTM parameters, country-level geography, device and browser, timings, error signatures. It is the same field-level data your dashboard shows, and no tool reaches data a member could not already see there.
Where those answers go is the part to assess. The recipient is whoever runs the model behind your client, which is your relationship rather than ours: fastmon does not choose that provider, has no contract with it, and does not list it as a sub-processor. Whether your organization may make that transfer, and on what basis, is your assessment. Hence the owner-level switch with an audit entry.
The built-in Ask assistant is a separate decision: it sends data to our own sub-processor under our DPA. Two recipients, two switches, neither implies the other.
Fastmon's own collection rules do not change here. A field the tracker never collected is a field the model cannot ask for.
Agencies
A partner organization can connect a client organization, personally: the client organizations appear among the choices on the consent screen. Partner access resolves on every request, so the connection stops working the moment the partnership does, and there is nothing left to revoke.
mcp_enabled stays with the customer, because it hangs on org:update, which partner access deliberately does not carry. Onboarding therefore costs one click on the customer's side, and the approval lands in their audit log marked as partner access.
Building an MCP client
Streamable HTTP (JSON-RPC 2.0), stateless, plain JSON responses. No SSE, no session id to hold.
OAuth is the only way in. The endpoint takes an fmt_ access token and nothing else: no personal fm_ key, no organization fmo_ key, no session cookie. A key carries no audience, so it cannot say whether it was issued for this endpoint.
Point your client at https://api.fastmon.eu/mcp and it discovers the rest:
- The unauthenticated call answers
401withWWW-Authenticate: Bearer …, scope="analytics:read synthetic:read", resource_metadata="…". - That document (
/.well-known/oauth-protected-resource/mcp, RFC 9728) names the authorization server and the scopes this resource uses. - Register (RFC 7591), run authorization code with PKCE, and name the endpoint in the
resourceparameter (RFC 8707) on the authorization request and on the token request. One string, the same for every customer. The authorization request decides the audience, consent binds it to the connection, and the token request may only repeat the same value.
The tenant is settled at consent and nowhere else. A client cannot name an organization anywhere in the flow, which is why nothing tenant-shaped appears in the tool arguments either.
Two defaults are worth knowing. A registration with no scope gets analytics:read alone, read back from the response, so the Lighthouse tools need an explicit synthetic:read. A registration silent on credential_owner gets a personal connection, which is what this endpoint requires; organization plus an MCP resource is refused at consent with a message saying so.
403 mcp_disabled means the organization switched the server off. It carries no WWW-Authenticate header, because re-authorizing cannot fix it. Show the message instead of starting a new authorization flow.
The full OAuth contract, including refresh rotation and the errors to branch on, is on OAuth apps.
Related
- OAuth apps: registration, PKCE, tokens, disconnecting.
- Organizations → Roles: the roles a connection is cut against.
- Privacy: what fastmon collects in the first place.
- Synthetic Monitoring: the opt-in the three lab tools also need.