vTilt
By roleFoundersKnow what to fix firstMarketersSee what happens after the clickCMOsSee what your budget really buys
By businessSaaSTurn signups into active usersEcommerceSee why shoppers abandon cartsAgenciesProve your work to clientsEnterpriseOne customer record for every team
MeasureWeb AnalyticsFind where buyers leaveSession ReplaySee why they left, not just that they didAdsKnow what paid visitors do nextIntegrationsOne snippet feeds your stack
ThinkPeopleOne profile per customerCustomer MemoryvTilt remembers every visitorSite KnowledgeAnswers and audits from your own pages
ActAsk AIAnswers from real behaviorAI ChatChat that knows the visitorEmail CampaignsEmail from real visits
PricingWhy vTiltDocs
Docs / MCP server
Getting Started
OverviewInstallInitializeIdentify usersTrack eventsLogout & resetVerify eventsCommon mistakes
Guides
Event forwardingReverse proxyRealtime dashboardSite knowledge
Frontend frameworks
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus
Backend frameworks
NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails
Backend languages
PythonPHPRubyElixirGoJava.NET / C#Rust
Stack guides
Vue + PHP
API Reference
Browser SDK
Script bundlesAutocaptureWeb VitalsSession recordingChat widgetFeature readinessRemote configurationDebug logging
Node SDK
Install & setupCapture, identify & aliasContext & shutdownGlobal properties & opt-outError tracking
MCP server
Guides
OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceSite knowledgeGoogle AdsMeta Ads
Client setup
CursorClaude DesktopVS CodeCodex

Getting Started

OverviewInstallInitializeIdentify usersTrack eventsLogout & resetVerify eventsCommon mistakes

Guides

Event forwardingReverse proxyRealtime dashboardSite knowledge

API Reference

MCP server

OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceSite knowledgeGoogle AdsMeta Ads
On this page

On this page

  • Documentation map
  • Toolsets — grouping the tool surface
  • Supported tools
  • What you can ask
  • How it works
  • Connect a client
  • Pinning a project
  • Switching projects conversationally
  • Parallel chats on one MCP config
  • Querying with VQL
  • What VQL is
  • Tools
  • $autocapture clicks in tool responses
  • Permissions
  • Example: pageviews per day, last 14 days
  • Error envelope
  • Searching the docs from your agent
  • Writing data
  • Limits and errors
  • Selecting toolsets
  • Next steps
DocsMCP serverGuidesOverview

MCP server

Last updated July 23, 2026

Connect Claude, Cursor, ChatGPT and other AI clients to your vTilt project. Ask "how many active users last week?" and your agent calls vTilt MCP tools to answer.

The vTilt MCP server lets your AI client — Cursor, Claude Desktop (via mcp-remote), VS Code, ChatGPT, Codex, custom agents — talk directly to your vTilt project data over the Model Context Protocol. The agent calls tools like get_kpis, list_persons, run_query, or query_ai_memory; vTilt runs them with the same privileges your dashboard uses, scoped to whatever project you've pinned. Almost every tool is read-only; update_project is the only core write tool today (Google Ads adds its own write surface), and only credentials minted with explicit write access can see or invoke it.

Warning

Tool names changed in June 2026. Every tool was renamed to a consistent verb_object snake_case convention (for example kpis-get → get_kpis, query-run → run_query, google-ads-search → google_ads_search) and there are no aliases — old names will fail. Tools are now also grouped into toolsets that you discover and enable on demand (see Toolsets). If you have saved prompts, ?tools= filters, or MCP configs that reference the old names, update them.

#Documentation map

Use the sidebar Guides and Client setup groups the same way as this table: concepts first, then wire your editor.

TopicPage
Personal API keys, headers, errorsAuthentication
Browser OAuth (Claude, ChatGPT, Cursor, …)OAuth
Slash-command prompts (/vtilt:…)Agent skills (prompts)
AI memory, VQL :embed(), query_ai_memory, get_person_memoryAI intelligence
Indexed website crawl, search_site_knowledge, site VQL tablesSite knowledge
CursorCursor
Claude DesktopClaude Desktop
VS CodeVS Code
ChatGPT / CodexCodex

Tip

New here? Authentication → pick your client setup guide. Prefer sign-in with your vTilt account instead of copying a long key? Start with OAuth.

#Toolsets — grouping the tool surface

To keep the tool list small and focused (and to scale as we add product surfaces like Google Ads), tools are grouped into toolsets. A fresh connection boots with only the always-on and default toolsets — roughly a dozen tools — and everything else is one enable_toolset call away. This mirrors the dynamic-toolset model used by GitHub's and other large MCP servers: a small, self-describing default surface that an agent expands on demand.

