ClickStream MCP server
ClickStream exposes a stateless Streamable HTTP MCP endpoint at:
https://einstein.clickstream.com/api/mcp
MCP access is available on Scale and higher plans. Tokens are separate from browser, mobile, and server ingestion keys. They are read-only, may be restricted to selected properties, and never expose email addresses, phone numbers, postal addresses, decrypted enrichment fields, or mobile/server install secrets.
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.
- Sign in at
einstein.clickstream.comand open Account (/settings). Scroll to the MCP tokens card. - 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.
- 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 401 Not a ClickStream MCP token, so a truncated paste fails closed instead of behaving strangely later.
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.
Use this configuration shape in an MCP client that supports Streamable HTTP — it is byte-identical to the block the dashboard hands you:
{
"mcpServers": {
"clickstream": {
"type": "http",
"url": "https://einstein.clickstream.com/api/mcp",
"headers": {
"Authorization": "Bearer cs_mcp_REPLACE_WITH_YOUR_TOKEN"
}
}
}
}
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:
| Property | Value |
|---|---|
| Transport | Streamable HTTP, stateless. No SSE, no session id, no server-side state between calls. |
| Methods | POST only. A GET returns 405 with Allow: POST and a plain JSON body (not JSON-RPC). |
| Protocol version | initialize advertises 2025-06-18. |
| Capabilities | { "tools": { "listChanged": false } }. There are no resources or prompts — resources/list and prompts/list return -32601. |
| Supported RPC methods | initialize, ping, notifications/initialized, tools/list, tools/call. Anything else is -32601 Method not found: <method>. |
notifications/initialized | Answered with HTTP 202 and an empty body — no JSON-RPC envelope. |
| Request body cap | 64 KiB. Larger bodies get HTTP 413 with JSON-RPC -32600 Request body exceeds the 64 KiB MCP limit, enforced while streaming, before parsing. |
| Tool annotations | Every tool carries readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false. |
| Result shape | tools/call returns both a content[0].text JSON string and a typed structuredContent object. Modern clients can read structuredContent directly. |
| Correlation | Every authenticated tools/call carrying a tool name gets an X-Request-ID response header — on success and on failure. |
| Caching | Every 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": "scale",
"property_type": "website"
}
]
}
}
}
plan is the property's stored tier id. Only MCP-entitled properties are ever returned, so it is always Scale or above. property_type is website, mobile_app, or server.
Developer tools
get_install_snippet(requiressite_id, optionalframework:html|npm|react|next|react-native) returns the property's realendpoint,api_key,key_type,tracking_domain,tracking_domain_verified,tracking_status, the generatedsnippet, anotesarray, and — forhtml— a separategtmblock with its caveat and snippet. Website keys may be returned because they already appear in page source; mobile and server keys are withheld, andapi_keycomes backnullwith anapi_key_withheldexplanation.verify_install(requiressite_id) returnsreceiving_events,first_seen,last_seen,visitor_records, threechecks(install_key,tracking_domain,events_arriving), and one concretenext_actionchosen by the first unmet precondition.visitor_recordscounts every visitor row including bots, so it deliberately disagrees withlist_people.get_signals_api,get_visitor_schema, andget_signals_exampledescribe the current Signals integration surface. They take no property and are stamped with anapi_version.get_signals_exampletakes aframework(vanilla|react|next|react-native) and an optionalgoal, which is flattened to one line and truncated to 120 characters before it is echoed into generated code.get_signal_catalog(requiressite_id) returns the canonical catalog version plus every signal's source, value type, freshness, confidence model, sensitivity, retention, supported platforms, surfaces, tier availability, and explicit state for one property. States areavailable,restricted,masked,stale,unavailable. The selected property's normalized platform is included and unsupported signals areunavailable, even when its tier would otherwise allow them. A property ID is required so an assistant cannot mistake a global schema for an enabled customer capability. MCP never establishes a password re-auth session or a visitor-scoped consent proof, so signals that depend on those stayrestrictedhere even when a human in the dashboard could reveal them.
Analytics tools
list_propertieslists the properties the token can read.list_segmentsreturns people-eligible interest, host, campaign, source, medium, and UTM-campaign facets. Exactly four interest rows and four host rows are pre-frozen — the top four of each by discovered people count. They carry a realpeoplenumber,count_is_exact: true, and an immutablecohort_snapshot_id. Every other row (interest and host rows 5+, and all campaign / source / medium / UTM-campaign rows) comes back withpeople: null,count_is_exact: false, andcohort_snapshot_id: null, because a count without its identity set invites cardinality-only reasoning. Testcohort_snapshot_id !== nullrather than row position, and select an unfrozen facet before quoting it.list_peoplereturns one cursor page without contact details. Rows are sorted most-recent-first; there is no sort control.count_peoplereturns a count without person rows.
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 all ten tools, including the ones not spelled out here.
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.
- Freeze. Call
list_peopleorcount_peoplewith nocohort_snapshot_id. When you selected a facet fromlist_segments, pass its preview asexpected_count; on an unsegmented call with noexpected_countthe 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 returnscohort_snapshot_id,behavioral_as_of, andbehavioral_since. A same-sized but different population is rejected rather than silently substituted. - Page. Pass
cohort_snapshot_id,behavioral_as_of,behavioral_since, and thenext_cursorfrom the previous page — plus the identical filter arguments. Acursorwithout acohort_snapshot_idis refused (People API cursor continuation requires cohort_snapshot_id). Continue untilhas_moreisfalse. Pages are capped at 100 rows and default to 25. - 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
Filtering by intent (high / medium / low) requires an exact intent-membership pass that runs in the background. When that pass has not finished, list_people and count_people return 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.
| Limit | Value | Scope | How it surfaces |
|---|---|---|---|
| Endpoint requests | 120 / minute | per token | HTTP 429, JSON-RPC -32002, Retry-After (1–60s) |
| Purchaser requests | 1,000,000 / fixed 31-day window | per purchaser, shared across every token and team member that can query its properties | HTTP 429, JSON-RPC -32002, Retry-After |
Segment discovery (list_segments) | 12 / minute | per user, shared across all of that user's tokens and their dashboard People screen | isError tool result — see below |
Cohort freezing (list_people / count_people calls that freeze a cohort) | 30 / minute | per user, same sharing | isError 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.
| HTTP | JSON-RPC | Message | Meaning / action |
|---|---|---|---|
401 | -32001 | Missing Authorization: Bearer <token> | No bearer header. Terminal. |
401 | -32001 | Not a ClickStream MCP token | Token is not cs_mcp_ + 64 hex. Terminal — usually a truncated paste. |
401 | -32001 | Invalid or revoked token | Unknown, revoked, or belongs to a suspended/deleted account. Terminal. |
403 | -32001 | MFA enrollment is required before this token can read account data | An MFA policy applies to the owner. Terminal until they enroll. |
403 | -32001 | AI assistant access (MCP) is available on Scale and above. Upgrade to enable it. | The token's properties exist but none is on an MCP-entitled plan. Terminal. |
403 | -32001 | This token has no readable sites | Scope no longer intersects the user's membership. Terminal. |
413 | -32600 | Request body exceeds the 64 KiB MCP limit | Shrink the request. Terminal. |
429 | -32002 | MCP token rate limit exceeded. Retry after the current minute window. | Retry after Retry-After seconds. |
429 | -32002 | MCP 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 | -32003 | Authentication service unavailable / Rate-limit service unavailable / MCP audit service unavailable | Fail-closed dependency outage. Retry-After: 5. Retryable. |
405 | — | This MCP endpoint accepts POST (Streamable HTTP) only. | Plain JSON with Allow: POST. |
200 | -32700 | Parse error | Body was not JSON. |
200 | -32600 | Invalid Request: … | Not a JSON-RPC 2.0 object, or a bad id/method. |
200 | -32601 | Method not found: <method> | Unsupported RPC method. |
200 | -32602 | Invalid params: … | Missing tool name, or non-object params / arguments. |
All 401/403 responses carry WWW-Authenticate: Bearer realm="ClickStream MCP".
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:
| Message | Cause | Action |
|---|---|---|
site_id <id> is not readable by this token — call list_properties first | Out-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 first | Omitted on a tool that requires it. | Supply it. |
No property <id> readable with this token | Unknown or out-of-scope id on a developer tool — the same message either way, on purpose. | Call list_properties. |
Unknown tool: <name> | Tool name is not in tools/list. Note this arrives as an isError result, not -32601. | Fix the name. |
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 integer | Bad expected_count. | Fix the argument. |
People API cohort_snapshot_id is malformed | Not seg_ + 24 hex. | Use the value the tool returned. |
People API cursor must be the exact snapshot continuation token from the previous page | Hand-built cursor. | Use next_cursor verbatim. |
People API cursor continuation requires cohort_snapshot_id | Paged 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 cohort | The 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 reads | A 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.