Skip to main content
Glama
getsentry

plausible-mcp

by getsentry

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

get_timeseries

Traffic and conversion metrics over time (daily/weekly/monthly)

get_breakdown

Break down by page, source, country, device, browser, OS, UTM params

get_conversions

Goal conversion rates, optionally per-page

compare_periods

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. --header is variadic, so if it comes last it swallows the URL and the CLI fails with error: 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/internal

The 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 /internal at plausible-mcp.sentry.dev is Sentry-only and can't be used outside the org. To run /internal for a different organization, self-host and set ALLOWED_EMAIL_DOMAIN to your own domain. (The public /mcp bring-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 build

Add to Claude Code:

claude mcp add plausible -e PLAUSIBLE_API_KEY=your-key -- node /path/to/plausible-mcp/dist/index.js

Or 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 deploy

The worker exposes two endpoints:

  • /mcp — bring-your-own-key. Each user passes their own Plausible API key via the Authorization: Bearer header. 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 a Cf-Access-Jwt-Assertion header. The Worker verifies that header and queries a shared, server-side Plausible API key. Access is gated to the email domain(s) in ALLOWED_EMAIL_DOMAIN (defaults to sentry.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.

  1. 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.dev with no path.

    • ⚠️ Do not scope it to /internal. Once Managed OAuth is enabled, Cloudflare rejects any path with access.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 /internal route 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 with invalid_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.

  2. Carve /mcp back 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, domain plausible-mcp.sentry.dev path mcp, with Managed OAuth OFF, and a policy whose Action is Bypass with the selector Everyone.

    • Bypass ≠ Allow: an Allow policy still forces an interactive login (the client gets an HTML 302 to the login page and fails with Unexpected content type: text/html). Only Bypass lets the request through with no authentication, so the Worker's own Bearer-key check applies.

  3. 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 telemetry

    CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD are not secrets — a public JWKS URL and an application identifier — so they go in [vars] in step 4.

  4. Set the [vars] in wrangler.toml:

    • CF_ACCESS_TEAM_DOMAIN — https://<team>.cloudflareaccess.com, no trailing slash. Verifies the Cf-Access-Jwt-Assertion JWKS 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 (default sentry.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. Replace plausible-mcp.sentry.dev with your worker's hostname; keep the localhost entries if you use wrangler dev.

    • MCP_ALLOWED_ORIGIN_HOSTNAMES — comma-separated browser Origin hostnames allowed to call /internal. Non-browser clients do not send an Origin header.

  5. Deploy (npx wrangler deploy), then point an RFC 8707-capable MCP client at https://<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

Couldn't register … / add an OAuth Client ID

Connector callback isn't in Allowed redirect URIs

Add the exact callback (step 1); read the rejected redirect_uri from Zero Trust → Logs → Access

domain can not have a path if oauth is configured

Managed OAuth app scoped to a path

Rescope app 1 to the bare host (step 1)

/mcp: Unexpected content type: text/html

/mcp app policy is Allow, not Bypass

Set the app-2 policy Action to Bypass (step 2)

/mcp: OAuth 401 invalid_token

No /mcp bypass app; the whole-host OAuth app is gating it

Create app 2 (step 2)

Configuration

Environment Variable

Required

Default

Description

PLAUSIBLE_API_KEY

Yes (STDIO; Worker /internal)

—

Your Plausible API key (get one here). On the Worker this is the shared key for /internal; /mcp takes each user's own key via Bearer.

PLAUSIBLE_BASE_URL

No

https://plausible.io

URL of your Plausible instance (for self-hosted)

PLAUSIBLE_DEFAULT_SITE_ID

No

—

Default site domain so you don't have to pass site_id every call

CF_ACCESS_TEAM_DOMAIN

Yes (Worker /internal)

—

https://<team>.cloudflareaccess.com — verifies the Cf-Access-Jwt-Assertion JWKS + issuer. No trailing slash.

CF_ACCESS_AUD

Yes (Worker /internal)

—

The Access application's Application Audience (AUD) tag — checked against the assertion's aud.

SENTRY_DSN

No (Worker)

—

Sentry DSN for the Worker's own telemetry (wrangler secret put SENTRY_DSN). Unset disables Sentry — use your own DSN if you want telemetry on a self-hosted deployment.

ALLOWED_EMAIL_DOMAIN

No (Worker /internal)

sentry.io

Comma-separated email domain(s) allowed to sign in to /internal. Set to your own domain when self-hosting.

MCP_ALLOWED_HOSTNAMES

Yes (Worker)

—

Comma-separated hostname allowlist used to validate MCP Host headers.

MCP_ALLOWED_ORIGIN_HOSTNAMES

No (Worker /internal)

—

Comma-separated browser Origin hostnames allowed to call /internal. A present Origin is rejected when the list is empty.

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" targets event:props:plan).

  • operator — is, is_not, contains, contains_not (default is). event:goal supports only is and contains.

  • Multiple entries combine with AND, as do the page/goal shortcut parameters. Targeting event:page/event:goal from both a shortcut and property_filters in 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_breakdown a dimension of event:props:<name> (e.g. event:props:plan).

  • Filter by a custom property via property_filters with 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 mode

Testing with MCP Inspector

pnpm build
PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector node dist/index.js

LLM 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 model

Architecture

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.ts

PlausibleClient 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.io email (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 tools
compare_periodsCompare PeriodsA
Read-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 %).

ParametersJSON Schema
NameRequiredDescriptionDefault
goalNoFilter by goal name (e.g. Signup, Purchase)
pageNoFilter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)
metricsNoMetrics to return. Defaults vary by tool.
site_idYesPlausible site domain (e.g. example.com). Required.
period_aYesFirst date range, e.g. "2024-01-01,2024-01-07" or "7d"
period_bYesSecond date range, e.g. "2024-01-08,2024-01-14" or "7d"
property_filtersNoFilter 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

