ClickStream MCP server

ClickStream exposes a stateless Streamable HTTP MCP endpoint at:

https://einstein.clickstream.com/api/mcp

MCP access is available on both Free and Paid packages within their request and property limits. Tokens are separate from browser, mobile, and server ingestion keys. They may be restricted to selected properties, and never expose email addresses, phone numbers, postal addresses, decrypted enrichment fields, or mobile/server install secrets.

Every tool is read-only with one exception: set_page_gates. That tool is advertised only on a token whose owner has explicitly enabled page-gate writes on it, per token, in the MCP settings panel. Tokens are read-only by default, and the endpoint exposes no tool that can grant the capability — a credential must not be able to widen its own authority.

Create a token

Minting requires a signed-in dashboard session — the mint route is session-authenticated, so an MCP token can never mint another one.

  1. Sign in and open Account → MCP tokens. "Account" is the /settings page. The card is not listed on that page by default: the #mcp fragment is what shows it, already open, so use the link rather than scrolling for it.
  2. Click New token. Give it a Name (max 80 characters — name it after where it will live, because the hint is all you get later). Optionally use Restrict to properties to pin the token to specific properties. Nothing selected means all current and future MCP-entitled properties you can access.
  3. Optionally use Restrict to IP addresses to pin the token to the networks it will be used from — see IP allowlist. Left empty, the token works from any address.
  4. Click Mint token, then copy the token from the one-time reveal. ClickStream stores the SHA-256 hash plus a non-usable hint — the last six characters, displayed as cs_mcp_…a1b2c3 — and cannot display the plaintext again. Lose it and you revoke and mint a new one.

A minted token is cs_mcp_ followed by exactly 64 lowercase hex characters. Anything that does not match that shape is rejected at the door with a 401 beginning Not a ClickStream MCP token, so a truncated paste fails closed instead of behaving strangely later. That message names the expected shape and links back to the mint panel, because it is the most common first-run failure and it appears inside your client, not the dashboard.

Credential issuance has its own allowance: 100 mints per purchaser per fixed 31-day window. Once it is spent the dashboard refuses further mints (mcp_credential_mint_allowance_exceeded) until the window rolls. Existing tokens keep working throughout.

Restrict a token by IP address

An MCP token is a static bearer: it has no origin binding, no expiry and no second factor, and it lives in a config file inside another application. Optionally pin one to the networks it will actually be used from, so a copied token is worth nothing anywhere else.

On the token's row in Account → MCP tokens, use the Any IP / IP-restricted control. One address or CIDR range per line; IPv4 and IPv6 are both accepted. You can also set it at mint time.

203.0.113.42
198.51.100.0/24
2001:db8::/32

Entries are validated when you save, not when a request arrives, and the whole save is rejected if any entry is malformed — the rejected entries come back named, with the reason. A bare address is stored as a single host (/32, or /128 for IPv6). Three rules are stricter than you may expect, each for the same reason (an entry that means something other than what you typed is worse than no entry):

An empty list means no restriction, and that is the state of every token minted before this feature existed. The dashboard shows it in words on each row (Usable from any IP address) rather than leaving it implied.

Enforcement happens on every request, before any of your data is read: a blocked request never causes a property, visitor or analytics read. The address used is the one Cloudflare observed at the edge (CF-Connecting-IP); X-Forwarded-For and similar client-settable headers are ignored entirely, because a caller can write whatever they like into them.

A blocked request is answered with exactly the message an unknown token gets — 401, Invalid or revoked token — with no hint that the token is otherwise valid. That is deliberate: a leaked token being tried from the wrong network should learn nothing. The consequence is that you cannot tell the two apart from the client either, so if a token you trust suddenly stops working after a network change, check your dashboard audit log: the denial is recorded there with the source address and the reason.

Two honest limits. This binds a network, not a person or a device — anything behind the same office NAT or VPN egress is inside the range. And it is only usable if your client runs from a stable address; an agent on a rotating consumer IP cannot be pinned this way.