ToolsetWhen you see itTools
metaalways onlist_toolsets, get_toolset, enable_toolset
contextalways onget_guide, get_data_model, get_context, list_projects, switch_project
analyticsdefaultget_kpis, get_attribution, list_recent_events, list_event_types
querydefaultrun_query, validate_query, get_query_schema
docsdefaultsearch_docs
personson demandlist_persons, get_person
recordingsauto when replay onlist_recordings
campaignon demandlist_campaigns, get_campaign
event-destinationson demandlist_event_destinations, get_event_destination
intelligenceauto when AI memory onquery_ai_memory, get_person_memory
site-knowledgeauto when site knowledge onsearch_site_knowledge, get_site_page, get_site_page_screenshots, list_site_pages, get_site_overview, list_page_versions, get_page_version, diff_page_versions, get_site_elements, get_site_business_context
projecton demandget_project, get_project_config, update_project
google-adsauto when Google Ads connected13 core Google Ads read/mutate tools
google-ads-shortcutson demand (pin only)16 typed Google Ads write shortcuts

Discover-then-enable flow — the three meta tools are always present:

  1. list_toolsets — every toolset, its description, tool count, and whether it's currently enabled.
  2. get_toolset { toolset } — preview a toolset's tools (names + descriptions + input schemas) without enabling it.
  3. enable_toolset { toolset } (or { toolsets: [...] }) — enable one or more for this session. The tools are returned inline as a preview and the enabled set is persisted in your MCP session (Redis, 30-day TTL).

Important

enable_toolset only makes tools callable once your client re-fetches tools/list. Cursor, Claude.ai, and ChatGPT cache the tool catalogue and do not re-list mid-conversation, so a freshly enabled tool is rejected as "tool not found". Two paths avoid this: the feature toolsets (recordings, intelligence, site-knowledge, google-ads) appear automatically the moment your pinned project has that feature connected — no enable step — and the ?toolsets= URL pin fixes the surface for the whole connection (see Selecting toolsets). Use enable_toolset for the remaining on-demand toolsets (persons, campaigns, event destinations, project) on clients that support live tool refresh.

Tip

If a capability you expect isn't in tools/list, it's almost certainly behind a disabled toolset, or a feature that isn't connected on the active project — call list_toolsets, connect the feature, or pin the toolset on the URL. Don't assume it's unsupported.

#Supported tools

The server defines 35 core tools across the toolsets above, plus the Google Ads toolsets (13 core + 16 typed write shortcuts) that become callable once a Google Ads account is connected. Each tool exposes both an input schema (the arguments it accepts) and an output schema (the shape of its successful result) over MCP tools/list, so your agent can reason about the data it's about to receive without first making a probe call. Successful responses are validated server-side against the output schema before they leave vTilt — if a tool ever returns a payload that doesn't match its declared shape, the call surfaces as an error rather than a malformed result.

The catalog spans orientation (get_guide, get_data_model, get_context), analytics & events, persons, session recordings, campaigns, event destinations, the open-ended VQL query layer (including AI memory tables when enabled), natural-language AI intelligence helpers, site knowledge (semantic search, page reads, change history + impact attribution, and structured element inventory over your indexed website), the public-docs reader (search_docs), project metadata + configuration + writes + navigation, and — when connected — Google Ads account reads and writes. switch_project, feature-gated tools, and write tools are omitted from the list when they cannot apply (see the notes after the table).

Tip

On any non-trivial task, point your agent at get_guide first and get_context (includes data_model_hints for traffic sources). For "where did visitors come from?" / Google Ads vs direct, use get_attribution — not get_kpis top_sources (gclid-only paid clicks appear as "direct" there).