ParametersJSON Schema
NameRequiredDescription
deltasYesPer-metric change from period_a to period_b (absolute and percent)
period_aYes
period_bYes

TDQS

A4.2/5.0
Behavior4/5

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 the result contains aggregate values for each period plus absolute and percentage deltas, which is genuinely useful behavioral context beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences with zero filler, front-loaded with the core comparison purpose and ending with the return shape. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With a full schema, an output schema, and annotations present, the description only needs to convey purpose, usage context and return nature, all of which it does. Nothing essential for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the description adds no parameter-level detail (no date-format syntax, no metric defaults, no filter semantics). Baseline 3 is appropriate when the schema fully documents the seven parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

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.' The comparison semantics clearly distinguish it from siblings like get_timeseries and get_breakdown, which retrieve rather than diff two periods.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

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 context that tells the agent when this tool is the right pick. It stops short of naming exclusions or the sibling tools to prefer for single-period retrieval, so it is clear context without routing rules.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_breakdownGet BreakdownA
Read-onlyIdempotent

Break down metrics by a dimension: page, traffic source, country, device, etc. Use to find top pages, sources, or segment traffic.

ParametersJSON Schema
NameRequiredDescriptionDefault
pageNoFilter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)
limitNoMax results to return
metricsNoMetrics to return. Defaults vary by tool.
site_idYesPlausible site domain (e.g. example.com). Required.
dimensionYesDimension 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).
date_rangeYesDate range: "7d", "30d", "12mo", "month", "year", "all", or "YYYY-MM-DD,YYYY-MM-DD"
property_filtersNoFilter 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

ParametersJSON Schema
NameRequiredDescription
metricsYesMetric keys, in the order they appear in each row's `metrics` array
resultsYesOne row per dimension-value combination returned by Plausible
dimensionsYesDimension keys, in the order they appear in each row's `dimensions` array

