MCP server
Harvv runs a hosted Model Context Protocol server so AI coding agents (GitHub Copilot, Cursor, Claude, Windsurf, Zed, Lovable) can read your behavioral findings and install the pixel without leaving the editor. One endpoint, one bearer token, scoped to your account.
Endpoint
https://harvv.com/mcp
The aliases /mcp/lovable and /mcp/harvv resolve to
the same server. Point any MCP client at the base /mcp URL.
Connect your client
Every client uses the same standard MCP config: an HTTP server URL plus an
Authorization: Bearer header. Only the file or UI you paste it
into changes.
- VS Code / GitHub Copilot, Cursor, Claude Desktop, Windsurf, Zed, Copilot CLI: use the generated config at /install/copilot. It fills in your token and shows the right file per client.
- Lovable: open Connectors, pick Harvv from the catalog, and paste your token. Walkthrough at /install/lovable.
- A custom client or your own agent: see Transport below.
Authentication
- Generate a token at harvv.com/app (or right on /install/copilot). The raw token is shown once.
- Each token is scoped to one Harvv account. Every tool call filters to that account's sites and no one else's.
- Token lifetime is yours to choose when you create one in the dashboard (no expiry, 30 days, 90 days, or a year). Connectors set up through OAuth (claude.ai, ChatGPT, Claude Code) get a 90-day token plus a refresh token and renew themselves. Tokens created before September 2026 never expire. A 401 carries a reason code that says which case you hit. See Troubleshooting.
- Keep the token out of committed config. The VS Code flow uses an input prompt so the editor stores it for you.
Transport (custom clients)
The server speaks JSON-RPC 2.0 over Streamable HTTP (MCP spec 2025-03-26 and later). For a hand-rolled client:
- Method:
POSTtohttps://harvv.com/mcp - Headers:
Content-Type: application/json,Authorization: Bearer YOUR_TOKEN, andAccept: application/json, text/event-stream(the MCP SDK 1.25+ enforces the dual Accept value). - Lifecycle:
initialize, thentools/list, thentools/call.pingis supported.
curl -X POST https://harvv.com/mcp \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list_my_sites","arguments":{}}}'
Tools
36 tools, listed from the same registry tools/list
serves, so this page cannot drift from the server. Every tool is read-only
and scoped to the account behind your token. Tools that act on a site take
site_id; on an account with more than one site it is required,
and list_my_sites is how you get it. The setup tools and
get_help are free; data tools count against the monthly
credit allowance only when they answer (a timeout or a bad argument is not
charged).
Setup and help
Free. Onboarding, verification, navigation, the product guide, and a self-check for a draft conclusion.
check_install
Check whether the Harvv pixel is firing on a public URL.
url(required): Public URL of the published site. Must be http/https. Localhost, RFC1918 ranges, and cloud-metadata IPs are rejected. Lovable preview URLs (id-preview--*.lovable.app) will return installed=false with a tailored message b
check_slack_integration (no arguments)
Check whether this account's Slack integration is actually wired up correctly, and say what to fix if not.
get_dashboard_url
Get a deep link to the Harvv dashboard for a site on your account.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it he
get_help
How to USE Harvv: product documentation and best practice, not site data.
question(required): The user's question in their own words, e.g. 'how do I turn off the slack integration'.
install_pixel
Get the Harvv pixel install snippet for a site on this account, with framework-specific guidance for where to place it.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it he
list_my_sites (no arguments)
List every site belonging to the Harvv account this MCP token is scoped to.
red_team
Red Team an analysis you are about to present: attack your own conclusion before the user sees it.
draft_conclusion(optional): (optional) The conclusion you are about to present, so the checks can be returned in the order most likely to bite it.
Data tools
Metered. Behavioral findings, page-level friction and behavior, traffic, conversion, speed, SEO and AI visibility for your sites. Every numeric response carries window, counted and sample blocks that say what was measured.
annotate_change
Record a change the team made to the site (a deploy, a theme edit, a campaign launch, a price change) on a date, so every later comparison can show it beside a rise or drop.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedate(required): When it happened, YYYY-MM-DD.note(required): What changed, one or two sentences.confirm(optional): Must be true to save. Omit to preview.
dismiss_issue
Dismiss a finding on this account's site with a reason, exactly as the dashboard's Dismiss button does.
issue_id(required): The finding id from get_issues / get_friction_findings.reason(required): Why it is being dismissed.confirm(optional): Must be true to actually dismiss. Omit to preview.note(optional): (optional) A one-line note for the activity log.
get_ad_spend
What this site's ads COST and what the ad platform says they returned: spend, impressions, clicks, conversions and conversion value per campaign, a daily series, and (Google Ads) the change history: who changed a budget, bid strategy, status, landing page, ad
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 30, max 90.
get_ad_traffic_quality
How the traffic from each AD CAMPAIGN behaves after it lands, compared against this site's own non-paid traffic: bounce, rage clicks, dead clicks, how far people scroll, whether they touch a form.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 30, max 90. Wider than other tools on purpose: a campaign sending 30 sessions a day cannot be separated from noise in 7 days.
get_ai_bot_activity
AI crawler activity on a site, including robots.txt VIOLATIONS (bots requesting paths your robots.txt disallows).
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 30, max 90 (clamped).
get_ai_recommendations
Which pages AI assistants (ChatGPT, Perplexity, Gemini, Claude, Copilot) actually sent visitors to, and whether those pages emit structured data a model can quote.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 90, max 365. Needs ~90 days to clear the reporting floor on most sites.
get_audience_state
Who the visitors are, as categories: signed-in vs anonymous vs unknown, whether logged-in CMS staff are in the numbers, whether they arrived with a cart, and first-time vs returning.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 7, max 30 (clamped).
get_commerce_funnel
Get the site's commerce funnel (browsed collection → viewed product → added to cart → viewed cart → began checkout → purchased) plus form starts/submits, with session counts, event counts, and total cart value where the site tags values.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 7, max 30 (clamped).
get_conversion_path
Find the interaction that gates conversion on a page, and how many visitors never reach it.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hepath(optional): (optional) Page path to analyse, e.g. /products/some-item, or / for the homepage. Matched EXACTLY by default; pass path_match 'prefix' with path /products to cover every product page. Omit to analyse the whole site. Samepage(optional): (deprecated alias of path; accepted for one release)path_match(optional): (optional) How `path` is matched. 'exact' (default): that one page only, so '/' is the homepage. 'prefix': the page and every page under it, e.g. path '/products' with 'prefix' covers all product pages. 'contains': any pdays(optional): (optional) Window in days. Default 7, max 30 (clamped). Keep it small on high-traffic stores.conversion(optional): (optional) What counts as a conversion. Default 'auto' = a click on a buy/submit control (add to cart, buy now, checkout, place order). Or pass a substring to match a specific element, e.g. 'Add to Cart'.
get_conversions
REVENUE AND CONVERSIONS, from two independent sources.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 30. Trends compare this window against the one immediately before it.
get_dismissed_findings
What this account has already dismissed on a site: findings marked dismissed with the reason given (false positive, noise, intentional, duplicate, fixed already, not useful) or auto-expired by housekeeping.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it helimit(optional): (optional) Max findings, default 20, max 50.
get_element_clicks
Which ELEMENTS get clicked most, ranked by click volume, optionally narrowed to a page path or an element-name filter (e.g.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hepath(optional): (optional) Page path to narrow to, e.g. /cart, or / for the homepage. Matched EXACTLY by default; see path_match.path_match(optional): (optional) How `path` is matched. 'exact' (default): that one page only, so '/' is the homepage. 'prefix': the page and every page under it, e.g. path '/products' with 'prefix' covers all product pages. 'contains': any pelement_contains(optional): (optional) Only elements whose handle contains this text, e.g. 'footer' or 'add-to-cart'.device(optional): (optional) Narrow to one device class: 'mobile', 'desktop' or 'tablet'. Omit for all devices. The response carries device_coverage: device was only recorded on this rollup from 2026-09-01, so a filtered read over an oldesegment(optional): (optional) Narrow to one session segment, e.g. 'logged_in', 'anonymous', 'has_cart', or a merchant-declared value namespaced by its window.harvv.user key like 'tags:wholesale' or 'plan:pro'. A value never observed on thedays(optional): (optional) Window in days. Default 7, max 30 (clamped).limit(optional): (optional) Max elements returned. Default 12, max 25.
get_fleet_benchmark
Compare this site with the whole Harvv fleet on five behaviour rates over the last 30 days: share of sessions on phones, sessions with a dead click, sessions with a rage click, sessions that never scroll past a quarter of the page, and dead clicks per 1,000 se
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it he
get_friction_findings
ALIAS of get_issues (kept for older clients; prefer get_issues, which returns the same findings with fix text and a status filter).
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 7, minimum 1, maximum 30. Values outside this range are clamped; the response will include clamped=true.
get_issue_sessions
The individual visitor sessions affected by ONE specific issue, by issue_id (get it from get_issues or get_friction_findings).
issue_id(optional): The issue_id from get_issues (or the id in a finding's dashboard_url). Required in practice — without it there is no finding to list sessions for.site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it helimit(optional): (optional) Max session ids to return. Default 50, max 200.
get_issues
The UX issues Harvv has detected on a site, with a plain-English fix for each: what is broken, how many sessions it affects, its status, and the recommended fix.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hestatus(optional): (optional) 'open' (default) = the active work pipeline; 'all' = everything not dismissed; 'resolved' = ONLY resolved and fixed issues; 'dismissed' = what was closed without a fix (false positive, won't fix, noise), whichlimit(optional): (optional) Max issues to return. Default 15, max 50.include_headline(optional): (optional) Also return a `headline` block: sessions, dead clicks and rage clicks over `days` (default 7) for the whole site. This is what get_friction_findings used to add.days(optional): (optional) Window for the headline block only. Default 7, max 30. Findings are never filtered by it.
get_js_errors
JavaScript exceptions and failed network requests on the site, grouped so you can name WHICH page and WHICH file or URL is breaking.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hepath(optional): (optional) Page path to narrow to, e.g. /checkout, or / for the homepage. Matched EXACTLY by default; see path_match.path_match(optional): (optional) How `path` is matched. 'exact' (default): that one page only, so '/' is the homepage. 'prefix': the page and every page under it, e.g. path '/products' with 'prefix' covers all product pages. 'contains': any pdays(optional): (optional) Window in days. Default 3, max 30 (clamped). Measured on the busiest store: 1d=3.4s, 3d=7.3s, 7d exceeds the query budget. Ask for a longer window deliberately, and expect it to come back degraded on a high-trlimit(optional): (optional) Max rows per list. Default 10, max 25.device(optional): (optional) Only sessions on this device type.segment(optional): (optional) Only sessions carrying this visitor segment. Unknown segments are refused with the observed list.
get_layout_changes
When did this site's layout or theme last change, and which findings the change may have invalidated.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days, like every other tool. Default 56 (8 weeks), max 182. Scanned in whole weeks.weeks(optional): (deprecated alias of days; whole weeks)
get_page_behavior
How visitors BEHAVE on a page (or the whole site) beyond clicks: what share left mid-visit to check something elsewhere (tab-away), stopped and hesitated (with the median pause), copied text like a price or SKU (comparison shopping), held the site open in mult
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hepath(optional): (optional) Page path to narrow to, e.g. /cart, /products/some-item, or / for the homepage. Matched EXACTLY by default; see path_match. Omit for site-wide. When set, the response carries path_scope (how many pages were mapath_match(optional): (optional) How `path` is matched. 'exact' (default): that one page only, so '/' is the homepage. 'prefix': the page and every page under it, e.g. path '/products' with 'prefix' covers all product pages. 'contains': any pdays(optional): (optional) Window in days. Default 7, max 30 (clamped).
get_page_friction
Per-page friction breakdown for a site on your account: dead clicks, rage clicks, and affected sessions per page, plus the top stuck elements.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 7, max 30 (clamped).path(optional): (optional) A page path (e.g. /merchants, or / for the homepage) or full URL. When set, returns the top dead/rage-clicked elements on THAT page in target_page_top_elements, so you can name which elements are stuck on a sppath_match(optional): (optional) How `path` is matched. 'exact' (default): that one page only, so '/' is the homepage. 'prefix': the page and every page under it, e.g. path '/products' with 'prefix' covers all product pages. 'contains': any pdevice(optional): (optional) Narrow to one device class: 'mobile', 'desktop' or 'tablet'. Omit for all devices. Friction is usually device-specific -- a control that is fine on desktop and unreachable on mobile reads as mild sitewide fricsegment(optional): (optional) Narrow to one session segment, e.g. 'logged_in', 'anonymous', 'has_cart', or a merchant-declared value namespaced by its window.harvv.user key like 'tags:wholesale' or 'plan:pro'. A value never observed on the
get_performance
Core Web Vitals for a site on your account, from real visitor sessions: LCP / INP / CLS / TTFB p50+p75, plus the slowest pages by LCP, over the last N days (default 7, max 30).
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 7, max 30 (clamped).device(optional): (optional) Only sessions on this device type.segment(optional): (optional) Only sessions carrying this visitor segment. Unknown segments are refused with the observed list.
get_search_console
Google Search Console data Harvv has synced for a site (needs the Search Console connection in the dashboard): totals, the top queries by clicks, CTR leaks (queries ranking well that under-click their expected rate) and almost-ranking queries (positions 8 to 2
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it helimit(optional): (optional) Max rows per list. Default 20, max 50.
get_seo_insights
Per-page SEO + GEO (AI-search) insights for a site, built from what the pixel already captured on the REAL rendered page of each URL your visitors loaded — no codebase needed.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 14, max 30 (clamped).
get_site_stack
The site's technology stack as Harvv last observed it: platform, theme, and the ACTIVE plugin list with an honest role for each.
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hecategory(optional): (optional) Filter to one category, e.g. 'page_builder', 'seo', 'security', 'caching'.
get_traffic_sources
Where a site's traffic actually comes from, classified into channels: Organic search, Paid search, Paid social, Social, Email, Referral, Direct — with sessions + share for each over the last N days (default 7, max 30).
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 7, max 30 (clamped).
get_traffic_summary
Traffic overview for a site on your account: sessions, unique visitors, top pages, the DEVICE split (mobile / desktop / tablet, as `devices`) and the BROWSER split (safari / chrome / edge / firefox / other, as `browsers`), referrer/source split, and bounce-equ
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Window in days. Default 7, max 30 (clamped).device(optional): (optional) Only sessions on this device type.browser(optional): (optional) Only sessions in this browser. The response's `browsers` split shows what a site actually gets; narrow to one when a defect looks browser-specific.segment(optional): (optional) Only sessions carrying this visitor segment (e.g. logged_in, anonymous, first_visit, has_cart). Unknown segments are refused with the observed list.
mark_issue_fixed
Record that a finding has been fixed, exactly as the dashboard's Mark fixed action does: it moves to verification, and Harvv measures the before/after on the next detection pass (call verify_finding after 14 days for the numbers).
issue_id(required): The finding id.confirm(optional): Must be true to act. Omit to preview.note(optional): (optional) What was changed, for the activity log.
set_conversion_goal
Tell Harvv what counts as a conversion on a site: 'form' (a form submit, call or booking), 'add_to_cart' (a store: cart and checkout), or 'cv' (the site sends its own conversion event).
site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hegoal(required): What a conversion is on this site.confirm(optional): Must be true to save. Omit to preview.
verify_finding
Re-check ONE finding against RECENT data to answer 'is this still happening right now?'.
issue_id(optional): The issue_id from get_issues. Required in practice.site_id(optional): (optional) The site to read. Accepts the domain (e.g. example.com), a 16-character pixel key, or the site UUID. Omit ONLY when you mean whichever site is first on the account — if the user named a site, always pass it hedays(optional): (optional) Recent window to re-check. Default 3, max 14. Short on purpose: the question is whether it is happening NOW.
Example: get_friction_findings
Request:
{"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"get_friction_findings","arguments":{"days":7}}}
The result is a content block whose text is JSON:
{
"ok": true,
"site": { "id": "c8d5fc24-...", "name": "Acme", "domain": "acme.com" },
"window": { "days": 7, "requested_days": 7, "clamped": false,
"min_days": 1, "max_days": 30 },
"headline": { "sessions": 1840, "events": 41230,
"dead_clicks": 212, "rage_clicks": 64,
"unit": "count", "window_days": 7 },
"findings": [
{ "id": "8231", "type": "dead_click",
"title": "Dead clicks on pricing CTA",
"element": "button.upgrade-cta", "priority": "high",
"summary": "212 clicks on a button with no handler ...",
"dashboard_url": "https://harvv.com/app/site/c8d5fc24-.../issue/8231" }
]
}
All headline counts are totals over the whole window, not
per-day averages. Findings come sorted high priority first.
Troubleshooting
token_expired: the token reached the lifetime it was created with. An OAuth connector refreshes on its own; a pasted token is regenerated at harvv.com/app.token_revoked: regenerate at harvv.com/app and reconnect.token_not_found: mistyped, or from another account. Regenerate and reconnect.wrong_scope: not a customer MCP token. Generate one from your dashboard.
error.data.reason (JSON-RPC) and the
WWW-Authenticate response header.
inputSchema looks
like {"properties":{}}, it cached an older catalog from before
the site_id / url / days parameters
shipped. Remove and re-add the Harvv connector (or refresh the connection)
to re-fetch tools/list and pick up the current schemas.
list_my_sites genuinely takes no arguments, so an empty schema
there is correct.
check_install reports not-installed on a Lovable preview.
Use the published URL, not the id-preview--*.lovable.app wrapper
(see check_install above).