---
title: "MCP server"
description: "Connect Claude, Cursor or any MCP client to your Pug project - twelve read-only analytics tools behind a private API key."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.pug.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP server

Pug serves its read-only analytics API as a **Model Context Protocol** server at
`/mcp`, so an agent can answer questions about your product's data (run an
insight, browse the raw event stream, look up one user) without you writing the
query. Twelve tools, one project, no writes.

The endpoint is a thin adapter, not a second API: every tool call is replayed
through the same Connect handlers an HTTP request hits, so validation,
authentication and authorization behave identically. Anything the [Insights](/api/insights),
[Profiles](/api/profiles) or Activity services refuse over HTTP, they refuse here.

## Endpoint

| Environment | URL |
|-------------|-----|
| Pug Cloud | `https://api.pugs.dev/mcp` |
| Self-hosted / local | `http://localhost:3000/mcp` |

Transport is **streamable HTTP** in stateless mode - no stdio bridge, so your
client needs remote-server support. Both `/mcp` and `/mcp/` are served, so a
trailing slash from an ingress or a pasted URL is fine.

Every reply comes back on the POST as JSON. The spec's optional standalone SSE
stream (`GET /mcp`) is refused with `405` - stateless means there is no session
for it to attach to. Clients that treat that stream as optional, which is all of
the ones above, are unaffected.

## Authentication

`/mcp` accepts a **private key only**. Send it either way:

```http
Authorization: Bearer prv_YOUR_PRIVATE_KEY
```

```http
x-api-key: prv_YOUR_PRIVATE_KEY
```

Most MCP clients can only attach an `Authorization` header, so Pug normalises the
`Bearer prv_...` form into the `x-api-key` the rest of the API expects. Reach for
whichever your client makes easy.

The key resolves the project, so there is no project ID in the config and the
agent sees exactly one project's data. Public keys (`pub_...`) and dashboard JWTs
are both rejected with `401` - a public key is extractable from client code, and
a JWT would widen the endpoint from one project to every project its holder can
read. See [Authentication](/get-started/authentication) for where keys live.

> **Keep the key out of the config file**
>
> `.cursor/mcp.json`, `.vscode/mcp.json` and a project's `.codex/config.toml` live
> in the repo and get committed. None of them needs the key inline: Cursor
> interpolates `${env:VAR}`, VS Code prompts for a `${input:...}`, and Codex reads a
> named environment variable - all three are used below. Issue a **dedicated key
> per client** under Settings > API Keys so you can revoke one without breaking the
> others.

## Connect a client

### Claude Code

```bash
claude mcp add --transport http pug https://api.pugs.dev/mcp \
  --header "Authorization: Bearer prv_YOUR_PRIVATE_KEY"
```

Run `/mcp` inside Claude Code to confirm the server connected and to list its
tools.

This writes to your own local config. Adding `--scope project` instead writes
`.mcp.json` in the repo, which would commit the key - use an environment
variable there, or keep the default.
### Codex

```bash
codex mcp add pug --url https://api.pugs.dev/mcp \
  --bearer-token-env-var PUG_PRIVATE_KEY
```

Or write it into `~/.codex/config.toml`, or `.codex/config.toml` for a single
trusted project:

```toml
[mcp_servers.pug]
url = "https://api.pugs.dev/mcp"
bearer_token_env_var = "PUG_PRIVATE_KEY"
```

Codex sends that variable's value as `Authorization: Bearer ...`. There is no
inline token field (a literal key in the config is rejected), so export
`PUG_PRIVATE_KEY` before launching Codex.
### Cursor

In `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (this project only):

```json
{
  "mcpServers": {
"pug": {
  "url": "https://api.pugs.dev/mcp",
  "headers": {
    "Authorization": "Bearer ${env:PUG_PRIVATE_KEY}"
  }
}
  }
}
```

Cursor resolves `${env:...}` from the environment it was launched with, so export
`PUG_PRIVATE_KEY` in your shell profile rather than pasting the key here.
### VS Code

In `.vscode/mcp.json`. The `inputs` entry makes VS Code prompt for the key once
and store it itself, so nothing secret lands in the file:

```json
{
  "inputs": [
{
  "type": "promptString",
  "id": "pug-private-key",
  "description": "Pug private API key",
  "password": true
}
  ],
  "servers": {
"pug": {
  "type": "http",
  "url": "https://api.pugs.dev/mcp",
  "headers": {
    "Authorization": "Bearer ${input:pug-private-key}"
  }
}
  }
}
```
### Any other client

Anything that speaks streamable HTTP and can set a request header works. Point it
at `https://api.pugs.dev/mcp` and add `Authorization: Bearer prv_...`.

To check the URL and key without a client, ask the server for its tool list. The
endpoint is stateless, so this works with no prior handshake:

```bash
curl -X POST https://api.pugs.dev/mcp \
  -H "Authorization: Bearer prv_YOUR_PRIVATE_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

A healthy response lists all twelve tools. `401` means the credential was refused;
check the `prv_` prefix and that the key has not been revoked. `400` with a
complaint about `Accept` means that header is missing a half: it must offer
**both** `application/json` and `text/event-stream`, even though the reply comes
back as JSON.

The server ships its own guidance to the agent at initialize, so a client that
surfaces MCP instructions already knows to discover the schema before querying,
and which tools are project-wide versus per-user.

## Use with Claude connectors

Claude's hosted surfaces (claude.ai, Desktop and mobile) have no config file to
edit. They connect through a **custom connector** instead: add
`https://api.pugs.dev/mcp` under **Add custom connector**, which lives in
Organization settings > Connectors for a Team or Enterprise owner, and in
Customize > Connectors otherwise.