TDQS

A3.6/5.0
Behavior3/5

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 no behavioral context beyond that — nothing about result limits, default metric selection, ordering, or how entries are combined.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences, front-loaded with the core action and followed by the use case. No filler, no repetition of the title.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists and all 7 parameters are fully described in the schema, so the description need not cover return values. It covers purpose and a use case adequately; only route-to-sibling guidance is absent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so every parameter including dimension enums and format examples is already documented in the schema. The description's dimension examples (page, traffic source, country, device) loosely map to schema values but add no syntax or format detail beyond it. Baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource (break down metrics by a dimension) and gives concrete example dimensions (page, traffic source, country, device) plus example goals (top pages, sources). It never names or contrasts its siblings (get_timeseries, get_conversions, compare_periods), so it stops short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Use to find top pages, sources, or segment traffic' implies the use case but gives no when-not conditions and points to no alternative tool. An agent must infer that breakdown-by-dimension is the right choice over timeseries or comparison, which the description does not address.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_conversionsGet ConversionsA
Read-onlyIdempotent

Get goal conversion rates and counts. Can break down by page to see which pages drive conversions.

ParametersJSON Schema
NameRequiredDescriptionDefault
goalNoFilter by goal name (e.g. Signup, Purchase)
pageNoFilter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)
site_idYesPlausible site domain (e.g. example.com). Required.
date_rangeYesDate range: "7d", "30d", "12mo", "month", "year", "all", or "YYYY-MM-DD,YYYY-MM-DD"
property_filtersNoFilter 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_pageNoIf true, shows conversion rate per page

Output Schema

ParametersJSON Schema
NameRequiredDescription
metricsYesMetric keys, in the order they appear in each row's `metrics` array
resultsYesOne row per dimension-value combination returned by Plausible
dimensionsYesDimension keys, in the order they appear in each row's `dimensions` array

TDQS

A3.5/5.0
Behavior3/5

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 and the bar is lower. The description adds only the page-breakdown capability and says nothing about result shape, pagination, or rate limits beyond what structured fields provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences, capability front-loaded with the optional breakdown as a secondary clause. No waste, though it is quite terse for a six-parameter analytics tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With annotations, a full output schema, and 100% schema description coverage, the description only needs to convey the tool's scope, which it does. Minor gaps around filtering/segmentation guidance are acceptable given the rich structured fields.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

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, breakdown_by_page) are already documented in the schema. The description adds no syntax or format detail beyond that, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource: goal conversion rates and counts, plus a page-breakdown capability. It distinguishes itself reasonably from siblings like get_timeseries and get_breakdown by scoping to conversions, though it never names an alternative directly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: use this when you need conversion metrics, optionally per page. There is no explicit when-to-use versus when-to-prefer get_breakdown or compare_periods, and no exclusions or prerequisites are stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_timeseriesGet TimeseriesA
Read-onlyIdempotent

Get traffic and conversion metrics over time for a site or specific page. Use to spot trends and changes around deploys.

ParametersJSON Schema
NameRequiredDescriptionDefault
goalNoFilter by goal name (e.g. Signup, Purchase)
pageNoFilter by page path. Exact match by default, use * as trailing wildcard (e.g. /blog*)
metricsNoMetrics to return. Defaults vary by tool.
site_idYesPlausible site domain (e.g. example.com). Required.
date_rangeYesDate range: "7d", "30d", "12mo", "month", "year", "all", or "YYYY-MM-DD,YYYY-MM-DD"
granularityNoTime bucket sizeday
property_filtersNoFilter 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