Connect a client

The credential is a static bearer header, so the client has to support two things: Streamable HTTP and a per-server headers block in its config. There is no hosted connector and no OAuth flow — a client whose only remote-server path is "sign in with the provider" cannot attach a ClickStream token.

Before you leave the mint dialog, press Test this token. It runs a real tools/call against this endpoint with the token you just minted and lists the properties it can read. If that passes, any later failure is the config file, not the credential.

{
  "mcpServers": {
    "clickstream": {
      "type": "http",
      "url": "https://einstein.clickstream.com/api/mcp",
      "headers": {
        "Authorization": "Bearer cs_mcp_REPLACE_WITH_YOUR_TOKEN"
      }
    }
  }
}

Where that block goes, and the one place clients disagree:

ClientConfig fileTop-level key
Claude Desktopclaude_desktop_config.json (Settings → Developer → Edit Config)mcpServers
Claude Code.mcp.json in the project rootmcpServers
Cursor~/.cursor/mcp.json, or .cursor/mcp.json for one projectmcpServers
VS Code.vscode/mcp.json in the workspaceservers

VS Code reads servers, not mcpServers. Everything inside the object is identical; only the wrapper key changes. A block written under the key a client does not read is ignored in silence, which looks exactly like "the tools never appeared". The dashboard's config block has a client selector that emits the right wrapper and names the file, so copying from there skips this table entirely. Each client's own MCP documentation is the authority on its config path.

If the file already contains other servers, merge the clickstream entry into the existing object rather than replacing it.

Treat the token as a secret. Revoke it from the same panel if it is copied into the wrong client, logged, or otherwise exposed; revocation takes effect on the next call.

Authorization is re-evaluated on every call. The token's frozen property scope is intersected with current site membership and current plan entitlement, so removing a user from a property or downgrading the property takes effect without reissuing the token. Suspended/deleted accounts and accounts that have not satisfied a required MFA-enrollment policy are denied as well.

Protocol mechanics

The details an MCP client implementation has to get right:

PropertyValue
TransportStreamable HTTP, stateless. No SSE, no session id, no server-side state between calls.
MethodsPOST only. A GET returns 405 with Allow: POST and a plain JSON body (not JSON-RPC).
AuthenticationA static Authorization: Bearer cs_mcp_… request header, and nothing else. No OAuth, no client registration, no .well-known discovery document.
CORSNo Access-Control-Allow-* headers are sent, so this endpoint is not callable from browser page JavaScript on another origin. It is built for MCP clients, which are not subject to CORS.
Argument validationEach tool's published inputSchema is enforced before dispatch: required, additionalProperties: false, enum and type. Violations are -32602, not tool results.
Protocol versioninitialize advertises 2025-06-18.
Capabilities{ "tools": { "listChanged": false } }. There are no resources or prompts — resources/list and prompts/list return -32601.
Supported RPC methodsinitialize, ping, notifications/initialized, tools/list, tools/call. Anything else is -32601 Method not found: <method>.
notifications/initializedAnswered with HTTP 202 and an empty body — no JSON-RPC envelope.
Request body cap64 KiB. Larger bodies get HTTP 413 with JSON-RPC -32600 Request body exceeds the 64 KiB MCP limit, enforced while streaming, before parsing.
Tool annotationsEvery tool carries readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false.
Result shapetools/call returns both a content[0].text JSON string and a typed structuredContent object. Modern clients can read structuredContent directly.
CorrelationEvery authenticated tools/call carrying a tool name gets an X-Request-ID response header — on success and on failure.
CachingEvery response sends Cache-Control: no-store.

A raw call, if you want to test the endpoint before wiring a client:

curl -sS https://einstein.clickstream.com/api/mcp \
  -H "Authorization: Bearer cs_mcp_REPLACE_WITH_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"list_properties","arguments":{}}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{ "type": "text", "text": "{ \"properties\": [ ... ] }" }],
    "structuredContent": {
      "properties": [
        {
          "site_id": "site_3f2b9c14",
          "name": "Example Store",
          "domain": "example.com",
          "plan": "paid",
          "property_type": "website"
        }
      ]
    }
  }
}

