plausible-mcp
The plausible-mcp server lets you query and analyze Plausible Analytics data through any MCP-compatible AI tool. All operations are read-only and work with both Plausible Cloud and self-hosted instances.
get_timeseries: Retrieve traffic and conversion metrics (visitors, pageviews, bounce rate, etc.) over time with daily, weekly, or monthly granularity — useful for spotting trends or investigating the impact of a deploy.get_breakdown: Segment metrics by page, traffic source, country, device, browser, OS, UTM parameters, and more — ideal for finding top pages, top sources, or understanding audience segments.get_conversions: Fetch goal conversion rates and counts, optionally broken down by page to see which pages drive the most conversions.compare_periods: Side-by-side comparison of two date ranges, returning aggregate metrics for each period along with absolute and percentage deltas — perfect for before/after analysis.send_feedback(hosted only): File feedback about the server itself into the maintainers' Sentry User Feedback inbox.
Filtering & date ranges: All query tools support optional filtering by page path (with wildcard support) and goal name, and accept relative ranges (e.g. 7d, 30d) or explicit YYYY-MM-DD,YYYY-MM-DD ranges.
Provides tools to query traffic and conversion metrics from Plausible Analytics, including timeseries, breakdowns, conversions, and period comparisons.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@plausible-mcpHow did our traffic change this month compared to last month?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
plausible-mcp
MCP server for Plausible Analytics — query traffic, conversions, and compare time periods from any AI tool that supports Model Context Protocol.
Built for teams that want to ask questions like:
"Did our deploy on Tuesday affect traffic to /pricing?"
"What's the signup conversion rate on /blog this month?"
"How does this week's bounce rate compare to last week?"
Tools
Tool | Description |
| Traffic and conversion metrics over time (daily/weekly/monthly) |
| Break down by page, source, country, device, browser, OS, UTM params |
| Goal conversion rates, optionally per-page |
| Side-by-side comparison of two date ranges with absolute and % deltas |
All query tools are read-only and annotated with readOnlyHint: true.
Hosted deployments additionally expose send_feedback, which files feedback about the server itself (confusing errors, missing capabilities) into the maintainers' Sentry User Feedback inbox. It is only registered when the server runs with Sentry (enableFeedbackTool).
Related MCP server: Yandex Metrica MCP
Quick Start
Remote (Hosted)
A hosted instance is available at https://plausible-mcp.sentry.dev.
With your own Plausible API key (any user):
claude mcp add --transport http plausible https://plausible-mcp.sentry.dev/mcp --header "Authorization: Bearer YOUR_PLAUSIBLE_API_KEY"Keep the URL before
--header.--headeris variadic, so if it comes last it swallows the URL and the CLI fails witherror: missing required argument 'commandOrUrl'.
Or add manually to your MCP client config (Claude Desktop, Cursor, etc.):
{
"mcpServers": {
"plausible": {
"url": "https://plausible-mcp.sentry.dev/mcp",
"headers": {
"Authorization": "Bearer YOUR_PLAUSIBLE_API_KEY"
}
}
}
}Sentry employees (via OAuth 2.1 + Cloudflare Access):
The /internal endpoint is an OAuth 2.1 server — no API key needed. Add it as a remote/custom connector in any OAuth-capable MCP client (Cowork, Claude.ai connectors, Claude Desktop):
https://plausible-mcp.sentry.dev/internalThe client discovers the OAuth endpoints automatically, sends you through Sentry SSO (Cloudflare Access), and only @sentry.io identities are granted access. Queries run against a shared, server-side Plausible API key — you never handle a key.
The hosted
/internalatplausible-mcp.sentry.devis Sentry-only and can't be used outside the org. To run/internalfor a different organization, self-host and setALLOWED_EMAIL_DOMAINto your own domain. (The public/mcpbring-your-own-key endpoint has no such restriction.)
Local (STDIO)
If you prefer to run it locally, use Node.js 20 or newer:
git clone https://github.com/getsentry/plausible-mcp.git
cd plausible-mcp
pnpm install
pnpm buildAdd to Claude Code:
claude mcp add plausible -e PLAUSIBLE_API_KEY=your-key -- node /path/to/plausible-mcp/dist/index.jsOr Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"plausible": {
"command": "node",
"args": ["/path/to/plausible-mcp/dist/index.js"],
"env": {
"PLAUSIBLE_API_KEY": "your-key"
}
}
}
}Self-Hosting (Cloudflare Workers)
Deploy your own instance:
git clone https://github.com/getsentry/plausible-mcp.git
cd plausible-mcp
pnpm install
npx wrangler deployThe worker exposes two endpoints:
/mcp— bring-your-own-key. Each user passes their own Plausible API key via theAuthorization: Bearerheader. No shared secrets needed on the server. Works with any header-capable MCP client (Claude Code, Cursor, MCP Inspector)./internal— Access-protected MCP endpoint for managed connectors (Cowork, Claude.ai). A Cloudflare Access application with Managed OAuth fronts the whole Worker hostname (see the constraint below): Access runs the OAuth 2.1 handshake with the client and forwards each request to the Worker with aCf-Access-Jwt-Assertionheader. The Worker verifies that header and queries a shared, server-side Plausible API key. Access is gated to the email domain(s) inALLOWED_EMAIL_DOMAIN(defaults tosentry.io) — not tied to Sentry when you self-host; set it to your own domain.
Because the Managed OAuth application must cover the bare hostname with no path (Cloudflare rejects a path when OAuth is enabled — domain can not have a path if oauth is configured), it also gates /mcp. To keep the bring-your-own-key /mcp endpoint public you add a second, more-specific Access application scoped to the /mcp path with a Bypass policy. Cloudflare matches the most specific hostname+path first, so /mcp requests bypass Access entirely while everything else goes through OAuth. Both apps live on one hostname; no separate subdomain is required.
Beta / client requirement. Cloudflare Access Managed OAuth is in Beta and requires an MCP client that supports RFC 8707 (resource indicators). Confirm your connector supports it before relying on this path.
Setting up the /internal endpoint (Cloudflare Access Managed OAuth)
The Worker runs no OAuth server — Cloudflare Access is the authorization server. There is no OAUTH_KV, no cookie key, and no OAuth client id/secret. You create two Access applications on the same hostname.
Create the Managed OAuth application over the bare hostname (Zero Trust → Access → Applications): a self-hosted app or MCP server application whose domain is
plausible-mcp.sentry.devwith no path.⚠️ Do not scope it to
/internal. Once Managed OAuth is enabled, Cloudflare rejects any path withaccess.api.error.invalid_request: domain can not have a path if oauth is configured. The app must be the whole host; the Worker enforces the/internalroute itself.Add an Access policy (Action
Allow) restricting to your email domain (e.g.@acme.com) and identity provider.Enable Managed OAuth (Advanced settings → Managed OAuth) and set Allowed redirect URIs to your connector's actual callback — for Claude/Cowork that is
https://claude.ai/api/mcp/auth_callback. Public HTTPS callbacks must be listed or Dynamic Client Registration fails withinvalid_client_metadata: redirect_uri is not allowed by the account configuration; loopback (http://localhost:*) callbacks are allowed by default.Copy the application's AUD tag → this becomes
CF_ACCESS_AUD.
Carve
/mcpback out with a second, path-scoped Bypass application. Because step 1 covers the whole host,/mcp(bring-your-own-key) is now gated too. Create another self-hosted app, domainplausible-mcp.sentry.devpathmcp, with Managed OAuth OFF, and a policy whose Action isBypasswith the selectorEveryone.Bypass≠Allow: anAllowpolicy still forces an interactive login (the client gets an HTML302to the login page and fails withUnexpected content type: text/html). OnlyBypasslets the request through with no authentication, so the Worker's own Bearer-key check applies.
Set the worker secrets:
npx wrangler secret put PLAUSIBLE_API_KEY # shared key for /internal queries npx wrangler secret put SENTRY_DSN # optional — the Worker's own telemetryCF_ACCESS_TEAM_DOMAINandCF_ACCESS_AUDare not secrets — a public JWKS URL and an application identifier — so they go in[vars]in step 4.Set the
[vars]inwrangler.toml:CF_ACCESS_TEAM_DOMAIN—https://<team>.cloudflareaccess.com, no trailing slash. Verifies theCf-Access-Jwt-AssertionJWKS and issuer.CF_ACCESS_AUD— the AUD tag you copied in step 1.ALLOWED_EMAIL_DOMAIN— the email domain(s) allowed to sign in, comma-separated,@optional (defaultsentry.io). Enforced in code in addition to the Access policy in step 1, so set it to your own domain — otherwise every login is rejected.MCP_ALLOWED_HOSTNAMES— comma-separated hostnames accepted by the MCP endpoints. Replaceplausible-mcp.sentry.devwith your worker's hostname; keep the localhost entries if you usewrangler dev.MCP_ALLOWED_ORIGIN_HOSTNAMES— comma-separated browser Origin hostnames allowed to call/internal. Non-browser clients do not send anOriginheader.
Deploy (
npx wrangler deploy), then point an RFC 8707-capable MCP client athttps://<your-worker-host>/internal.
Troubleshooting. All of these are Cloudflare Access configuration, not the Worker — a request only reaches the Worker (and its Sentry spans) once Access forwards it:
Symptom (in the connector) | Cause | Fix |
| Connector callback isn't in Allowed redirect URIs | Add the exact callback (step 1); read the rejected |
| Managed OAuth app scoped to a path | Rescope app 1 to the bare host (step 1) |
|
| Set the app-2 policy Action to |
| No | Create app 2 (step 2) |
Configuration
Environment Variable | Required | Default | Description |
| Yes (STDIO; Worker | — | Your Plausible API key (get one here). On the Worker this is the shared key for |
| No |
| URL of your Plausible instance (for self-hosted) |
| No | — | Default site domain so you don't have to pass |
| Yes (Worker | — |
|
| Yes (Worker | — | The Access application's Application Audience (AUD) tag — checked against the assertion's |
| No (Worker) | — | Sentry DSN for the Worker's own telemetry ( |
| No (Worker |
| Comma-separated email domain(s) allowed to sign in to |
| Yes (Worker) | — | Comma-separated hostname allowlist used to validate MCP |
| No (Worker | — | Comma-separated browser Origin hostnames allowed to call |
On the Worker, the /mcp endpoint needs no server-side key — each user passes their own via Authorization: Bearer. The /internal endpoint is fronted by Cloudflare Access Managed OAuth and uses a shared server-side PLAUSIBLE_API_KEY secret (see self-hosting).
Plausible API
This server wraps the Plausible Stats API v2 (POST /api/v2/query). It works with both Plausible Cloud and self-hosted instances.
Supported Metrics
visitors, visits, pageviews, views_per_visit, bounce_rate, visit_duration, events, scroll_depth, percentage, conversion_rate, group_conversion_rate, average_revenue, total_revenue, time_on_page
Supported Dimensions
event:page, event:goal, event:hostname, visit:entry_page, visit:exit_page, visit:source, visit:referrer, visit:channel, visit:utm_medium, visit:utm_source, visit:utm_campaign, visit:utm_content, visit:utm_term, visit:device, visit:browser, visit:browser_version, visit:os, visit:os_version, visit:country, visit:region, visit:city, visit:country_name, visit:region_name, visit:city_name
The *_name geography dimensions return human-readable names (e.g. "Canada"); the plain visit:country/region/city return ISO/Geoname codes.
Filtering
Every query tool accepts property_filters, which — despite the name — filters by built-in dimensions as well as custom event properties. Each entry is { "property", "operator", "values" }:
property— a built-in dimension (e.g.visit:channel,visit:source,event:page) or a custom property as its bare name ("plan"targetsevent:props:plan).operator—is,is_not,contains,contains_not(defaultis).event:goalsupports onlyisandcontains.Multiple entries combine with AND, as do the
page/goalshortcut parameters. Targetingevent:page/event:goalfrom both a shortcut andproperty_filtersin the same call is rejected — use one or the other.
For example, top pages for organic search traffic: get_breakdown with dimension: "event:page" and property_filters: [{ "property": "visit:channel", "values": ["Organic Search"] }].
Custom Properties
Sites send their own custom event properties, addressed as event:props:<name>. These are site-specific, so there's no fixed list.
Break down by a custom property: pass
get_breakdownadimensionofevent:props:<name>(e.g.event:props:plan).Filter by a custom property via
property_filterswith the bare name, e.g.[{ "property": "plan", "operator": "is", "values": ["pro"] }].
Development
pnpm install
pnpm build # TypeScript compilation
pnpm test # Run unit + integration tests
pnpm test:watch # Watch modeTesting with MCP Inspector
pnpm build
PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector node dist/index.jsLLM Evals
Verifies the model picks the right tool for natural language analytics questions. Runs through
OpenRouter, so any tool-calling model works — the default is anthropic/claude-sonnet-5:
OPENROUTER_API_KEY=sk-or-... pnpm eval
OPENROUTER_MODEL=openai/gpt-5 OPENROUTER_API_KEY=sk-or-... pnpm eval # try another modelArchitecture
src/
├── index.ts # STDIO entry point (local use)
├── worker.ts # Cloudflare Worker entry point (remote)
├── env.ts # Worker environment bindings
├── cf-access.ts # Verifies the Cloudflare Access assertion on /internal
├── server.ts # Creates McpServer, registers all tools
├── plausible.ts # PlausibleClient — standalone API client
├── schemas.ts # Shared Zod schemas and filter helpers
├── errors.ts # UserFacingError and tool-error reporting
├── telemetry.ts # Pure classifiers — route, MCP request kind, client family
├── mcp-telemetry.ts # Records MCP client info onto the active span
├── redaction.ts # Strips PII from Sentry events on the BYOK path
└── tools/
├── get-timeseries.ts
├── get-breakdown.ts
├── get-conversions.ts
├── compare-periods.ts
└── send-feedback.tsPlausibleClient has zero MCP dependency and can be used standalone.
Observability & data collection
The Worker reports to Sentry with an endpoint-dependent privacy posture:
/mcp(bring-your-own-key) — fully anonymous. Tool inputs and outputs are not recorded (that data belongs to the caller and their own key), no identity is attached, and the ingest-inferred client IP is stripped (src/redaction.ts). Only operational telemetry remains: tool names, span timings, and failures./internal(SSO-gated) — attributed. Requests carry the authenticated@sentry.ioemail (Sentry.setUser), and tool inputs/outputs are recorded (recordToolIO) for attribution and abuse-tracing on the shared server-side key.
Authorization / Cookie / Cf-Access-Jwt-Assertion headers are stripped from spans on both paths. As a belt-and-suspenders backstop, enable Prevent Storing of IP Addresses in the Sentry project's Security & Privacy settings.
License
MIT — see LICENSE.
Available Tools
4 toolscompare_periodsCompare PeriodsARead-onlyIdempotent
Compare metrics between two date ranges side by side. Ideal for before/after deploy analysis. Returns aggregate values for each period plus the delta (absolute and %).
| Name | Required | Description | Default |
|---|---|---|---|
| goal | No | Filter by goal name (e.g. Signup, Purchase). Do not also target event:goal in property_filters; the call is rejected. | |
| page | No | Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected. | |
| metrics | No | Metrics to return. Defaults vary by tool. Plausible rejects these combinations: bounce_rate or visit_duration with event:goal or event:props:<name> dimensions; pageviews, events, scroll_depth or time_on_page with visit:entry_page or visit:exit_page dimensions; views_per_visit with any non-time dimension or a page filter. For goal conversions use get_conversions. | |
| site_id | Yes | Plausible site domain (e.g. example.com). Required. | |
| period_a | Yes | First date range, e.g. "2024-01-01,2024-01-07" or "7d" | |
| period_b | Yes | Second date range, e.g. "2024-01-08,2024-01-14" or "7d" | |
| property_filters | No | Filter results by built-in dimensions or custom event properties, e.g. [{ "property": "visit:channel", "operator": "is", "values": ["Organic Search"] }] or [{ "property": "plan", "values": ["pro"] }]. Entries are combined with AND. |
Output Schema
| Name | Required | Description |
|---|---|---|
| deltas | Yes | Per-metric change from period_a to period_b (absolute and percent) |
| period_a | Yes | |
| period_b | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and openWorldHint, so the safety profile is covered. The description adds that results include per-period aggregates plus absolute and percentage deltas, which is useful behavioral context, but says nothing about behavior under rejected metric/dimension combinations or how periods of differing lengths are handled.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, zero filler, with the core comparison action front-loaded ahead of the use case and the return shape. Nothing could be removed without losing information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With annotations covering the safety profile, an output schema documenting return values, and a fully-described 7-parameter schema, the description only needs to establish purpose, use case, and comparison semantics — all of which it does. The one gap is guidance on choosing this over the sibling analysis tools.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, including detailed regex patterns for period_a/period_b and metric-combination rejection rules, so the schema carries the semantics. The description repeats nothing about parameter formats, meaning the baseline 3 applies — no added meaning, no harm.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource — 'Compare metrics between two date ranges side by side' — which an agent can distinguish from get_timeseries and get_breakdown on its own. However, it never names those siblings or explains how the comparison output differs from a two-call timeseries, leaving the differentiation implicit.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
'Ideal for before/after deploy analysis' gives a concrete usage scenario, but there is no statement of when NOT to use it and no mention of get_timeseries/get_breakdown as alternatives for single-period or dimension-based analysis. Usage is implied rather than prescribed.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_breakdownGet BreakdownBRead-onlyIdempotent
Break down metrics by a dimension: page, traffic source, country, device, etc. Use to find top pages, sources, or segment traffic.
| Name | Required | Description | Default |
|---|---|---|---|
| page | No | Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected. | |
| limit | No | Max results to return per page. Use offset to fetch further pages; pagination.has_more in the result says whether more rows exist. | |
| offset | No | Number of rows to skip, for paging past the first `limit` rows | |
| metrics | No | Metrics to return. Defaults vary by tool. Plausible rejects these combinations: bounce_rate or visit_duration with event:goal or event:props:<name> dimensions; pageviews, events, scroll_depth or time_on_page with visit:entry_page or visit:exit_page dimensions; views_per_visit with any non-time dimension or a page filter. For goal conversions use get_conversions. | |
| site_id | Yes | Plausible site domain (e.g. example.com). Required. | |
| dimension | Yes | Dimension to group results by: a standard dimension (e.g. event:page, visit:source), or a custom event property as "event:props:<name>" (e.g. event:props:plan). Prefer visit:country_name/region_name/city_name over the code variants when presenting geography to people. An empty visit:entry_page or visit:exit_page value means a session with no recorded pageview. | |
| date_range | Yes | Date range: "1h", "24h" (last N hours, ending now), "7d", "30d", "12mo", "month", "year", "all", "YYYY-MM-DD,YYYY-MM-DD" (start,end, both inclusive), or "YYYY-MM-DDTHH:MM:SSZ,YYYY-MM-DDTHH:MM:SSZ" for a sub-day window (any offset, e.g. +02:00). Days after today count as zero. | |
| property_filters | No | Filter results by built-in dimensions or custom event properties, e.g. [{ "property": "visit:channel", "operator": "is", "values": ["Organic Search"] }] or [{ "property": "plan", "values": ["pro"] }]. Entries are combined with AND. |
Output Schema
| Name | Required | Description |
|---|---|---|
| metrics | Yes | Metric keys, in the order they appear in each row's `metrics` array |
| results | Yes | One row per dimension-value combination returned by Plausible |
| dimensions | Yes | Dimension keys, in the order they appear in each row's `dimensions` array |
| pagination | No | Present only on paged tools (get_breakdown) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and openWorldHint, so the safety profile is fully covered. The description adds no behavioral context of its own—nothing about result caps, pagination implications, rejection of metric/dimension combinations, or the open-world nature of custom event properties.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences with the core action front-loaded, followed by the use case. Zero filler, though the second sentence is somewhat generic.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With 8 parameters, three required, full schema coverage and an output schema, the description need only establish intent and it does. What is missing—metric/dimension rejection rules and the get_conversions pointer—lives in the schema, though surfacing the routing hint in the description would help selection.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema carries the parameter burden and baseline 3 applies. The description does map loosely onto dimension values (page, traffic source, country, device), but its use of 'page' arguably blurs the `page` filter parameter with the `event:page` dimension, and it adds no syntax detail beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'Break down metrics by a dimension,' then names concrete dimensions (page, traffic source, country, device). An agent can distinguish this from a timeseries or period-comparison tool by the 'group by dimension' framing, though no sibling is named explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives a positive use case ('find top pages, sources, or segment traffic') but offers no exclusions or explicit routing to alternatives such as get_timeseries or compare_periods. The 'For goal conversions use get_conversions' guidance exists only in the schema, not in the description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_conversionsGet ConversionsARead-onlyIdempotent
Get goal conversion rates and counts. Can break down by page to see which pages drive conversions.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | No | Filter by goal name (e.g. Signup, Purchase). Do not also target event:goal in property_filters; the call is rejected. | |
| page | No | Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected. | |
| site_id | Yes | Plausible site domain (e.g. example.com). Required. | |
| date_range | Yes | Date range: "1h", "24h" (last N hours, ending now), "7d", "30d", "12mo", "month", "year", "all", "YYYY-MM-DD,YYYY-MM-DD" (start,end, both inclusive), or "YYYY-MM-DDTHH:MM:SSZ,YYYY-MM-DDTHH:MM:SSZ" for a sub-day window (any offset, e.g. +02:00). Days after today count as zero. | |
| property_filters | No | Filter results by built-in dimensions or custom event properties, e.g. [{ "property": "visit:channel", "operator": "is", "values": ["Organic Search"] }] or [{ "property": "plan", "values": ["pro"] }]. Entries are combined with AND. | |
| breakdown_by_page | No | If true, shows conversion rate per page |
Output Schema
| Name | Required | Description |
|---|---|---|
| metrics | Yes | Metric keys, in the order they appear in each row's `metrics` array |
| results | Yes | One row per dimension-value combination returned by Plausible |
| dimensions | Yes | Dimension keys, in the order they appear in each row's `dimensions` array |
| pagination | No | Present only on paged tools (get_breakdown) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and openWorldHint, so the safety profile is covered. The description adds nothing beyond that — it does not say whether a site must have goals configured, what a conversion rate is computed against, or any limits on the breakdown — so it clears the lower bar annotations set but does not exceed it.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the core purpose and followed by the optional breakdown use case. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present and a fully documented input schema, the description only needs to frame purpose and usage. It does, but it omits the precondition that conversions refer to configured site goals, which an agent needs to interpret empty results correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all six parameters including goal, page, property_filters and the goal/page filter-conflict warning are already documented in the schema. The description only echoes the breakdown_by_page parameter, adding no syntax or constraint details beyond the schema baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Get goal conversion rates and counts") and describes the breakdown capability. It is distinct enough from get_timeseries/get_breakdown in subject matter, though it never explicitly contrasts itself with those siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase "Can break down by page to see which pages drive conversions" implies a use case for the breakdown mode, but there is no guidance on when to choose this tool over get_breakdown or compare_periods, and no prerequisites (e.g. goals must be configured) are mentioned.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_timeseriesGet TimeseriesARead-onlyIdempotent
Get traffic and conversion metrics over time for a site or specific page. Use to spot trends and changes around deploys.
| Name | Required | Description | Default |
|---|---|---|---|
| goal | No | Filter by goal name (e.g. Signup, Purchase). Do not also target event:goal in property_filters; the call is rejected. | |
| page | No | Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected. | |
| metrics | No | Metrics to return. Defaults vary by tool. Plausible rejects these combinations: bounce_rate or visit_duration with event:goal or event:props:<name> dimensions; pageviews, events, scroll_depth or time_on_page with visit:entry_page or visit:exit_page dimensions; views_per_visit with any non-time dimension or a page filter. For goal conversions use get_conversions. | |
| site_id | Yes | Plausible site domain (e.g. example.com). Required. | |
| date_range | Yes | Date range: "1h", "24h" (last N hours, ending now), "7d", "30d", "12mo", "month", "year", "all", "YYYY-MM-DD,YYYY-MM-DD" (start,end, both inclusive), or "YYYY-MM-DDTHH:MM:SSZ,YYYY-MM-DDTHH:MM:SSZ" for a sub-day window (any offset, e.g. +02:00). Days after today count as zero. | |
| granularity | No | Time bucket size. Use "hour" with a sub-day or few-day date_range. | day |
| property_filters | No | Filter results by built-in dimensions or custom event properties, e.g. [{ "property": "visit:channel", "operator": "is", "values": ["Organic Search"] }] or [{ "property": "plan", "values": ["pro"] }]. Entries are combined with AND. |
Output Schema
| Name | Required | Description |
|---|---|---|
| metrics | Yes | Metric keys, in the order they appear in each row's `metrics` array |
| results | Yes | One row per dimension-value combination returned by Plausible |
| dimensions | Yes | Dimension keys, in the order they appear in each row's `dimensions` array |
| pagination | No | Present only on paged tools (get_breakdown) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent and openWorld, so the safety profile is covered. The description adds only the scope ('a site or specific page') and gives no behavioral detail such as rate limits, data freshness, or handling of empty buckets. With annotations and an output schema carrying most of the burden, a 3 is appropriate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with what is returned and followed by when to reach for it. No filler, no repetition of schema content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema, a fully documented input schema, and annotations, the description needs to supply only purpose and routing, and it does so. The one real gap is lack of explicit sibling differentiation against get_breakdown and compare_periods, which an agent would need in order to pick this tool confidently.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so every parameter including date_range syntax, granularity, metrics conflicts, and property_filters is fully documented in the schema. The description only hints at page-level scoping ('or specific page'), which adds marginal value and earns the baseline 3.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: retrieve traffic and conversion metrics over time, scoped to a site or a specific page. That is clear enough to distinguish it from get_breakdown or compare_periods, but the description never names those siblings, so differentiation depends on inference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'Use to spot trends and changes around deploys' gives an implied usage context, which is better than nothing. However, it names no alternatives and sets no exclusions, so an agent must read the sibling schemas or the get_conversions reference buried in the metrics parameter to choose correctly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
v0.10.0- Changed
compare_periods5 fields changed- changed
Input schema / properties / goal / descriptionPrevious value: -"Filter by goal name (e.g. Signup, Purchase)"New value: +"Filter by goal name (e.g. Signup, Purchase). Do not also target event:goal in property_filters; the call is rejected." - changed
Input schema / properties / metrics / descriptionPrevious value: -"Metrics to return. Defaults vary by tool."New value: +"Metrics to return. Defaults vary by tool. Plausible rejects these combinations: bounce_rate or visit_duration with event:goal or event:props:<name> dimensions; pageviews, events, scroll_depth or time_on_page with visit:entry_page or visit:exit_page dimensions; views_per_visit with any non-time dimension or a page filter. For goal conversions use get_conversions." - changed
Input schema / properties / page / descriptionPrevious value: -"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)"New value: +"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected." - changed
Input schema / properties / period_a / patternPrevious value: -"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2})$"New value: +"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}),\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}))$" - changed
Input schema / properties / period_b / patternPrevious value: -"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2})$"New value: +"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}),\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}))$"
- Changed
get_breakdown8 fields changed- changed
Input schema / properties / date_range / descriptionPrevious value: -"Date range: \"7d\", \"30d\", \"12mo\", \"month\", \"year\", \"all\", or \"YYYY-MM-DD,YYYY-MM-DD\""New value: +"Date range: \"1h\", \"24h\" (last N hours, ending now), \"7d\", \"30d\", \"12mo\", \"month\", \"year\", \"all\", \"YYYY-MM-DD,YYYY-MM-DD\" (start,end, both inclusive), or \"YYYY-MM-DDTHH:MM:SSZ,YYYY-MM-DDTHH:MM:SSZ\" for a sub-day window (any offset, e.g. +02:00). Days after today count as zero." - changed
Input schema / properties / date_range / patternPrevious value: -"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2})$"New value: +"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}),\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}))$" - changed
Input schema / properties / dimension / descriptionPrevious value: -"Dimension to group results by: a standard dimension (e.g. event:page, visit:source), or a custom event property as \"event:props:<name>\" (e.g. event:props:plan)."New value: +"Dimension to group results by: a standard dimension (e.g. event:page, visit:source), or a custom event property as \"event:props:<name>\" (e.g. event:props:plan). Prefer visit:country_name/region_name/city_name over the code variants when presenting geography to people. An empty visit:entry_page or visit:exit_page value means a session with no recorded pageview." - changed
Input schema / properties / limit / descriptionPrevious value: -"Max results to return"New value: +"Max results to return per page. Use offset to fetch further pages; pagination.has_more in the result says whether more rows exist." - changed
Input schema / properties / metrics / descriptionPrevious value: -"Metrics to return. Defaults vary by tool."New value: +"Metrics to return. Defaults vary by tool. Plausible rejects these combinations: bounce_rate or visit_duration with event:goal or event:props:<name> dimensions; pageviews, events, scroll_depth or time_on_page with visit:entry_page or visit:exit_page dimensions; views_per_visit with any non-time dimension or a page filter. For goal conversions use get_conversions." - added
Input schema / properties / offsetAdded value: +{ + "description": "Number of rows to skip, for paging past the first `limit` rows", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" +} - changed
Input schema / properties / page / descriptionPrevious value: -"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)"New value: +"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected." - added
Output schema / properties / paginationAdded value: +{ + "additionalProperties": false, + "description": "Present only on paged tools (get_breakdown)", + "properties": { + "has_more": { + "description": "True when rows exist beyond this page", + "type": "boolean" + }, + "limit": { + "description": "Max rows requested", + "type": "number" + }, + "offset": { + "description": "Rows skipped before the first returned row", + "type": "number" + } + }, + "required": [ + "limit", + "offset", + "has_more" + ], + "type": "object" +}
- Changed
get_conversions5 fields changed- changed
Input schema / properties / date_range / descriptionPrevious value: -"Date range: \"7d\", \"30d\", \"12mo\", \"month\", \"year\", \"all\", or \"YYYY-MM-DD,YYYY-MM-DD\""New value: +"Date range: \"1h\", \"24h\" (last N hours, ending now), \"7d\", \"30d\", \"12mo\", \"month\", \"year\", \"all\", \"YYYY-MM-DD,YYYY-MM-DD\" (start,end, both inclusive), or \"YYYY-MM-DDTHH:MM:SSZ,YYYY-MM-DDTHH:MM:SSZ\" for a sub-day window (any offset, e.g. +02:00). Days after today count as zero." - changed
Input schema / properties / date_range / patternPrevious value: -"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2})$"New value: +"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}),\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}))$" - changed
Input schema / properties / goal / descriptionPrevious value: -"Filter by goal name (e.g. Signup, Purchase)"New value: +"Filter by goal name (e.g. Signup, Purchase). Do not also target event:goal in property_filters; the call is rejected." - changed
Input schema / properties / page / descriptionPrevious value: -"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)"New value: +"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected." - added
Output schema / properties / paginationAdded value: +{ + "additionalProperties": false, + "description": "Present only on paged tools (get_breakdown)", + "properties": { + "has_more": { + "description": "True when rows exist beyond this page", + "type": "boolean" + }, + "limit": { + "description": "Max rows requested", + "type": "number" + }, + "offset": { + "description": "Rows skipped before the first returned row", + "type": "number" + } + }, + "required": [ + "limit", + "offset", + "has_more" + ], + "type": "object" +}
- Changed
get_timeseries8 fields changed- changed
Input schema / properties / date_range / descriptionPrevious value: -"Date range: \"7d\", \"30d\", \"12mo\", \"month\", \"year\", \"all\", or \"YYYY-MM-DD,YYYY-MM-DD\""New value: +"Date range: \"1h\", \"24h\" (last N hours, ending now), \"7d\", \"30d\", \"12mo\", \"month\", \"year\", \"all\", \"YYYY-MM-DD,YYYY-MM-DD\" (start,end, both inclusive), or \"YYYY-MM-DDTHH:MM:SSZ,YYYY-MM-DDTHH:MM:SSZ\" for a sub-day window (any offset, e.g. +02:00). Days after today count as zero." - changed
Input schema / properties / date_range / patternPrevious value: -"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2})$"New value: +"^(\\d+h|\\d+d|\\d+mo|day|month|year|all|\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}),\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2}))$" - changed
Input schema / properties / goal / descriptionPrevious value: -"Filter by goal name (e.g. Signup, Purchase)"New value: +"Filter by goal name (e.g. Signup, Purchase). Do not also target event:goal in property_filters; the call is rejected." - changed
Input schema / properties / granularity / descriptionPrevious value: -"Time bucket size"New value: +"Time bucket size. Use \"hour\" with a sub-day or few-day date_range." - changed
Input schema / properties / granularity / enumPrevious value: -[ - "day", - "week", - "month" -]New value: +[ + "hour", + "day", + "week", + "month" +] - changed
Input schema / properties / metrics / descriptionPrevious value: -"Metrics to return. Defaults vary by tool."New value: +"Metrics to return. Defaults vary by tool. Plausible rejects these combinations: bounce_rate or visit_duration with event:goal or event:props:<name> dimensions; pageviews, events, scroll_depth or time_on_page with visit:entry_page or visit:exit_page dimensions; views_per_visit with any non-time dimension or a page filter. For goal conversions use get_conversions." - changed
Input schema / properties / page / descriptionPrevious value: -"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)"New value: +"Filter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*). Do not also target event:page in property_filters; the call is rejected." - added
Output schema / properties / paginationAdded value: +{ + "additionalProperties": false, + "description": "Present only on paged tools (get_breakdown)", + "properties": { + "has_more": { + "description": "True when rows exist beyond this page", + "type": "boolean" + }, + "limit": { + "description": "Max rows requested", + "type": "number" + }, + "offset": { + "description": "Rows skipped before the first returned row", + "type": "number" + } + }, + "required": [ + "limit", + "offset", + "has_more" + ], + "type": "object" +}
4 tool updates
v0.9.0- Changed
compare_periods8 fields changed- removed
Output schema / properties / deltas / additionalProperties / properties / absolute / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / deltas / additionalProperties / properties / absolute / typeAdded value: +[ + "number", + "null" +] - removed
Output schema / properties / deltas / additionalProperties / properties / percent / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / deltas / additionalProperties / properties / percent / typeAdded value: +[ + "number", + "null" +] - removed
Output schema / properties / period_a / properties / metrics / additionalProperties / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / period_a / properties / metrics / additionalProperties / typeAdded value: +[ + "number", + "null" +] - removed
Output schema / properties / period_b / properties / metrics / additionalProperties / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / period_b / properties / metrics / additionalProperties / typeAdded value: +[ + "number", + "null" +]
- Changed
get_breakdown4 fields changed- removed
Output schema / properties / results / items / properties / dimensions / items / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "number" - } -] - added
Output schema / properties / results / items / properties / dimensions / items / typeAdded value: +[ + "string", + "number" +] - removed
Output schema / properties / results / items / properties / metrics / items / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / results / items / properties / metrics / items / typeAdded value: +[ + "number", + "string", + "null" +]
- Changed
get_conversions4 fields changed- removed
Output schema / properties / results / items / properties / dimensions / items / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "number" - } -] - added
Output schema / properties / results / items / properties / dimensions / items / typeAdded value: +[ + "string", + "number" +] - removed
Output schema / properties / results / items / properties / metrics / items / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / results / items / properties / metrics / items / typeAdded value: +[ + "number", + "string", + "null" +]
- Changed
get_timeseries4 fields changed- removed
Output schema / properties / results / items / properties / dimensions / items / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "number" - } -] - added
Output schema / properties / results / items / properties / dimensions / items / typeAdded value: +[ + "string", + "number" +] - removed
Output schema / properties / results / items / properties / metrics / items / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / results / items / properties / metrics / items / typeAdded value: +[ + "number", + "string", + "null" +]
4 tool updates
v0.7.2- Changed
compare_periods1 field changed- added
Input schema / properties / property_filters / items / properties / values / items / maxLengthAdded value: +1024
- Changed
get_breakdown1 field changed- added
Input schema / properties / property_filters / items / properties / values / items / maxLengthAdded value: +1024
- Changed
get_conversions1 field changed- added
Input schema / properties / property_filters / items / properties / values / items / maxLengthAdded value: +1024
- Changed
get_timeseries1 field changed- added
Input schema / properties / property_filters / items / properties / values / items / maxLengthAdded value: +1024
4 tool updates
v0.7.1- Changed
compare_periods5 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / properties / property_filtersAdded value: +{ + "description": "Filter results by built-in dimensions or custom event properties, e.g. [{ \"property\": \"visit:channel\", \"operator\": \"is\", \"values\": [\"Organic Search\"] }] or [{ \"property\": \"plan\", \"values\": [\"pro\"] }]. Entries are combined with AND.", + "items": { + "properties": { + "operator": { + "default": "is", + "description": "Match operator: is, is_not, contains, contains_not (default: is)", + "enum": [ + "is", + "is_not", + "contains", + "contains_not" + ], + "type": "string" + }, + "property": { + "description": "What to filter on: a built-in dimension (e.g. \"visit:channel\", \"visit:source\", \"event:page\") or a custom event property as its bare name (e.g. \"plan\" targets event:props:plan)", + "maxLength": 312, + "minLength": 1, + "type": "string" + }, + "values": { + "description": "One or more values to match the property against", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "property", + "values" + ], + "type": "object" + }, + "type": "array" +} - changed
Input schema / properties / site_id / descriptionPrevious value: -"Plausible site domain (e.g. example.com). Uses PLAUSIBLE_DEFAULT_SITE_ID if omitted."New value: +"Plausible site domain (e.g. example.com). Required." - changed
Input schema / requiredPrevious value: -[ - "period_a", - "period_b" -]New value: +[ + "site_id", + "period_a", + "period_b" +] - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
- Changed
get_breakdown9 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / properties / dimension / anyOfAdded value: +[ + { + "enum": [ + "event:page", + "event:goal", + "event:hostname", + "visit:entry_page", + "visit:exit_page", + "visit:source", + "visit:referrer", + "visit:channel", + "visit:utm_medium", + "visit:utm_source", + "visit:utm_campaign", + "visit:utm_content", + "visit:utm_term", + "visit:device", + "visit:browser", + "visit:browser_version", + "visit:os", + "visit:os_version", + "visit:country", + "visit:region", + "visit:city", + "visit:country_name", + "visit:region_name", + "visit:city_name" + ], + "type": "string" + }, + { + "maxLength": 312, + "minLength": 13, + "pattern": "^event:props:", + "type": "string" + } +] - changed
Input schema / properties / dimension / descriptionPrevious value: -"Dimension to group results by"New value: +"Dimension to group results by: a standard dimension (e.g. event:page, visit:source), or a custom event property as \"event:props:<name>\" (e.g. event:props:plan)." - removed
Input schema / properties / dimension / enumRemoved value: -[ - "event:page", - "event:goal", - "event:hostname", - "visit:entry_page", - "visit:exit_page", - "visit:source", - "visit:referrer", - "visit:channel", - "visit:utm_medium", - "visit:utm_source", - "visit:utm_campaign", - "visit:utm_content", - "visit:utm_term", - "visit:device", - "visit:browser", - "visit:browser_version", - "visit:os", - "visit:os_version", - "visit:country", - "visit:region", - "visit:city", - "visit:country_name", - "visit:region_name", - "visit:city_name" -] - removed
Input schema / properties / dimension / typeRemoved value: -"string" - added
Input schema / properties / property_filtersAdded value: +{ + "description": "Filter results by built-in dimensions or custom event properties, e.g. [{ \"property\": \"visit:channel\", \"operator\": \"is\", \"values\": [\"Organic Search\"] }] or [{ \"property\": \"plan\", \"values\": [\"pro\"] }]. Entries are combined with AND.", + "items": { + "properties": { + "operator": { + "default": "is", + "description": "Match operator: is, is_not, contains, contains_not (default: is)", + "enum": [ + "is", + "is_not", + "contains", + "contains_not" + ], + "type": "string" + }, + "property": { + "description": "What to filter on: a built-in dimension (e.g. \"visit:channel\", \"visit:source\", \"event:page\") or a custom event property as its bare name (e.g. \"plan\" targets event:props:plan)", + "maxLength": 312, + "minLength": 1, + "type": "string" + }, + "values": { + "description": "One or more values to match the property against", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "property", + "values" + ], + "type": "object" + }, + "type": "array" +} - changed
Input schema / properties / site_id / descriptionPrevious value: -"Plausible site domain (e.g. example.com). Uses PLAUSIBLE_DEFAULT_SITE_ID if omitted."New value: +"Plausible site domain (e.g. example.com). Required." - changed
Input schema / requiredPrevious value: -[ - "date_range", - "dimension" -]New value: +[ + "site_id", + "date_range", + "dimension" +] - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
- Changed
get_conversions5 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / properties / property_filtersAdded value: +{ + "description": "Filter results by built-in dimensions or custom event properties, e.g. [{ \"property\": \"visit:channel\", \"operator\": \"is\", \"values\": [\"Organic Search\"] }] or [{ \"property\": \"plan\", \"values\": [\"pro\"] }]. Entries are combined with AND.", + "items": { + "properties": { + "operator": { + "default": "is", + "description": "Match operator: is, is_not, contains, contains_not (default: is)", + "enum": [ + "is", + "is_not", + "contains", + "contains_not" + ], + "type": "string" + }, + "property": { + "description": "What to filter on: a built-in dimension (e.g. \"visit:channel\", \"visit:source\", \"event:page\") or a custom event property as its bare name (e.g. \"plan\" targets event:props:plan)", + "maxLength": 312, + "minLength": 1, + "type": "string" + }, + "values": { + "description": "One or more values to match the property against", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "property", + "values" + ], + "type": "object" + }, + "type": "array" +} - changed
Input schema / properties / site_id / descriptionPrevious value: -"Plausible site domain (e.g. example.com). Uses PLAUSIBLE_DEFAULT_SITE_ID if omitted."New value: +"Plausible site domain (e.g. example.com). Required." - changed
Input schema / requiredPrevious value: -[ - "date_range" -]New value: +[ + "site_id", + "date_range" +] - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
- Changed
get_timeseries5 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / properties / property_filtersAdded value: +{ + "description": "Filter results by built-in dimensions or custom event properties, e.g. [{ \"property\": \"visit:channel\", \"operator\": \"is\", \"values\": [\"Organic Search\"] }] or [{ \"property\": \"plan\", \"values\": [\"pro\"] }]. Entries are combined with AND.", + "items": { + "properties": { + "operator": { + "default": "is", + "description": "Match operator: is, is_not, contains, contains_not (default: is)", + "enum": [ + "is", + "is_not", + "contains", + "contains_not" + ], + "type": "string" + }, + "property": { + "description": "What to filter on: a built-in dimension (e.g. \"visit:channel\", \"visit:source\", \"event:page\") or a custom event property as its bare name (e.g. \"plan\" targets event:props:plan)", + "maxLength": 312, + "minLength": 1, + "type": "string" + }, + "values": { + "description": "One or more values to match the property against", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "property", + "values" + ], + "type": "object" + }, + "type": "array" +} - changed
Input schema / properties / site_id / descriptionPrevious value: -"Plausible site domain (e.g. example.com). Uses PLAUSIBLE_DEFAULT_SITE_ID if omitted."New value: +"Plausible site domain (e.g. example.com). Required." - changed
Input schema / requiredPrevious value: -[ - "date_range" -]New value: +[ + "site_id", + "date_range" +] - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
4 tool updates
v0.5.1- First observed
compare_periods - First observed
get_breakdown - First observed
get_conversions - First observed
get_timeseries
TDQS
Scored across 4 tools
The four tools have mostly distinct purposes: timeseries for trends over time, breakdown for dimensional segmentation, conversions for goal performance, and period comparison for deltas. There is some conceptual overlap because conversion metrics can also appear in timeseries and breakdown queries, but the descriptions provide enough guidance to choose correctly.
All tool names use consistent snake_case verb_noun phrasing: get_timeseries, get_breakdown, get_conversions, and compare_periods. The get_* prefix dominates and compare_periods remains a clear action-oriented name, so the pattern is predictable.
Four tools are well-scoped for a focused analytics server. Each tool covers a distinct analysis pattern, and none appears redundant or trivial.
The surface covers core analytics workflows: time series, dimensional breakdowns, conversions, and period comparisons. Minor gaps exist around discovering available sites, goals, dimensions, or realtime data, but agents can likely work around them with the provided query tools.
Maintenance
Related MCP Connectors
MCP server for querying and analyzing data from ad platforms, analytics tools, and spreadsheets
Hosted MCP server for marketing agencies: connect Google Ads, GA4, Search Console, Meta, LinkedIn and more to Claude or ChatGPT. Pull reports and make changes via OAuth login, no setup.
Clamp Analytics MCP server: traffic, revenue, funnels, cohorts, errors, and search, for AI agents.
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP server that provides read access to Plausible Analytics data with natural-language date resolution, enabling users to query analytics like 'yesterday' or 'last week' without needing to know exact date formats.8MIT
- AlicenseAqualityAmaintenanceMCP server for Yandex Metrica analytics: query web analytics metrics, goals, conversions, and raw API data using natural language from AI clients like Claude and Cursor.8195 npm4MIT
- AlicenseBqualityFmaintenanceMCP server for Plausible Analytics, enabling querying of traffic, conversions, sources, and device breakdowns from any MCP-compatible AI assistant.1243 npmMIT
- FlicenseBqualityDmaintenanceA read-only stdio-based MCP server that provides tools to query Plausible Analytics data, including listing sites, fetching statistics with metrics/dimensions/filters, and getting realtime visitor counts.3-