ParametersJSON Schema
NameRequiredDescription
metricsYesMetric keys, in the order they appear in each row's `metrics` array
resultsYesOne row per dimension-value combination returned by Plausible
dimensionsYesDimension keys, in the order they appear in each row's `dimensions` array

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint and openWorldHint, so safety and idempotency are covered. The description adds no behavioral context beyond that (no mention of pagination, granularity defaults, or result volume), and the output schema handles the return shape, so a baseline 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.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences, front-loaded with the primary purpose and followed by the usage cue. No filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only timeseries tool with a full output schema and 100% parameter coverage, the description covers purpose and one use case adequately. It is slightly thin on when to prefer it over compare_periods or get_breakdown, which keeps it from a 5.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% with rich inline docs for goal, page, metrics, date_range, granularity and property_filters. The description adds no parameter meaning beyond the schema, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource (get traffic/conversion metrics over time) plus the scope (site or specific page), which clearly separates it from get_breakdown and compare_periods. It does not explicitly name the sibling tools, so the differentiation is implied rather than stated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Use to spot trends and changes around deploys' gives one concrete use case, implying time-series analysis rather than aggregation. But there is no when-not guidance and no routing to siblings like compare_periods for period-over-period analysis, so usage is only partially covered.

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.

  1. 4 tool updatesv0.9.0
    • Changedcompare_periods8 fields changed
      • removedOutput schema / properties / deltas / additionalProperties / properties / absolute / anyOf
        Removed value: -[
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • addedOutput schema / properties / deltas / additionalProperties / properties / absolute / type
        Added value: +[
        +  "number",
        +  "null"
        +]
      • removedOutput schema / properties / deltas / additionalProperties / properties / percent / anyOf
        Removed value: -[
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • addedOutput schema / properties / deltas / additionalProperties / properties / percent / type
        Added value: +[
        +  "number",
        +  "null"
        +]
      • removedOutput schema / properties / period_a / properties / metrics / additionalProperties / anyOf
        Removed value: -[
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • addedOutput schema / properties / period_a / properties / metrics / additionalProperties / type
        Added value: +[
        +  "number",
        +  "null"
        +]
      • removedOutput schema / properties / period_b / properties / metrics / additionalProperties / anyOf
        Removed value: -[
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • addedOutput schema / properties / period_b / properties / metrics / additionalProperties / type
        Added value: +[
        +  "number",
        +  "null"
        +]
    • Changedget_breakdown4 fields changed
      • removedOutput schema / properties / results / items / properties / dimensions / items / anyOf
        Removed value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "number"
        -  }
        -]
      • addedOutput schema / properties / results / items / properties / dimensions / items / type
        Added value: +[
        +  "string",
        +  "number"
        +]
      • removedOutput schema / properties / results / items / properties / metrics / items / anyOf
        Removed value: -[
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • addedOutput schema / properties / results / items / properties / metrics / items / type
        Added value: +[
        +  "number",
        +  "string",
        +  "null"
        +]
    • Changedget_conversions4 fields changed
      • removedOutput schema / properties / results / items / properties / dimensions / items / anyOf
        Removed value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "number"
        -  }
        -]
      • addedOutput schema / properties / results / items / properties / dimensions / items / type
        Added value: +[
        +  "string",
        +  "number"
        +]
      • removedOutput schema / properties / results / items / properties / metrics / items / anyOf
        Removed value: -[
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • addedOutput schema / properties / results / items / properties / metrics / items / type
        Added value: +[
        +  "number",
        +  "string",
        +  "null"
        +]
    • Changedget_timeseries4 fields changed
      • removedOutput schema / properties / results / items / properties / dimensions / items / anyOf
        Removed value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "number"
        -  }
        -]
      • addedOutput schema / properties / results / items / properties / dimensions / items / type
        Added value: +[
        +  "string",
        +  "number"
        +]
      • removedOutput schema / properties / results / items / properties / metrics / items / anyOf
        Removed value: -[
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]
      • addedOutput schema / properties / results / items / properties / metrics / items / type
        Added value: +[
        +  "number",
        +  "string",
        +  "null"
        +]
  2. 4 tool updatesv0.7.2
    • Changedcompare_periods1 field changed
      • addedInput schema / properties / property_filters / items / properties / values / items / maxLength
        Added value: +1024
    • Changedget_breakdown1 field changed
      • addedInput schema / properties / property_filters / items / properties / values / items / maxLength
        Added value: +1024
    • Changedget_conversions1 field changed
      • addedInput schema / properties / property_filters / items / properties / values / items / maxLength
        Added value: +1024
    • Changedget_timeseries1 field changed
      • addedInput schema / properties / property_filters / items / properties / values / items / maxLength
        Added value: +1024
  3. 4 tool updatesv0.7.1
    • Changedcompare_periods5 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • addedInput schema / properties / property_filters
        Added 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"
        +}
      • changedInput schema / properties / site_id / description
        Previous 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."
      • changedInput schema / required
        Previous value: -[
        -  "period_a",
        -  "period_b"
        -]New value: +[
        +  "site_id",
        +  "period_a",
        +  "period_b"
        +]
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedget_breakdown9 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • addedInput schema / properties / dimension / anyOf
        Added 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"
        +  }
        +]
      • changedInput schema / properties / dimension / description
        Previous 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)."
      • removedInput schema / properties / dimension / enum
        Removed 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"
        -]
      • removedInput schema / properties / dimension / type
        Removed value: -"string"
      • addedInput schema / properties / property_filters
        Added 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"
        +}
      • changedInput schema / properties / site_id / description
        Previous 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."
      • changedInput schema / required
        Previous value: -[
        -  "date_range",
        -  "dimension"
        -]New value: +[
        +  "site_id",
        +  "date_range",
        +  "dimension"
        +]
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedget_conversions5 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • addedInput schema / properties / property_filters
        Added 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"
        +}
      • changedInput schema / properties / site_id / description
        Previous 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."
      • changedInput schema / required
        Previous value: -[
        -  "date_range"
        -]New value: +[
        +  "site_id",
        +  "date_range"
        +]
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
    • Changedget_timeseries5 fields changed
      • changedInput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
      • addedInput schema / properties / property_filters
        Added 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"
        +}
      • changedInput schema / properties / site_id / description
        Previous 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."
      • changedInput schema / required
        Previous value: -[
        -  "date_range"
        -]New value: +[
        +  "site_id",
        +  "date_range"
        +]
      • changedOutput schema / $schema
        Previous value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema"
  4. 4 tool updatesv0.5.1
    • First observedcompare_periods
    • First observedget_breakdown
    • First observedget_conversions
    • First observedget_timeseries