plan is the property's normalized package id (free or paid). property_type is website, mobile_app, or server.

Developer tools

Page-gate tools

Page gates are a per-property compliance policy: "on these pages, do not collect / do not store / do not enrich". All three tools are owner-only — a property merely shared with the calling account is refused, because its compliance posture belongs to another tenant.

Enabling writes

set_page_gates does not appear in tools/list unless the token has page-gate writes enabled. Turn it on per token in Account → MCP tokens; it is off on every token by default, including tokens minted before the capability existed. Calling the tool without it returns -32602 with a message naming the setting.

What page gates do and do not guarantee

Gates hold against honest clients, stale cached SDK bundles, tampered or tag-manager-injected SDKs, and replayed or hand-crafted batches. They do not hold against an attacker who controls the request body: the page a gate matches on is client-declared, and the only server-observed provenance is the Origin/Referer hostname, which carries no path. Do not describe page gates to a customer as a guarantee against a hostile client. Every case where the page cannot be determined is treated as maximally restrictive — an unresolvable page applies everything the property gates anywhere, and an unreadable policy denies everything everywhere.

Analytics tools

list_people, count_people, and list_segments share the same people-eligible default population and 30-day window. Defaults for list_people / count_people: status: all (identified or partial), traffic: people (known bots suppressed), intent: all, range: 30d. tools/list carries the authoritative JSON Schema for every argument of every advertised tool, including the ones not spelled out here — and that schema is enforced all the way into nested arrays and objects, so an argument this page omits is still rejected if it is not in the schema.

Each list_people row is exactly:

{
  "person_id": "0f4c9a2e-7b31-4d58-9c60-2ab8e5f1d743",
  "site_id": "site_3f2b9c14",
  "identity_status": "identified",
  "traffic_classification": "human_confirmed",
  "person_eligibility": "human_eligible",
  "traffic_classification_source": "analytics_engine_bot_detection",
  "traffic_classification_reasons": ["..."],
  "intent_score": 88,
  "identity_confidence": "deterministic",
  "sessions": 3,
  "visits": 5,
  "devices": 2,
  "language": "en",
  "campaigns": ["summer-sale"],
  "first_seen": "2026-07-02T18:14:00.000Z",
  "last_seen": "2026-08-01T09:31:22.000Z"
}

person_id is the stable identity-graph key — an opaque id, not the physical visitor row id, so it does not change as the representative row changes. identity_status is identified | partial | anonymous; identity_confidence is deterministic | probabilistic | observed | null; traffic_classification is one of human_confirmed, human_likely, unknown, bot_likely, bot_confirmed, and any value outside those sets is normalized to null rather than passed through. Display names are dropped on purpose: MCP is a long-lived machine credential with no human re-authentication, so its identity surface is limited to a pseudonymous key plus metrics.

The exact-cohort protocol

Every list_people / count_people call answers about a frozen set of person ids, so a page 2 cannot describe different people than page 1.

  1. Freeze. Call list_people or count_people with no cohort_snapshot_id. When you selected a facet from list_segments, pass its preview as expected_count; on an unsegmented call with no expected_count the tool takes its own exact quote first and refuses to proceed if that quote is capped. Either way the call freezes the canonical person ids and returns cohort_snapshot_id, behavioral_as_of, and behavioral_since. A same-sized but different population is rejected rather than silently substituted.
  2. Page. Pass cohort_snapshot_id, behavioral_as_of, behavioral_since, and the next_cursor from the previous page — plus the identical filter arguments. A cursor without a cohort_snapshot_id is refused (People API cursor continuation requires cohort_snapshot_id). Continue until has_more is false. Pages are capped at 100 rows and default to 25.
  3. Expire. A frozen cohort is valid for 30 minutes from the moment it is frozen. The exact deadline comes back as filters_applied.cohortSnapshotExpiresAt (Unix milliseconds) on every page. Reads do not extend it. After it passes, the walk restarts — you must freeze a new cohort; a half-finished enumeration cannot be resumed.

