On this page
MCP server
Last updated
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.
#Documentation map
Use the sidebar Guides and Client setup groups the same way as this table: concepts first, then wire your editor.
| Topic | Page |
|---|---|
| Personal API keys, headers, errors | Authentication |
| Browser OAuth (Claude, ChatGPT, Cursor, …) | OAuth |
Slash-command prompts (/vtilt:…) | Agent skills (prompts) |
AI memory, VQL :embed(), query_ai_memory, get_person_memory | AI intelligence |
Indexed website crawl, search_site_knowledge, site VQL tables | Site knowledge |
| Cursor | Cursor |
| Claude Desktop | Claude Desktop |
| VS Code | VS Code |
| ChatGPT / Codex | Codex |
#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.
| Toolset | When you see it | Tools |
|---|---|---|
meta | always on | list_toolsets, get_toolset, enable_toolset |
context | always on | get_guide, get_data_model, get_context, list_projects, switch_project |
analytics | default | get_kpis, get_attribution, list_recent_events, list_event_types |
query | default | run_query, validate_query, get_query_schema |
docs | default | search_docs |
persons | on demand | list_persons, get_person |
recordings | auto when replay on | list_recordings |
campaign | on demand | list_campaigns, get_campaign |
event-destinations | on demand | list_event_destinations, get_event_destination |
intelligence | auto when AI memory on | query_ai_memory, get_person_memory |
site-knowledge | auto when site knowledge on | search_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 |
project | on demand | get_project, get_project_config, update_project |
google-ads | auto when Google Ads connected | 13 core Google Ads read/mutate tools |
google-ads-shortcuts | on demand (pin only) | 16 typed Google Ads write shortcuts |
Discover-then-enable flow — the three meta tools are always present:
list_toolsets— every toolset, its description, tool count, and whether it's currently enabled.get_toolset { toolset }— preview a toolset's tools (names + descriptions + input schemas) without enabling it.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).
#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).
| Tool | Toolset | Description |
|---|---|---|
get_guide | context | The 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_model | context | Where 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_context | context | Situational 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_projects | context | Enumerate 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_project | context | Pin 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_kpis | analytics | Headline 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_attribution | analytics | First-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_events | analytics | Newest-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_types | analytics | Discover 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_query | query | Execute 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_query | query | Dry-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_schema | query | Return 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_docs | docs | Search 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_persons | persons | Paginated list of identified persons. Optional substring filter against name/email/id. Requires person:read. |
get_person | persons | One 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_recordings | recordings | Session recording metadata (id, distinct id, duration, first url, click/keypress counts). Requires replay:read. |
list_campaigns | campaign | List campaigns on the active project — id, name, description, status, AI model, schedule type. Optional status filter (draft / active / paused / archived). Requires campaign:read. |
get_campaign | campaign | Fetch 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_destinations | event-destinations | List 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_destination | event-destinations | Fetch 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_memory | intelligence | Natural-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_memory | intelligence | Canonical snapshot of one person's latest AI memory row (hot ai JSON fields flattened). Requires person:read. See AI intelligence. |
search_site_knowledge | site-knowledge | Semantic 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_page | site-knowledge | Full 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_screenshots | site-knowledge | Opt-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_pages | site-knowledge | Paginated 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_overview | site-knowledge | Project-level site index status: page counts, auth-gated count, last crawl time. Requires analytics:read. |
list_page_versions | site-knowledge | Change 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_version | site-knowledge | One historical version's full content, CTA fingerprint, and desktop+mobile screenshots (restore / inspect). Requires analytics:read. |
diff_page_versions | site-knowledge | Structured before/after between two version ids (section + CTA diffs + screenshot pairs). Requires analytics:read. |
get_site_elements | site-knowledge | Raw 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_context | site-knowledge | Operator-provided tier-0 business context (project description + business instructions). Requires analytics:read. |
get_project | project | Active project's id, slug, settings, timestamps. |
get_project_config | project | A 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_project | project | Write. 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. |
#What you can ask
These prompts work as soon as the server is connected and a project is pinned:
| Prompt | Tools 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 $autocapture | list_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 / RedisThree properties matter:
- 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 themcp:writescope) — 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. - 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 withswitch_project. - 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 aretryAfterhint.
#Connect a client
| Client | Guide |
|---|---|
| Cursor | Cursor |
| Claude Desktop | Claude Desktop |
| VS Code (Copilot) | VS Code |
| ChatGPT / Codex | Codex |
| Anything else | Point 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:
| Method | Example |
|---|---|
| Header | x-vtilt-project-id: 7c5c… |
| Header | x-vtilt-organization-id: 123e… (project-less reads) |
| Header | x-vtilt-mcp-session-id: <uuid> (optional — isolates switch_project pins per agent when multiple MCP connections share one key; see Cursor) |
| Query parameter | https://www.vtilt.com/api/mcp?project_id=7c5c… |
Session — switch_project tool | the 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.
#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?"The agent will:
- Call
list_projectsto resolve the name to a UUID (and see which project is currently pinned). - Call the analytics tool with a per-call
project_idfor Marketing site — the sticky pin is left alone. - If you keep working on that project, it should ask before calling
switch_projectto 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."…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.
#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 optionalproject_idargument 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 appendproject_idautomatically. - One-off comparisons ("compare Marketing and Product"). Same pattern — pass
project_idper call instead ofswitch_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_idper call, add a differentx-vtilt-mcp-session-idper duplicate server entry so each connection gets its own Redis pin namespace (Cursor shows the exact JSON). - Per-project server entries — register
vtilt-marketingandvtilt-productas separate MCP servers, each with its own staticx-vtilt-project-idheader. Useful when the integrator wants a hard guarantee that one connection never touches the other project.
#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
SELECTstatement (with optionalWITH/ CTEs,JOIN,GROUP BY, window functions). NoINSERT,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 assession_summariesandperson_memory_latestwhen 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_schemais 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_idfilter to every catalog table at the storage engine level (ClickHouseadditional_table_filters). Do not addproject_idto yourWHEREclause — 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 vialimit).
#Tools
| Tool | Use it when |
|---|---|
get_query_schema | Discover what tables and columns exist for the active project. Tables you cannot read (per project permissions) are omitted entirely. |
validate_query | Dry-run a query through the compiler — parse + catalog + permission check. Returns { valid, referencedTables, error: { code, message, position?, hint? } }. No CH call. |
run_query | Execute 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:
| Field | Stored in ClickHouse | In list_recent_events / run_query responses |
|---|---|---|
$elements_chain | Yes | Yes |
$el_text | Yes | Yes |
$elements | Rarely (legacy opt-out) | When present |
$element | No | Yes — parsed clicked node (JSON) |
data-vt-capture-attribute-* keys | Yes — top-level on properties | Same (not inside $element) |
$external_click_url, $element_selectors, form/scroll extras | Yes when applicable | Same |
$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 table | Required permission |
|---|---|
events | analytics:read |
persons | person:read |
person_distinct_ids | person:read |
person_overrides | person: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.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…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, sameerrorobject as above. MCP clients surface this instructuredContentinstead 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", … } } } }onisError: 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."
}
}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"] }
}
}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
}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."…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):
- Scoped credentials. Read-only personal API keys (the default when you create a key) and OAuth tokens without
mcp:writenever see write tools intools/list. Mint a new key with Read + write access from Account → Personal API keys, or includemcp:writein your OAuthscope=parameter (paired withoffline_accessif your client refreshes tokens). - Permission gate.
update_projectdeclaresrequires: project:update. The MCP layer hides it fromtools/listwhen you lack the permission and re-checks per call (so a staletools/listcache cannot smuggle the tool past the auth wrapper). - Audit log. Every successful or failed call lands a row in
org.mcp_audit_logswithuserId,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.
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
| Limit | Default |
|---|---|
| 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 call | list_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 timeout | 30s execution, 50M-row scan cap |
| VQL result cell truncation | 4 KB per string cell (overage replaced with …[truncated]); truncated: true flag returned in response |
| Result payload size | No 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- A static
?toolsets=/X-MCP-Toolsetsvalue overrides the session-enabled set for that connection (GitHub-style). The always-onmeta+contexttoolsets are always included. allexposes every toolset (the full 58-tool surface);defaultis the boot set (context+analytics+query+docs).- Valid ids:
analytics,persons,recordings,campaign,event-destinations,intelligence,project,query,docs,google-ads,google-ads-shortcuts(plusall/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#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.