TDQS

A3.7/5.0

Scored across 4 tools

Disambiguation4/5

The four tools have largely distinct purposes: time series trends, dimension breakdowns, goal conversions, and period comparisons. However, get_conversions can break down by page, which overlaps slightly with get_breakdown, though descriptions clarify the focus on goals.

Naming Consistency5/5

All tool names follow a clear verb_noun pattern in snake_case: get_timeseries, get_breakdown, get_conversions, compare_periods. Minor deviation in verb (get_ vs compare_) is appropriate and predictable.

Tool Count4/5

Four tools are well within the typical 3-15 range and each covers a distinct analytical need. It is slightly thin for a full analytics surface (e.g., no site listing or realtime), but not problematic for a focused metrics server.

Completeness3/5

Core analytical queries are covered, but there are notable gaps such as listing available sites, retrieving site metadata, or accessing realtime/current visitors. For a read-only analytics server, these omissions could force agents to work around missing context.

Maintenance

ActivityActive
ResponsivenessWithin a week

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    MCP 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.
    8
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP 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.
    8
    103 npm
    4
    MIT
  • A
    license
    B
    quality
    F
    maintenance
    MCP server for Plausible Analytics, enabling querying of traffic, conversions, sources, and device breakdowns from any MCP-compatible AI assistant.
    12
    38 npm
    MIT
  • F
    license
    B
    quality
    C
    maintenance
    A 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
    -