The credential goes in that dialog's **Request headers** section. Pug's key is a
fixed credential rather than an OAuth identity, so this is the path - pick
`x-api-key` from the header list and paste the key on its own:

| Header | Value |
|--------|-------|
| `x-api-key` | `prv_YOUR_PRIVATE_KEY` |

> **Claude sends the header value verbatim**
>
> It adds no scheme of its own. Under `Authorization` you would have to type
> `Bearer ` and then the key, space included; entering the bare key there sends
> `Authorization: prv_...`, which has no bearer scheme to parse and comes back
> `401`. `x-api-key` carries no scheme to get wrong, which is why it is the one to
> choose.
>
> Request-header authentication is in **beta** on Anthropic's side and still
> rolling out. If the section is not in your dialog yet, ask Anthropic for access;
> there is nothing to change on the Pug end.

A request header is stored on the connector, not on a person, so everyone who can
use that connector shares one key and reads the same project - and revoking the
key disconnects all of them at once. For a team looking at its own analytics that
is usually the intent; it is not a way to give each person their own scope.

Pug is not in the Claude connectors directory. A directory listing requires
OAuth 2.0, and `/mcp` authenticates with a static private key.

## Tools

### Discovery: call these first

Filter schemas tell the agent which event kinds and property keys actually exist
in your project, which is what keeps a query from being invented out of thin air.

| Tool | Returns |
|------|---------|
| `get_insights_filter_schema` | Event kinds and the property keys/types available to filter and break down by |
| `get_insights_property_values` | The observed values for one property key (e.g. every country seen for `$country`) |
| `get_activity_filter_schema` | Alias of `get_insights_filter_schema` |
| `get_activity_property_values` | Alias of `get_insights_property_values` |

The `activity` pair is **not** a differently-scoped schema: both services hand the
request to the same underlying call with the same arguments, so the two return
identical data. They exist as four tools because each proto package declares the
concept. Call whichever the agent reaches for.

### Project-wide questions

| Tool | Returns |
|------|---------|
| `query_insights` | The main analysis tool: trends, funnel, retention, segmentation, user flow (Sankey), top-K and map, chosen by `insight_type`. Carries its own time range and granularity. See [Insights API](/api/insights) for the full spec |
| `explore_events` | A paginated, filterable page of raw events across all users. Does not resolve aliases, so a merged anonymous id appears under the id that sent the event |

### One user at a time

Start with a lookup:

| Tool | Returns |
|------|---------|
| `get_profile` | One profile by its Pug profile id |
| `get_profile_by_external_id` | One profile by the `external_id` your application assigned |

The three per-user reads then take that profile's `distinct_id`. Each describes a
**single** user and cannot answer an aggregate question - `query_insights` is for
those.

| Tool | Returns |
|------|---------|
| `get_activity_feed` | That user's events, newest first, filterable by session, time range and properties. Resolves aliases |
| `get_activity_heatmap` | Per-day event counts for that user (last 60 days when no time range is given) |
| `get_profile_stats` | First/last seen, total events, device, browser and location from the latest event, plus profile properties. Resolves aliases, so events from before the user identified are included |

### Compliance

| Tool | Returns |
|------|---------|
| `get_deletion_request` | The status of a previously submitted erasure request: the DSAR audit trail |

## What is deliberately not exposed

The tool surface is a curated subset, not everything the API can do:

- **Profile erasure** (`Delete`, `DeleteDataSubject`): GDPR/DPDP erasure is
  irreversible and reaches the events table and every derived rollup. It stays a
  deliberate human action through the dashboard or the [Profiles API](/api/profiles),
  never an LLM-callable tool. `get_deletion_request` is read-only and remains
  available so an agent can report on a request someone else submitted.
- **`SegmentUsers`**: the drill-down insight is still in progress; it is served
  as a Connect RPC but held off the tool surface until it lands.
- **`List` (profiles)**: server-streaming, which MCP tools do not model. Use
  `explore_events` or a profile lookup instead.
- **Everything under `sdk.*` and `dashboard.*`**: ingest, identity and
  management are out of scope. `/mcp` exposes the `shared.*` read surface only,
  which is why it is safe to hand an agent.

The exposed set is pinned in the server: a generated tool with no policy entry,
or a policy entry with no matching tool, fails startup rather than shipping a
surprise tool. Renaming an RPC upstream breaks the build, not your agent.

## Limits and errors

- **Two minutes per tool call.** A wide `query_insights` over a long window is
  the one that will hit it; narrow the time range or the breakdown.
- **Time-range caps by granularity** apply exactly as they do on the
  [Insights API](/api/insights) - the same handler enforces them.
- **Errors are Connect errors.** A rejected argument comes back as
  `invalid_argument` naming the field, an unknown profile as `not_found`. See the
  [error model](/api#error-model).
- **A failed tool returns the whole error as JSON**: `code`, `message` and the
  `details` array. The correlation id is in there, as a `google.rpc.RequestInfo`;
  quote it in a support request.

## Self-hosting

`/mcp` is mounted on every Pug deployment with no configuration to enable - if
your server is up, the endpoint is up, on the same host and port as the rest of
the API (`3000` by default, see [Configuration](/self-hosting/configuration)).

Behind a reverse proxy, make sure `/mcp` is passed through and that the
`Authorization` header survives the hop. Stripping it is the usual cause of a
server that connects from `localhost` but 401s in production.

## Further reading

- [Insights API](/api/insights): the query spec behind `query_insights`
- [Profiles API](/api/profiles): profile lookups and erasure over HTTP
- [Authentication](/get-started/authentication): issuing and revoking private keys
- [API overview](/api): headers, base URLs and the Connect error model

Source: https://docs.pug.sh/mcp/index.mdx