Only the freeze step consumes the cohort-freezing rate limit below; subsequent pages carrying a cohort_snapshot_id do not.

Around the rows, list_people returns total_matching, returned, has_more, next_cursor, filters_applied, behavioral_as_of, behavioral_since, and cohort_snapshot_id. count_people returns the same envelope minus rows and pagination, with the total in count.

The exactness flags on these two tools are constants, not a signal to test. count_people always reports count_is_capped: false / count_is_exact: true, and list_people always reports total_is_capped: false / total_is_exact: true. If the underlying population is approximate, the tool fails with People API reported a capped result for an immutable exact cohort rather than returning a truncated set — the flags never carry the bad news. Testing them is only meaningful on list_segments rows, where count_is_exact distinguishes a frozen row from an unfrozen preview. If list_segments returns people: null, select the facet first; do not quote or reconstruct that unfrozen preview.

status: "building" is not an empty result

This is the one result shape most likely to be misread, so it is declared where an agent will actually see it: the list_people and count_people descriptions in tools/list both name it, the intent enum says it is the trigger, and the initialize instructions repeat the rule. You do not have to have read this page to handle it correctly.

Filtering by intent (high / medium / low) requires an exact intent-membership pass that runs in the background. On a property large enough that the inline accelerator cannot finish the pass in one call, the first intent-filtered call returns HTTP 200 with this shape:

{
  "status": "building",
  "count_is_exact": false,
  "count": null,
  "people": [],
  "cohort_snapshot_id": "seg_0123456789abcdef01234567",
  "candidate_count": 8500,
  "processed_count": 1200,
  "retry_after_ms": 60000,
  "message": "Exact person membership is still building; retry this tool with cohort_snapshot_id."
}

count: null with people: [] means "not ready yet", never "zero people". Branch on status === 'building' before you read count or people, or your assistant will confidently report an empty cohort.

Retry the same tool with the same arguments plus cohort_snapshot_id. retry_after_ms is the server's suggested wait and is currently 60,000 ms; processed_count / candidate_count show progress across retries. If a retry returns the generic backend-failure text instead of rows, the cohort no longer matches the request — it expired, or the filters differ — and the selection must be started again from list_segments.

Rate limits

Four ceilings apply, and only the first two ever surface as an HTTP 429.

LimitValueScopeHow it surfaces
Endpoint requests120 / minuteper tokenHTTP 429, JSON-RPC -32002, Retry-After (1–60s)
Purchaser requests1,000,000 / fixed 31-day windowper purchaser, shared across every token and team member that can query its propertiesHTTP 429, JSON-RPC -32002, Retry-After
Segment discovery (list_segments)12 / minuteper user, shared across all of that user's tokens and their dashboard People screenisError tool result — see below
Cohort freezing (list_people / count_people calls that freeze a cohort)30 / minuteper user, same sharingisError tool result — see below

There is no active-token, tool, workflow, or agent-count ceiling. Rotating or minting more credentials never resets or multiplies the request allowance, because the counter is keyed on the purchaser.

The two inner limits do not look like rate limits. They are enforced by the dashboard routes each tool wraps, and the MCP layer converts a tool failure into a successful HTTP 200 carrying result.isError. There is no 429, no Retry-After, and no machine-readable code:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [{ "type": "text", "text": "Tool failed: segments lookup failed (429)" }],
    "isError": true
  }
}

The 13th list_segments call in a minute produces exactly that. The 31st cohort-freezing call in a minute is worse: its message does not survive the error filter and degrades to Tool failed: A backend dependency failed while running this tool. Write your backoff against isError and the text, not against HTTP status.

Both inner windows are fixed 60-second buckets, so the ceiling resets at the next minute boundary rather than sliding.