ToolToolsetDescription
get_guidecontextThe platform mental model for agents — the four-entity data model, a decision tree, the full tool map, and worked end-to-end examples. No arguments, always available. Call it first when unsure which tool to use or how vTilt data is organised.
get_data_modelcontextWhere each kind of data lives (events vs persons vs AI memory vs derived sessions): useFor / notFor, key property paths with ready-made VQL JSONExtractString fragments, and an attribution block ($initial_* on persons.properties, not events.$referrer). No arguments, always available. Call before writing SQL for traffic sources or "where users came from".
get_contextcontextSituational awareness in one call: the active project + how it was pinned, your permissions, whether your credential can write, and which features (replay / AI memory / chat) are enabled. Always available.
list_projectscontextEnumerate the projects this credential can read, plus the active project id and how it was pinned (header / query / session / default). The discovery tool agents reach for when the user names a project conversationally or you need to confirm which project the next tool will target. Always available.
switch_projectcontextPin a different project for this MCP lane's Redis session. Accepts project_id (UUID, preferred) or project_name — matched fuzzily (exact → prefix → substring, case-insensitive); when no name matches, the error lists the closest project names so the agent can retry. Omitted from tools/list when you have only one accessible project, or when the project is fixed with x-vtilt-project-id / ?project_id=.
get_kpisanalyticsHeadline volume KPIs (visits, pageviews, bounce rate, top countries/browsers) for a date range. top_sources is session-entry referrer — Google Ads without an HTTP referrer appears as "direct". Use get_attribution for acquisition. Requires analytics:read.
get_attributionanalyticsFirst-touch traffic source breakdown from persons.properties ($initial_gclid, $initial_gad_source, $initial_referring_domain). Splits Google Ads (Search vs Search Partners vs gclid-only) from true direct. Optional date_from / date_to filter by person first-seen. Requires person:read.
list_recent_eventsanalyticsNewest-first event feed. Optional filters: event_name, distinct_id, since. When since is omitted the feed is bounded to the last 30 days (for performance); pass an explicit since timestamp to look further back. Parses properties as JSON; for $autocapture, adds synthetic properties.$element from stored $elements_chain. Adds a synthetic campaign_params object per row (gclid / utm_* / fbclid for that event, backfilled from $current_url). Requires analytics:read.
list_event_typesanalyticsDiscover the project's event vocabulary — distinct event names with per-name counts and last-seen timestamps. The right first call when an agent doesn't yet know which custom events your SDK emits. Optional substring filter and since. Requires analytics:read.
run_queryqueryExecute a VQL query (a single ClickHouse SELECT against a curated catalog of analytics tables) and return the rows as JSON. Tenant scoping and read-only safety are enforced server-side. When rows include event + properties, responses add synthetic properties.$element (from $elements_chain) and a campaign_params object (click / UTM params). Non-blocking attribution warnings[] flag common mistakes (e.g. aggregating event-level gclid / $referrer). campaign_params is a read-time projection — never reference it in VQL.
validate_queryqueryDry-run a VQL query (parse + catalog + permissions) without touching ClickHouse. Returns structured { code, message, hint } errors so agents can self-correct before paying the round-trip.
get_query_schemaqueryReturn the VQL virtual-table catalog the caller can query: table names, column lists, descriptions, required permissions. Tables the caller cannot access are omitted. Pair with get_data_model when you need to know which entity owns a field (especially $initial_* attribution keys).
search_docsdocsSearch the vTilt public documentation. Returns matched pages with title, URL, description, and a body snippet — useful when an agent needs to remember a config option, an SDK signature, or an integration step. Available to every authenticated MCP client.
list_personspersonsPaginated list of identified persons. Optional substring filter against name/email/id. Requires person:read.
get_personpersonsOne person's profile (name, email, properties), a first_touch object (first-touch attribution — the $initial_* keys with the prefix stripped), plus every distinct id linked across merges. Requires person:read.
list_recordingsrecordingsSession recording metadata (id, distinct id, duration, first url, click/keypress counts). Requires replay:read.
list_campaignscampaignList campaigns on the active project — id, name, description, status, AI model, schedule type. Optional status filter (draft / active / paused / archived). Requires campaign:read.
get_campaigncampaignFetch one campaign by id — targeting query, business context, conversion definition, schedule config, branding metadata. The full rendered HTML body is intentionally omitted. Requires campaign:read.
list_event_destinationsevent-destinationsList event-forwarding destinations (Facebook CAPI, GA4 server-side, PostHog, gtag-based GA4 / Google Ads) with names, types, enable state, consent category, and a non-secret credential summary. Bearer tokens are never returned. Requires settings:read.
get_event_destinationevent-destinationsFetch one destination by id with the full forwarding config (event filters, mappings, gtag proxy mode, conversion mappings). Bearer tokens / Meta access keys are never included. Requires settings:read.
query_ai_memoryintelligenceNatural-language question over AI memory — the server plans SQL, runs it, and returns { intent, sql, results, answer? } so the agent can audit what executed. Requires analytics:read and AI memory enabled on the project. See AI intelligence.
get_person_memoryintelligenceCanonical snapshot of one person's latest AI memory row (hot ai JSON fields flattened). Requires person:read. See AI intelligence.
search_site_knowledgesite-knowledgeSemantic search over your indexed website (marketing pages, docs, pricing). Returns text chunks with page URL and heading path. Requires analytics:read and site knowledge enabled on the project. Use for product/pricing questions — not for vTilt SDK docs (search_docs).
get_site_pagesite-knowledgeFull page content + metadata (meta/OG/json_ld/page_type/http_status), version awareness (version_count, last_changed_at), Class B screenshot URLs for humans, and all chunks for one URL or pathname. Requires analytics:read. For agent vision pixels use get_site_page_screenshots — do not HTTP-fetch screenshot URLs.
get_site_page_screenshotssite-knowledgeOpt-in agent vision: full-page crawl screenshot as MCP image content (scaled/tiled WebP) for viewport desktop|mobile, plus a Class B url for humans. Requires analytics:read. See Site knowledge.
list_site_pagessite-knowledgePaginated list of indexed pages; filters for auth-gated pages, crawl source (http, recording, hybrid), pathname search, and order (freshest / stalest / recently-changed). Requires analytics:read.
get_site_overviewsite-knowledgeProject-level site index status: page counts, auth-gated count, last crawl time. Requires analytics:read.
list_page_versionssite-knowledgeChange timeline for one page (change_kind, hashes, desktop+mobile screenshots per version) — the read half of change detection → impact attribution. Join captured_at to events for the metric shift. Requires analytics:read.
get_page_versionsite-knowledgeOne historical version's full content, CTA fingerprint, and desktop+mobile screenshots (restore / inspect). Requires analytics:read.
diff_page_versionssite-knowledgeStructured before/after between two version ids (section + CTA diffs + screenshot pairs). Requires analytics:read.
get_site_elementssite-knowledgeRaw structured element inventory (CTAs, links, forms, buttons, nav) for one page — CRO audits, funnel-gap detection, semantic event enrichment (selector / elements_chain → text). Requires analytics:read.
get_site_business_contextsite-knowledgeOperator-provided tier-0 business context (project description + business instructions). Requires analytics:read.
get_projectprojectActive project's id, slug, settings, timestamps.
get_project_configprojectA project's feature configuration (not its data): session-recording masking (mask_all_inputs / mask_all_text), replay sample rate, autocapture, consent / DNT, chat config. The right tool for "are inputs masked in recordings?". No secrets are returned. Requires settings:read.
update_projectprojectWrite. Rename the active project. Requires project:update and a write-scoped credential (mcp:write OAuth scope or a personal API key created with Read + write access). Every successful or failed call lands a row in the MCP audit log.

