Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ node_modules/
dist/
.env
.env.local
.dev.vars
*.tsbuildinfo
.sentry-build/
.dev.vars
.wrangler/

# Sentry Config File
Expand Down
36 changes: 24 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,26 @@ MCP server for Plausible Analytics — wraps the Plausible Stats API v2 (`POST /

Two entry points:
- **STDIO** (`src/index.ts`) — local use, reads `PLAUSIBLE_API_KEY` from env
- **Cloudflare Worker** (`src/worker.ts`) — multi-tenant remote, each user passes their own API key via `Authorization: Bearer` header
- **Cloudflare Worker** (`src/worker.ts`) — remote, with two endpoints:
- `/mcp` — bring-your-own-key: each user passes their own Plausible API key via `Authorization: Bearer` (header clients like Claude Code/Cursor).
- `/internal` — OAuth 2.1 server (via `@cloudflare/workers-oauth-provider`) for managed connectors (Cowork/Claude.ai). Federates login to Cloudflare Access as an upstream OIDC provider (`src/access-handler.ts` + vendored `src/workers-oauth-utils.ts`), gates on `@sentry.io`, and queries a shared server-side `PLAUSIBLE_API_KEY`. The Access id_token is verified by reusing `src/cf-access.ts`.

## Commands

```bash
bun install # Install dependencies
bun run build # TypeScript compilation (tsc)
bun run dev # Run locally via STDIO
bun run test # Run all tests (vitest)
bun run test:watch # Watch mode
bun run test -- __tests__/tools/get-timeseries.test.ts # Single test file
bun run deploy # Deploy to Cloudflare Workers (includes Sentry sourcemaps)
pnpm install # Install dependencies
pnpm build # TypeScript compilation (tsc)
pnpm dev # Run locally via STDIO (tsx)
pnpm test # Run all tests (vitest)
pnpm test:watch # Watch mode
pnpm test __tests__/tools/get-timeseries.test.ts # Single test file
pnpm deploy # Deploy to Cloudflare Workers (includes Sentry sourcemaps)

# LLM evals (requires ANTHROPIC_API_KEY)
ANTHROPIC_API_KEY=sk-... bun run eval
ANTHROPIC_API_KEY=sk-... pnpm eval

# Test with MCP Inspector
PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector bun run src/index.ts
pnpm build && PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector node dist/index.js
```

## Architecture
Expand All @@ -46,12 +48,22 @@ Shared Zod schemas and filter builders live in `src/schemas.ts`. Plausible filte

## Testing

Tests use Vitest with mocked `fetch` — no Plausible account needed. Test helpers are in `__tests__/tools/_helpers.ts` (`createMockClient`, `getToolHandler`). The `worker.ts` entry point is excluded from `tsconfig.json` (it uses Cloudflare-specific types).
Tests use Vitest with mocked `fetch` — no Plausible account needed. Test helpers are in `__tests__/tools/_helpers.ts` (`createMockClient`, `getToolHandler`). The Cloudflare-specific worker files (`worker.ts`, `env.ts`, `access-handler.ts`, `workers-oauth-utils.ts`) are excluded from the default `tsconfig.json` (they use Cloudflare/Workers globals, not Node), and are type-checked separately via `pnpm typecheck` (`tsconfig.worker.json`, which swaps in `@cloudflare/workers-types`).

## Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `PLAUSIBLE_API_KEY` | Yes (STDIO only) | Plausible API key |
| `PLAUSIBLE_API_KEY` | Yes (STDIO; also Worker `/internal`) | Plausible API key. On the Worker it's the shared key used by the OAuth `/internal` endpoint (the `/mcp` BYOK endpoint takes the user's key via Bearer). |
| `PLAUSIBLE_BASE_URL` | No | Custom Plausible instance URL (default: `https://plausible.io`) |
| `PLAUSIBLE_DEFAULT_SITE_ID` | No | Default site domain to avoid passing `site_id` every call |

Worker `/internal` (OAuth 2.1 via Cloudflare Access) also needs these secrets — see README "Setting up the `/internal` OAuth endpoint":

| Variable | Description |
|----------|-------------|
| `CF_ACCESS_TEAM_DOMAIN` | `https://<team>.cloudflareaccess.com` — verifies the Access id_token JWKS + issuer |
| `ACCESS_CLIENT_ID` / `ACCESS_CLIENT_SECRET` | Credentials from the Access SaaS/OIDC app |
| `ACCESS_AUTHORIZATION_URL` / `ACCESS_TOKEN_URL` | Access OIDC authorize/token endpoints (upstream) |
| `COOKIE_ENCRYPTION_KEY` | `openssl rand -hex 32` — signs approval/CSRF cookies |
| `OAUTH_KV` (binding) | KV namespace for OAuth tokens/grants/state |
17 changes: 9 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ Thanks for your interest in contributing!
```bash
git clone https://github.com/getsentry/plausible-mcp.git
cd plausible-mcp
bun install
pnpm install
```

## Running Tests

```bash
bun run test # All tests
bun run test:watch # Watch mode
bun run test:coverage # With coverage report
pnpm test # All tests
pnpm test:watch # Watch mode
pnpm test:coverage # With coverage report
```

Tests use [Vitest](https://vitest.dev) with mocked `fetch` — no Plausible account needed to run them.
Expand All @@ -34,17 +34,18 @@ Tests use [Vitest](https://vitest.dev) with mocked `fetch` — no Plausible acco
Requires an Anthropic API key:

```bash
ANTHROPIC_API_KEY=sk-... bun run eval
ANTHROPIC_API_KEY=sk-... pnpm eval
```

## Testing the MCP Server Locally

```bash
PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector bun run src/index.ts
pnpm build
PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector node dist/index.js
```

## Pull Requests

- Make sure `bun run test` passes
- Make sure `bun run build` compiles cleanly
- Make sure `pnpm test` passes
- Make sure `pnpm build` compiles cleanly
- Keep PRs focused — one feature or fix per PR
66 changes: 52 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ All tools are **read-only** and annotated with `readOnlyHint: true`.

### Remote (Hosted)

A hosted instance is available at **`https://plausible-mcp.sentry.dev`**. Each user provides their own Plausible API key as a Bearer token — no setup required.
A hosted instance is available at **`https://plausible-mcp.sentry.dev`**.

Add to Claude Code:
**With your own Plausible API key** (any user):

```bash
claude mcp add plausible --transport http --header "Authorization: Bearer YOUR_PLAUSIBLE_API_KEY" https://plausible-mcp.sentry.dev/mcp
Expand All @@ -45,20 +45,31 @@ Or add manually to your MCP client config (Claude Desktop, Cursor, etc.):
}
```

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

### Local (STDIO)

If you prefer to run it locally:

```bash
git clone https://github.com/getsentry/plausible-mcp.git
cd plausible-mcp
bun install
pnpm install
pnpm build
```

Add to Claude Code:

```bash
claude mcp add plausible -e PLAUSIBLE_API_KEY=your-key -- bun run src/index.ts
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`):
Expand All @@ -67,8 +78,8 @@ Or Claude Desktop (`claude_desktop_config.json`):
{
"mcpServers": {
"plausible": {
"command": "bun",
"args": ["run", "/path/to/plausible-mcp/src/index.ts"],
"command": "node",
"args": ["/path/to/plausible-mcp/dist/index.js"],
"env": {
"PLAUSIBLE_API_KEY": "your-key"
}
Expand All @@ -84,11 +95,37 @@ Deploy your own instance:
```bash
git clone https://github.com/getsentry/plausible-mcp.git
cd plausible-mcp
bun install
pnpm install
npx wrangler deploy
```

The worker is multi-tenant — each user passes their own Plausible API key via the `Authorization: Bearer` header. No shared secrets needed on the server.
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`** — OAuth 2.1 server for managed connectors (Cowork, Claude.ai). Federates login to Cloudflare Access and queries a shared, server-side Plausible API key.

#### Setting up the `/internal` OAuth endpoint

1. **Create the KV namespace** (stores OAuth tokens/grants/state) and paste the id into `wrangler.toml`:
```bash
npx wrangler kv namespace create OAUTH_KV
```
2. **Register a Cloudflare Access SaaS app** (Zero Trust → Access → Applications → *Add an application* → *SaaS* → **OIDC**):
- **Redirect URL**: `https://<your-worker-host>/callback`
- Scopes: `openid`, `email`, `profile`
- Add an Access **policy** restricting to your email domain (e.g. `@sentry.io`) and your identity provider (Google SSO).
- Copy the **Client ID**, **Client secret**, **Authorization endpoint**, and **Token endpoint**.
3. **Set the worker secrets**:
```bash
npx wrangler secret put CF_ACCESS_TEAM_DOMAIN # https://<team>.cloudflareaccess.com
npx wrangler secret put ACCESS_CLIENT_ID
npx wrangler secret put ACCESS_CLIENT_SECRET
npx wrangler secret put ACCESS_AUTHORIZATION_URL
npx wrangler secret put ACCESS_TOKEN_URL
npx wrangler secret put COOKIE_ENCRYPTION_KEY # openssl rand -hex 32
npx wrangler secret put PLAUSIBLE_API_KEY # shared key for /internal queries
```
4. **Deploy** (`npx wrangler deploy`), then point an OAuth MCP client at `https://<your-worker-host>/internal`.

## Configuration

Expand All @@ -115,24 +152,25 @@ This server wraps the [Plausible Stats API v2](https://plausible.io/docs/stats-a
## Development

```bash
bun install
bun run build # TypeScript compilation
bun run test # Run 53 unit + integration tests
bun run test:watch # Watch mode
pnpm install
pnpm build # TypeScript compilation
pnpm test # Run unit + integration tests
pnpm test:watch # Watch mode
```

### Testing with MCP Inspector

```bash
PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector bun run src/index.ts
pnpm build
PLAUSIBLE_API_KEY=your-key npx @modelcontextprotocol/inspector node dist/index.js
```

### LLM Evals

Verifies Claude picks the right tool for natural language analytics questions:

```bash
ANTHROPIC_API_KEY=sk-... bun run eval
ANTHROPIC_API_KEY=sk-... pnpm eval
```

## Architecture
Expand Down
Loading
Loading