MCP tool calls are not billable — there is no per-call charge and no usage meter attached to them.

Error taxonomy

Transport-level — the call did not run

These carry a real HTTP status and a JSON-RPC error object.

HTTPJSON-RPCMessageMeaning / action
401-32001Missing Authorization: Bearer <token>. Mint one at …/settings#mcpNo bearer header. Terminal.
401-32001Not a ClickStream MCP token (expected cs_mcp_ followed by 64 hex characters — check for a truncated paste). Mint a new one at …/settings#mcpToken is not cs_mcp_ + 64 hex. Terminal — usually a truncated paste.
401-32001Invalid or revoked token. Mint a new one at …/settings#mcpUnknown, revoked, belongs to a suspended/deleted account, or came from an address outside the token's IP allowlist — one message for all four, on purpose. Terminal. See IP allowlist.
403-32001MFA enrollment is required before this token can read account data. Enroll at …/settingsAn MFA policy applies to the owner. Terminal until they enroll.
403-32001AI assistant access (MCP) is unavailable for the selected property or allowance. Review …/billingThe token's properties exist but none is currently entitled or within its package allowance. Terminal.
403-32001This token has no readable sites. Review its property scope at …/settings#mcpScope no longer intersects the user's membership. Terminal.
413-32600Request body exceeds the 64 KiB MCP limitShrink the request. Terminal.
429-32002MCP token rate limit exceeded. Retry after the current minute window.Retry after Retry-After seconds.
429-32002MCP purchaser request allowance exceeded. Review account usage or wait for the next 31-day window.31-day allowance. Retry-After is the seconds left in the window.
503-32003Authentication service unavailable / Rate-limit service unavailable / MCP audit service unavailableFail-closed dependency outage. Retry-After: 5. Retryable.
405—This MCP endpoint accepts POST (Streamable HTTP) only.Plain JSON with Allow: POST.
200-32700Parse errorBody was not JSON.
200-32600Invalid Request: …Not a JSON-RPC 2.0 object, or a bad id/method.
200-32601Method not found: <method>Unsupported RPC method.
200-32602Invalid params: …Missing tool name, non-object params / arguments, an unknown tool name, or arguments that violate the tool's published schema. See below.

Each terminal auth message names where to go, because it surfaces inside your MCP client rather than in the dashboard. The … above stands for https://einstein.clickstream.com.

All 401/403 responses carry WWW-Authenticate: Bearer realm="ClickStream MCP". It has no resource_metadata parameter and there is no OAuth metadata document — the server does not speak OAuth, and advertising discovery it cannot complete would be worse than saying nothing.

Argument errors — the tool never ran

An unknown tool name and a call that violates a published inputSchema are answered with JSON-RPC -32602 before anything is dispatched. They are not tool results, and they never carry isError — a call that did not run cannot have failed inside a tool, and an agent must be able to tell "fix the arguments and retry" from "the backend is unwell, back off".

MessageCause
Invalid params: unknown tool "<name>". Call tools/list for the available tools.The name is not in tools/list. The echoed name is stripped of non-printable characters and truncated to 64.
Invalid params: <tool> requires "<field>". Call tools/list for its schema.A required argument was absent.
Invalid params: <tool> has no argument "<key>". Accepts: …Every schema is additionalProperties: false. A misspelled filter is refused, not dropped — dropping it would answer a narrow question with a wider number.
Invalid params: <tool>.<field> must be one of …An enum violation, e.g. intent: "HIGH". Previously this reached the query as a filter matching nothing.
Invalid params: <tool>.<field> must be a string / a number / an integer / a boolean.A type violation. limit is a number, not "25".
Invalid params: <tool>.<path>[<i>] must be one of …An enum violation inside an array, e.g. a misspelled capability in set_page_gates.rules[1].off[0]. Nested arrays and objects are validated to full depth, not just at the top level.
Invalid params: <tool>.rules[<i>] requires "off". Call tools/list for its schema.A required field missing from a nested object.
This MCP token cannot change page gates. …set_page_gates was called on a token without the page-gate write opt-in. It is also absent from that token's tools/list. Enable it per token at …/settings#mcp.