Note

Read-only personal API keys and OAuth tokens without the mcp:write scope never see update_project in tools/list. switch_project is omitted when you have only one accessible project, or when the project is pinned via x-vtilt-project-id / ?project_id= — the exact number of tools your agent sees depends on permissions, write access, pinning, and which toolsets are enabled.

Tip

Every project-scoped tool (everything in the table except list_projects, switch_project, and search_docs) accepts an optional project_id argument as a per-call override. When you name a project in the prompt ("list people in Pilates Forever"), the agent should pass that id on the tool call and leave the sticky pin alone — then ask before switch_project if you will keep working there. The same argument is also the cleanest way to compare two projects or run parallel chats off one MCP connection. See Parallel chats on one MCP config below.

Note

Tools that need a specific dashboard permission (analytics:read, person:read, replay:read, chat:read, campaign:read, settings:read, project:update, …) are silently hidden from tools/list when the caller lacks that permission on the active project — even after the toolset is enabled. Org owners and admins always see the full list. The VQL trio (run_query, validate_query, get_query_schema) enforces permissions per referenced table at compile time — get_query_schema only returns tables you are allowed to read. search_docs, get_guide, get_data_model, and get_context are always exposed.

Note

Feature toolsets (site knowledge, AI memory, recordings, Google/Meta Ads) appear in the first tools/list when any project you can access has that feature connected — not only the currently pinned project. Calls still run against the sticky pin unless you pass project_id. get_context.features describes the pinned project only. Session replay tools stay hidden when replay is off on the effective project; AI memory and site-knowledge tools stay listed when the pin has them off so a per-call override to an enabled project can succeed (a call against a project without the feature returns feature_disabled).

Warning

Claude.ai / Cursor / ChatGPT cache the tool catalog at connect time. Mid-conversation enable_toolset may show tools in its payload but still fail with "Tool not found" until you start a new conversation (or pin ?toolsets=site-knowledge / x-vtilt-project-id on the connector URL before connecting).

Note

The Google Ads tools live in the google-ads toolset (13 core: google_ads_search, google_ads_get_resource_metadata, google_ads_list_customers, google_ads_mutate, and the service-RPC tools) plus the optional google-ads-shortcuts toolset (16 typed write helpers). The 13-tool core appears automatically once a Google Ads account is connected to the pinned project — no enable_toolset needed; the typed shortcuts stay opt-in via a ?toolsets=google-ads-shortcuts URL pin. Until an account is connected the core tools stay hidden (calls would return feature_disabled). They act on your Google Ads advertiser account, not on vTilt analytics — reads need google_ads:read, writes need google_ads:write and a write-scoped credential, and every write is audited. After switch_project, check features.google_ads in the response. See Google Ads for the connection steps and full tool list.

#What you can ask

These prompts work as soon as the server is connected and a project is pinned:

Note

Prompts that need a feature toolset (recordings, AI memory, Google Ads) work as soon as the project has that feature connected — those tools are auto-listed. Prompts that need a pure on-demand toolset (persons, campaigns) make the agent call enable_toolset first. The rows below list the data tools the agent reaches for.

