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, 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:
Authorization: Bearer prv_YOUR_PRIVATE_KEYx-api-key: prv_YOUR_PRIVATE_KEYMost 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 for where keys live.
Connect a client
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 mcp add pug --url https://api.pugs.dev/mcp \
--bearer-token-env-var PUG_PRIVATE_KEYOr write it into ~/.codex/config.toml, or .codex/config.toml for a single
trusted project:
[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.
In ~/.cursor/mcp.json (global) or .cursor/mcp.json (this project only):
{
"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.
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:
{
"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}"
}
}
}
}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:
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 |
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 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, never an LLM-callable tool.get_deletion_requestis 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. Useexplore_eventsor a profile lookup instead.- Everything under
sdk.*anddashboard.*: ingest, identity and management are out of scope./mcpexposes theshared.*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_insightsover 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 - the same handler enforces them.
- Errors are Connect errors. A rejected argument comes back as
invalid_argumentnaming the field, an unknown profile asnot_found. See the error model. - A failed tool returns the whole error as JSON:
code,messageand thedetailsarray. The correlation id is in there, as agoogle.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).
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: the query spec behind
query_insights - Profiles API: profile lookups and erasure over HTTP
- Authentication: issuing and revoking private keys
- API overview: headers, base URLs and the Connect error model