pattern is deliberately not enforced here: cohort_snapshot_id and cursor are values the tool itself minted, so a malformed one stays a tool-level error with a more useful message (see the next table).

Every rejected call is still written to the audit log and still returns an X-Request-ID.

Tool-level — HTTP 200 with isError: true

A failure inside a tool stays inside the tool result so the model can see it and adjust. The text is always Tool failed: <message>. Only a fixed allowlist of messages passes through verbatim; everything else — including cohort expiry, the cohort-freeze rate limit, and any raw dependency error — is replaced with A backend dependency failed while running this tool. so no internal detail leaks into a transcript.

Messages you will actually see, and what to do:

MessageCauseAction
site_id <id> is not readable by this token — call list_properties firstOut-of-scope property id. An out-of-scope site_id is an error, never a silent widening to the whole account.Call list_properties.
site_id is required — call list_properties firstSupplied but blank. A missing site_id is refused earlier as -32602, because the schema marks it required.Supply a real id.
No property <id> readable with this tokenUnknown or out-of-scope id on a developer tool — the same message either way, on purpose.Call list_properties.
No property <id> owned by this account — page gates are a compliance policy …A page-gate tool was called on a property that is shared with this account rather than owned by it. Readable and writable only by the owning account.Open the property in the dashboard, or ask its owner.
url is required — pass a page URL or path to check, … / url must be 2048 characters or fewerexplain_page_gates was given a blank or oversized url.Pass a real path, e.g. /checkout.
segments lookup failed (<status>)list_segments upstream failure. 429 here is the 12/min discovery limit.Back off on 429; otherwise retry once.
persons lookup failed (<status>)list_people / count_people upstream failure.Retry once, then re-freeze.
People API expected_count must be a non-negative integerBad expected_count.Fix the argument.
People API cohort_snapshot_id is malformedNot seg_ + 24 hex.Use the value the tool returned.
People API cursor must be the exact snapshot continuation token from the previous pageHand-built cursor.Use next_cursor verbatim.
People API cursor continuation requires cohort_snapshot_idPaged without the cohort id.Send both.
People API quote is capped and cannot seed an exact cohort / People API reported a capped result for an immutable exact cohortThe population is too large or too approximate to freeze exactly.Narrow the filter (a property, a shorter range, a facet).
People API … (other)An internal exactness contract was violated; the tool refuses to return a set it cannot vouch for.Re-freeze; if it repeats, report it with the X-Request-ID.
Server is not configured for internal readsA ClickStream-side configuration fault affecting list_people, count_people, and list_segments only. The six developer tools keep working.Not a client bug. Retry later.
A backend dependency failed while running this tool.Everything else — most commonly an expired 30-minute cohort or the 30/min cohort-freezing limit.Re-freeze the cohort; if it repeats immediately, back off a minute.

Include the X-Request-ID header value in any support report: it is the correlation id written to the audit log, and it is the only way to find the call again.

What is logged

Every authenticated tools/call request with a tool name receives an X-Request-ID. ClickStream commits a payload-free, append-only audit row before releasing tool output. The row contains that correlation ID, its own row id and creation timestamp, the account and token ids (user_id, key_id), the registered method (tools/call) and tool name, the authorized property scope, a result category (success, pending, validation_error, authorization_error, dependency_error, unknown_tool), and the duration. Tool arguments, outputs, bearer tokens, contact data, network addresses, and raw dependency errors are structurally excluded — the audit API does not accept them. Rows cannot be updated; a database trigger rejects it.

If the audit write is unavailable, the call fails closed with 503 and does not return the analytics result.

Separately, a request refused by a token's IP allowlist writes a row to the account audit log — the one visible in the dashboard, not the tool-execution log above. That row does record the source address, along with the token id and the reason, because it is the only place the real reason for the refusal exists: the caller is told only that the token is invalid.