PromptTools the agent will reach for
"What's our top traffic source?" / "Is it Google Ads or direct?"get_attribution (not get_kpis top_sources)
"How many visits did we have last week vs the previous one?"get_kpis
"What events am I tracking on this project?"list_event_types
"Show me the 20 most recent events for distinct id device_abc."list_recent_events
"What button did this user click?" / recent $autocapturelist_recent_events with event_name=$autocapture, or run_query with event, properties
"Find the person with email alice@example.com and list their last 10 sessions."list_persons → get_person → list_recordings
"What's the bounce rate this month for www.example.com? Compare it to last month."get_kpis (twice)
"Summarise the last 50 recordings — which pages are users dropping off on?"list_recordings
"Which campaigns are currently active and what's their schedule?"list_campaigns
"Show me the targeting query for the Win-back churned users campaign."list_campaigns → get_campaign
"Is my GA4 destination wired up and forwarding purchase_completed?"list_event_destinations → get_event_destination
"What's bounce rate on Marketing site?" (one-off)get_kpis with project_id (ask before switch_project if continuing)
"Switch me to project Marketing site and keep working there."switch_project → then unnamed tools use the new pin
"How do I install the browser SDK in a Next.js app?"search_docs
"What did we learn from sessions that look like rage-quits on checkout?"query_ai_memory or VQL with :embed() — see AI intelligence
"Summarise everything we know about person abc123 from AI memory."get_person_memory
"What does our pricing page say about enterprise plans?"search_site_knowledge or get_site_page with pathname /pricing — see Site knowledge
"Did conversion change after we edited /pricing? What changed?"list_page_versions → diff_page_versions / join captured_at to events (+ get_page_version for full copy)
"Rename this project to Acme Marketing." (write-scoped credential required)update_project
"Which Google Ads campaigns spent the most last month?" (account connected)google_ads_get_resource_metadata → google_ads_search
"Pause the Brand - Exact Google Ads campaign." (write-scoped credential required)google_ads_search → google_ads_set_campaign_status

The agent picks the tools, the project, and the time ranges automatically — you just describe what you want.

#How it works

Your AI client (Cursor / Claude Desktop / ChatGPT / …)
        │
        ▼   POST https://www.vtilt.com/api/mcp + Authorization: Bearer vtu_…
vTilt MCP server (hosted)
        │
        ▼
Tool registry  ──►  Entity action  ──►  Postgres / ClickHouse / Redis

Three properties matter:

  1. Reads are read-only; writes are explicit and audited. The read tools touch no customer data and write nothing to your account except a per-lane "pinned project" pointer and your enabled-toolset set in Redis (see Pinning a project). The first core write tool (update_project) requires a personal API key created with Read + write access (or an OAuth token with the mcp:write scope) — it isn't visible to read-only credentials at all. Every write call lands a durable row in the MCP audit log so admins can answer "who renamed this project?" months later.
  2. It's scoped to your projects. A vtu_ key is owned by you, not by a project. Tools resolve the active project from a header, query parameter, or per-lane session pin — you can switch projects mid-conversation with switch_project.
  3. You control the budget. Every key counts against a 60-tool-call-per-minute sliding window. The connection handshake and discovery calls (initialize, tools/list, resources/*, prompts/list) are exempt, so reconnecting never burns your budget. Hit the limit and the next call returns a JSON-RPC error with a retryAfter hint.

#Connect a client

ClientGuide
CursorCursor
Claude DesktopClaude Desktop
VS Code (Copilot)VS Code
ChatGPT / CodexCodex
Anything elsePoint it at https://www.vtilt.com/api/mcp with a Bearer header — the authentication page lists the exact header format.

#Pinning a project

Most clients let you pass headers or query parameters per connection. vTilt accepts:

MethodExample
Headerx-vtilt-project-id: 7c5c…
Headerx-vtilt-organization-id: 123e… (project-less reads)
Headerx-vtilt-mcp-session-id: <uuid> (optional — isolates switch_project pins per agent when multiple MCP connections share one key; see Cursor)
Query parameterhttps://www.vtilt.com/api/mcp?project_id=7c5c…
Session — switch_project toolthe agent calls it for you on demand

If you only have one project, you don't need to pin anything — the server auto-pins to your single project.

Note

Until you add x-vtilt-mcp-session-id, every MCP connection for the same user credential shares one Redis pin namespace — switch_project in one agent can change the project another agent sees, and enabled toolsets are shared too. Use a static project pin (x-vtilt-project-id / ?project_id=) on duplicate MCP server entries, or send a distinct UUID per entry via x-vtilt-mcp-session-id.

#Switching projects conversationally

You don't need to copy a UUID from the dashboard — once the MCP server is connected, just say what you want:

"What's last week's bounce rate on the Marketing site?"
text

The agent will:

  1. Call list_projects to resolve the name to a UUID (and see which project is currently pinned).
  2. Call the analytics tool with a per-call project_id for Marketing site — the sticky pin is left alone.
  3. If you keep working on that project, it should ask before calling switch_project to make the pin sticky for the rest of the chat.

When you explicitly want a sticky switch:

"Switch to the Marketing site project and keep working there."
text

…the agent calls switch_project with { "project_name": "Marketing site" } (fuzzy match; ambiguous names return candidates). If the agent already has the UUID, project_id is preferred.

Tip

Ask "which projects do I have access to?" any time to make the agent call list_projects — handy when you can't remember the exact display name.

#Parallel chats on one MCP config

If the same MCP entry in your mcp.json powers several chats at once (Cursor tabs, VS Code chats, cloud agents) and each chat is working on a different project, switch_project is the wrong tool — its Redis session pin is shared across the connection and concurrent calls overwrite each other. Pick one of:

  • Per-call project_id (recommended). Every data tool — get_kpis, list_recent_events, list_persons, run_query, update_project, … — accepts an optional project_id argument that overrides the active project for that single call only. The session pin is not touched, so two chats can hit different projects on one MCP config without racing. Ask each chat "work on the Marketing project" / "work on the Product project" and the agent will append project_id automatically.
  • One-off comparisons ("compare Marketing and Product"). Same pattern — pass project_id per call instead of switch_project-ing back and forth. The audit log records the effective project per row (project_id_source = 'param' for overrides, 'session' / 'default' for pin-path reads).
  • Distinct lane UUIDs — for older agents that don't pass project_id per call, add a different x-vtilt-mcp-session-id per duplicate server entry so each connection gets its own Redis pin namespace (Cursor shows the exact JSON).
  • Per-project server entries — register vtilt-marketing and vtilt-product as separate MCP servers, each with its own static x-vtilt-project-id header. Useful when the integrator wants a hard guarantee that one connection never touches the other project.

Note

A static pin (x-vtilt-project-id or ?project_id=) wins outright — switch_project is hidden from tools/list and supplying a different project_id argument errors with -32602 "conflicts with the header pin". That is intentional: integrator policy beats agent preference.

#Querying with VQL

The three query tools (run_query, validate_query, get_query_schema) let an agent ask questions that don't fit the curated readers — funnels, custom segmentation, joins between events and persons, and (when AI memory is enabled) semantic search over distilled sessions, chats, recordings, and per-person intelligence. They share one pipeline (the VQL compiler) that enforces tenant scoping, read-only safety, and per-table permissions before any SQL touches ClickHouse. Vector similarity uses the :embed('text') macro documented on AI intelligence.

#What VQL is

A controlled subset of ClickHouse SQL:

  • Single SELECT statement (with optional WITH / CTEs, JOIN, GROUP BY, window functions). No INSERT, UPDATE, DELETE, CREATE, ALTER, SET, etc. Multi-statement input is rejected.
  • Curated catalog of virtual tables — analytics core (events, persons, person_distinct_ids, person_overrides) plus AI memory tables such as session_summaries and person_memory_latest when your project has AI memory turned on. Database-qualified names (system.users) and table functions (remote(), s3(), file()) are blocked at compile time. get_query_schema is the source of truth for the exact table and column list your caller can use.
  • Curated function allowlist — standard aggregations (count, sum, uniq, quantile*), JSON helpers (JSONExtract*), date helpers (toStartOfDay, dateDiff, now), string / array / conditional functions, and the vector distance family (cosineDistance, L2Distance, L2SquaredDistance, dotProduct) where embeddings exist. System / cluster / file / dictionary functions are rejected.
  • Tenant scoping is automatic. The server applies a project_id filter to every catalog table at the storage engine level (ClickHouse additional_table_filters). Do not add project_id to your WHERE clause — you'd be filtering an already-filtered set.
  • Hard ceilings. readonly = 2, 30s execution timeout, 50M-row scan cap, 1000-row result cap (clamp the result smaller via limit).

#Tools

ToolUse it when
get_query_schemaDiscover what tables and columns exist for the active project. Tables you cannot read (per project permissions) are omitted entirely.
validate_queryDry-run a query through the compiler — parse + catalog + permission check. Returns { valid, referencedTables, error: { code, message, position?, hint? } }. No CH call.
run_queryExecute the compiled query and return { ok: true, rows, rowCount, … } on success. Compile failures return { ok: false, error: { code, message, hint? } } in the normal tool result (not a generic execution error). Runtime failures use { error: { code, message, next, details } } on isError: true. Truncates string cells over 4 KB.

The recommended agent loop is get_query_schema → write VQL → validate_query → fix any errors using the structured code + hint → run_query. The validate step is cheap (no ClickHouse round-trip) and catches almost every class of failure agents make.

#$autocapture clicks in tool responses

The browser SDK stores compact autocapture: $elements_chain (encoded string) plus $el_text, not the verbose $elements array. ClickHouse keeps that shape; MCP tools enrich at read time:

FieldStored in ClickHouseIn list_recent_events / run_query responses
$elements_chainYesYes
$el_textYesYes
$elementsRarely (legacy opt-out)When present
$elementNoYes — parsed clicked node (JSON)
data-vt-capture-attribute-* keysYes — top-level on propertiesSame (not inside $element)
$external_click_url, $element_selectors, form/scroll extrasYes when applicableSame

$element and $elements_chain describe DOM structure (tag, classes, attr__* HTML attributes). Many sites attach additional business metadata that lives as top-level keys on properties — especially data-vt-capture-attribute-* (promoted to keys like feature, variant) and flags such as $external_click_url. Do not stop at $element; scan the full properties object on every $autocapture row.

Do not JSON.parse($elements_chain) or expect properties.$elements on modern sites. Invoke /vtilt:conventions for the full data-model rules.

#Permissions

VQL enforces permissions per referenced table. The matrix below is the analytics core; AI memory tables (session_summaries, conversation_summaries, recording_summaries, person_memory_latest, ai_usage_log) each carry their own requirement — see the table on AI intelligence. The query toolset is enabled by default, so these tools are in the boot set.

Virtual tableRequired permission
eventsanalytics:read
personsperson:read
person_distinct_idsperson:read
person_overridesperson:read

Org owners and admins always pass. A regular member sees permission_denied from run_query / validate_query and gets the table omitted from get_query_schema if they lack the permission for it. The query trio enforces the per-table check at compile time, not at registry-filter time.

#Example: pageviews per day, last 14 days

Find the top 10 distinct ids by pageview count over the last 14 days,
ordered descending. Use VQL.
text

The agent will typically call get_query_schema, write something like:

SELECT distinct_id, count() AS pageviews
FROM events
WHERE event = '$pageview'
  AND timestamp > now() - INTERVAL 14 DAY
GROUP BY distinct_id
ORDER BY pageviews DESC
LIMIT 10
sql

…then validate_query (catches typos / unknown columns), then run_query. Note the absence of any project_id clause — the server injects it.

#Error envelope

validate_query returns { valid: false, error: { code, message, position?, hint? } } when the compiler rejects the SQL — the tool call itself succeeded; read error.code and error.hint to fix the query.

run_query uses two shapes:

  • Compile failure (bad syntax, unknown table/column, permission denied at compile time) — { ok: false, error: { … } } in the normal tool result, same error object as above. MCP clients surface this in structuredContent instead of collapsing it to "Error occurred during tool execution".
  • Runtime failure (query compiled but ClickHouse rejected it) — { error: { code: "invalid_arguments", message, next, details: { vql: { code: "execution_error", … } } } } on isError: true.
{
  "ok": false,
  "error": {
    "code": "unknown_table",
    "message": "Unknown table: secret_internal_table.",
    "hint": "Call get_query_schema for the catalog of virtual tables your credential can access."
  }
}
json

Stable VQL error.code values: parse_error, unsupported_statement, unsupported_construct, unknown_table, unknown_function, permission_denied, execution_error, plus embed-specific codes (embed_invalid_argument, embed_text_too_long, embed_disabled) described on AI intelligence. Agents pattern-match on code to self-correct (e.g. parse_error → re-read the syntax; unknown_table → call get_query_schema; permission_denied → ask a project admin or pick a different table).

Every other tool follows the same idea with a slightly richer envelope so agents can recover in one turn:

{
  "error": {
    "code": "project_not_found",
    "message": "No accessible project matches \"markting site\".",
    "next": "list_projects, switch_project",
    "details": { "suggestions": ["Marketing site", "Marketing app"] }
  }
}
json

code is one of no_active_project, project_not_found, project_ambiguous, permission_denied, feature_disabled, resource_not_found, invalid_arguments, internal_error. next names the tool(s) to call to recover, and details carries actionable structure (closest project names, the missing permission, the disabled feature). The agent reads next and details and retries without asking you.

#Searching the docs from your agent

The search_docs tool gives your AI client access to the same documentation corpus that powers /docs, /llms.txt, and /llms-full.txt. Use it when you'd otherwise paste a doc link into the conversation — the agent grabs the relevant snippet itself and can keep working without context-switching.

Input: { query: string, limit?: number (default 5, max 15) }. Output: { results: Array<{ title, slug, url, description, snippet, score }>, total_matched: number }.

{
  "query": "how do I configure event forwarding to GA4?",
  "limit": 3
}
json

Returns matched pages with body excerpts and absolute URLs (https://www.vtilt.com/docs/guides/event-forwarding). The tool runs in-process — no third-party search service — so results are deterministic and self-host installations get the same surface for free.

"Where do I put my project token in a Next.js app? Show me the exact code."
text

…will typically call search_docs with the question, then weave the matched code samples into the response.

#Writing data

vTilt's core MCP surface is mostly read-only by design — the curated readers and the VQL query layer cover almost every "summarise / analyse / explore" prompt without touching tenant state. The first core write tool, update_project (in the on-demand project toolset), ships with three-layer safety (the Google Ads write tools follow the same model):

  1. Scoped credentials. Read-only personal API keys (the default when you create a key) and OAuth tokens without mcp:write never see write tools in tools/list. Mint a new key with Read + write access from Account → Personal API keys, or include mcp:write in your OAuth scope= parameter (paired with offline_access if your client refreshes tokens).
  2. Permission gate. update_project declares requires: project:update. The MCP layer hides it from tools/list when you lack the permission and re-checks per call (so a stale tools/list cache cannot smuggle the tool past the auth wrapper).
  3. Audit log. Every successful or failed call lands a row in org.mcp_audit_logs with userId, projectId, traceId, the tool name, the credential's scope at call time, a SHA-256 hash of the arguments, a one-line summary (e.g. name → New Name), the result, and the wall-clock duration. The args themselves are never stored — only the hash, mirroring the wide log's privacy convention. The Google Ads write tools use the same audit channel.

Warning

Write-capable keys can mutate project state. Treat them like a database password — don't paste them into shared chats, don't commit them to git, and don't leave them in dotfiles backed up to a public service. Most agent workflows want read-only keys; reach for write only when you actively need the agent to change project state on your behalf.

The core write surface is intentionally tiny (one tool, one mutation: rename the active project) — a demonstrator for the pattern. The richer write surface lives in the Google Ads toolsets, built on the same audit + scope foundation.

#Limits and errors

LimitDefault
Tool calls per minute (per user)60 (sliding window). The connection handshake and discovery calls (initialize, tools/list, resources/*, prompts/list) do not count.
AI tool calls per minute (per user)20 for query_ai_memory (sliding window), on top of the global budget — these run a language model, so they carry a tighter cap.
Maximum rows per tool calllist_recent_events 200, list_event_types 200, list_persons 200, list_recordings 100, list_campaigns 100, run_query 1000, get_kpis n/a
VQL query timeout30s execution, 50M-row scan cap
VQL result cell truncation4 KB per string cell (overage replaced with …[truncated]); truncated: true flag returned in response
Result payload sizeNo hard cap; agents typically truncate themselves

When you hit the rate limit the server returns a JSON-RPC error with code -32099 and error.data.retryAfter (seconds). Standard MCP clients surface this to the agent; well-behaved agents back off automatically.

#Selecting toolsets

By default a connection boots with the always-on + default toolsets, and the agent expands the surface at runtime with enable_toolset (see Toolsets). If you'd rather pin a fixed surface on the connection — handy for read-only deployments, focused agents, or clients that can't call meta-tools — pass ?toolsets= on the MCP URL or the X-MCP-Toolsets header:

https://www.vtilt.com/api/mcp?toolsets=analytics,persons,google-ads
https://www.vtilt.com/api/mcp?toolsets=all
https://www.vtilt.com/api/mcp?toolsets=default
text
  • A static ?toolsets= / X-MCP-Toolsets value overrides the session-enabled set for that connection (GitHub-style). The always-on meta + context toolsets are always included.
  • all exposes every toolset (the full 58-tool surface); default is the boot set (context + analytics + query + docs).
  • Valid ids: analytics, persons, recordings, campaign, event-destinations, intelligence, project, query, docs, google-ads, google-ads-shortcuts (plus all / default). Unknown ids are silently ignored.

You can still narrow to individual tools with ?tools= (union semantics — a named tool surfaces even if its toolset is off):

https://www.vtilt.com/api/mcp?tools=get_kpis,list_recent_events
text

Note

?features= is retained as a deprecated alias for ?toolsets= so existing configs keep working, but it will be removed — migrate to ?toolsets=.

#Next steps

Guides

  • Authentication — personal API keys, headers, troubleshooting.
  • OAuth — browser sign-in flow for Claude, ChatGPT, Cursor, VS Code, …
  • Agent skills (prompts) — /vtilt:… slash commands bundled with the server.
  • AI intelligence — AI memory tables, :embed('text'), query_ai_memory, get_person_memory.

Client setup

  • Cursor · Claude Desktop · VS Code · Codex

SDK

  • Identify & alias — make sure your dashboard data has high-quality persons before asking the agent to reason about them.
PreviousError trackingNode SDKNextAuthenticationMCP server