MCP Server

Last updated: October 18, 2026

BetterReviews is an MCP server (Model Context Protocol, Streamable HTTP transport, protocol version 2025-06-18). Instead of writing curl calls, an AI client gets a small set of tools over your store: search the review corpus, read a review, pull the best reviews for a product, record and manage marketing claims, link supporting reviews, ask a reviewer for permission to feature their review, read your saved Studio ads and emails, and run the whole product-page optimization loop — regenerate, edit, fact-check, approve and publish.

The server is stateless and plain JSON: every POST carries one JSON-RPC 2.0 message and gets one JSON response; a notification gets 202 with no body. There are no sessions, no server-sent events and no batching.

Two ways to connect

DoorURLAuthTools
Headerhttps://api.betterreviews.app/api/v1/mcpX-API-Key header with a store API keyAll eighteen (ten need Read & Write)
Connector URLhttps://api.betterreviews.app/api/v1/mcp/c/<token>The URL itself (a connector token, up to 90 days)Read-only eight

Claude Code

claude mcp add --transport http betterreviews https://api.betterreviews.app/api/v1/mcp \
  --header "X-API-Key: YOUR_API_KEY"

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "betterreviews": {
      "type": "http",
      "url": "https://api.betterreviews.app/api/v1/mcp",
      "headers": { "X-API-Key": "YOUR_API_KEY" }
    }
  }
}

claude.ai and ChatGPT custom connectors

claude.ai and ChatGPT connectors cannot send headers, so you use a connector URL instead. Mint one with a Read & Write API key (201):

curl -X POST https://api.betterreviews.app/api/v1/mcp/connector-tokens \
  -H "X-API-Key: YOUR_API_KEY"
{
  "id": "…",
  "name": "mcp-connector",
  "prefix": "ppo_AbCd",
  "expires_at": "2026-12-12T10:00:00Z",
  "url": "https://api.betterreviews.app/api/v1/mcp/c/ppo_…"
}

Paste url as the connector URL in claude.ai, or as a custom connector in ChatGPT. It is shown once. Then:

  • Treat the URL as a password. Anyone who has it can read your reviews, claims and saved creatives until it expires or you revoke it.
  • It is read-only: only search_reviews, get_review, top_reviews, list_claims, permission_preview, get_optimization, list_creatives and get_creative are available. Mutating tools are not listed and return read_only_token if called.
  • It expires after 90 days (or when the key that minted it expires, if sooner); at most two are active per store (422 too_many_connector_tokens).
  • It only works at its own URL. Presented as a header, Bearer token or query parameter anywhere on the REST API it is refused with 401.
  • Revoke it from Settings → Developer in the BetterReviews app (it appears as mcp-connector) or with DELETE /api/v1/mcp/connector-tokens/:id. Revocation is immediate.

Managing connector tokens requires a Read & Write API key: POST mints, GET /api/v1/mcp/connector-tokens lists the active ones as {"data": [...]} (id, prefix, last used, expiry — never the URL), DELETE /:id revokes.


Tools

ToolAuthArgumentsReturns
search_reviewseitherquery (required, ≤500 chars), product_id, min_rating 1–5, limit 1–50reviews[], total, has_more. Natural-language search over the corpus. Requires an active paid subscription — the Free plan gets subscription_required.
get_revieweitherreview_idRating, title, content, display name, status, stage, rights_status, tags, media count. Never the reviewer's email.
top_reviewseitherproduct_id (required, ≤64 chars), objective (marketing | helpful, default marketing), limit 1–20 (default 3), min_rating 1–5, exclude_ids (≤50 review IDs){reviews[], tier, coverage, objective}. The best reviews for one product, ranked in SQL — no model call, no AI cost. Each row carries id, product_id, rating, title, content, reviewer_display_name, is_verified_buyer, rights_status, word_count, quality_score, review_created_at and media_count — the same key names as get_review, not the REST endpoint's created_at/media[] shape. There is no require_media argument (REST only), and limit defaults to 3 here versus 5 on REST. tier is A (some reviews AI-analyzed) or C (none yet, analysis-free ranking); coverage is {total, analyzed}. Product-scoped: Review Group siblings are never pooled in. Capped at 60 calls/minute per store on top of the shared limit, in a bucket shared with GET /reviews/top.
list_claimseitherproduct_id, status, collect_enabled, include_archived, limit, offsetSame shape as GET /claims.
permission_previeweitherreview_id{reachable, masked_email} — the full address never leaves the server.
upsert_claimheader + writeCreate: product_id, claim_text, topic, source (+ optional ask_phrasings, status, collect_enabled). Update: claim_id + fields.{claim}. Same validation as the REST API (invalid_prompt_text, product_not_found, another_claim_active).
set_claim_collectionheader + writeclaim_id, enabled{claim}. One collect-enabled claim per product.
link_reviews_to_claimheader + writeclaim_id, reviews[] of {review_id, evidence_quote?, confidence?} (≤50){claim_id, reviews[], links_total} — the links written by this call plus the claim's total link count.
request_review_permissionheader + writereview_id, scope (text | photo | both, default both){permission_request_id, status}. Consent rules apply: unreachable, opted-out or suppressed reviewers are refused.
get_optimizationeitherproduct_idThe latest product-page optimization: the five widgets, status, version and the fact-check report. Same body as GET /products/:id/optimization.
list_creativeseitherchannel (ad | email), product_id, limit 1–25 (default 25), offset 0–10,000{creatives[], limit, offset, has_more}, newest first. Saved Studio creatives only (not unsaved or dismissed drafts). Each row: id, channel, angle, copy (hook + body for ads, subject + body for emails), proof_quote (the verbatim review sentence), review_id, rating, product_id (bare numeric), product_title, reviewer_display_name (the same public "Jane D." form the review widget shows, or Anonymous — never the full name or email), rights_status, permission_declined, permission_warning (see get_creative), created_at, updated_at. Only creatives whose review is still published (approved and visible) are returned — rejected, hidden or deleted reviews drop their creatives — so the count can be lower than the Studio library's.
get_creativeeithercreative_idOne creative in the same shape. permission_warning is {tone, text} (critical when the reviewer declined — a decline resets rights_status to none, so read permission_declined, not the badge), the same warning the Studio editor shows; null once the reviewer has cleared the review). Another store's id, an unsaved draft, or a creative whose review is no longer live is creative_not_found.
regenerate_optimizationheader + writeproduct_id, blocks[] (≤5), feedback (≤1000 chars), featured_review_idThe new draft's summary with accepted: true — generation runs in the background. Costs credits past the free regenerations per block; needs an active subscription. featured_review_id requires top_review to be among the blocks (invalid_featured_review_id otherwise); a review that is not eligible leaves the widget empty with a reason in the report rather than failing the call. Only the blocks enabled in the store's ppo_content.enabled_widgets setting are charged; naming a disabled block returns widget_disabled.
edit_optimization_widgetheader + writeproduct_id, widget_key, contentThe updated draft. Edited text is re-fact-checked before it can be published; a byte-identical resubmit changes nothing. removed_on_verify lists, per widget, how many sentences or quotes the check removed and why (inexact_quote, unsupported_claim, fabricated_quote).
recheck_optimizationheader + writeproduct_idThe draft with a fresh report over the widgets that had not passed. Widgets that already passed are untouched.
approve_optimizationheader + writeproduct_idThe draft, now approved. Publishing is a separate call.
publish_optimizationheader + writeproduct_idThe published optimization. Spends credits and writes the live Shopify metafields; refused with unverified_content while any widget has not passed the fact check. omitted_widgets lists the blocks left off the live page and why: insufficient_evidence (too little review evidence) or disabled (turned off in the store's ppo_content setting).
flag_optimization_widgetheader + writeproduct_id, widget, reason, note{flagged, refunded} plus the regeneration counters. Always recorded; refunds only when the fact-check report agrees.

The ten header + write tools also require the key's Read & Write scope — a Read-only key sees them in tools/list but calling one returns insufficient_scope.

Claim, permission, optimization and Studio creative tools are entitlement-gated (claim_collection, rights_management, product_pages, marketing_creatives) exactly like the equivalent REST operations; without the entitlement the tool returns entitlement_required. Better and Studio include them. On legacy plans (no longer sold), they are available on Growth and above, and on legacy Pro and Enterprise; stores on the legacy Starter plan do not have them. The Free plan does not include them, and neither does a cancelled store (cancelling returns the store to Free).

Customer text is data

Review content, quotes, reviewer names and creative copy are written by customers or edited by you. The server's initialize instructions tell the client to treat everything a tool returns as data, never as instructions — keep that rule if you build your own agent on top of these tools.

Errors

Two layers, matching the MCP specification:

  • JSON-RPC errors for protocol problems: -32601 unknown method, -32602 unknown tool or invalid arguments (wrong type, over-long string, out-of-range number, too many items — checked before any tool runs), -32600 for batch arrays or malformed requests, -32700 for an empty body.
  • Tool results with isError: true for domain outcomes. The text content and structuredContent carry {"error": "snake_case_code"} using the same codes as the REST API: review_not_found, claim_not_found, creative_not_found, claim_archived, product_not_found, another_claim_active, invalid_prompt_text, validation_error, missing_fields (with a fields array — upsert_claim create needs product_id, claim_text, topic and source), invalid_search, invalid_product_id (top_reviews, list_creatives — not numeric after stripping a shopify- prefix or a product GID), entitlement_required, subscription_required, read_only_token, insufficient_scope (mutating tool with a Read-only key), immutable_fields (product_id / source cannot change on update), rate_limited, search_unavailable, and the permission-request outcomes (no_reachable_email, consent_blocked, suppressed, already_requested). The optimization tools call the same implementation as their REST endpoints, so they answer the same codes: optimization_not_found, optimization_in_progress, optimization_changed, insufficient_credits, credit_limit_reached, unverified_content, invalid_blocks, widget_disabled, invalid_featured_review_id, invalid_widget_key, invalid_content, legacy_draft_regenerate and unpublished_optimization_limit.

HTTP-level: 401 bad/expired/revoked key or token, 403 credential without a store, 413 body over 256 KB, 429 rate limited (120 requests/minute per store on each door; top_reviews adds a 60/minute per-store bucket shared with GET /reviews/top). Some tools also draw on per-store hourly buckets shared with the equivalent REST endpoints, so one quota covers both doors: optimization_generate 30/h (regenerate_optimization), optimization_verify 60/h (recheck_optimization and a non-empty edit_optimization_widget), optimization_feedback 20/h (flag_optimization_widget) and optimization_publish 30/h (publish_optimization); request_review_permission is capped at 10/minute and 200/day per store. Exceeding any of these returns rate_limited as a tool result. GET returns 405; DELETE returns 204.

Workflow: does our corpus support this claim?

With the header door connected in Claude Code, a typical exchange is:

  1. search_reviews — "keeps drinks hot for hours", filtered to the product.
  2. get_review on the promising hits to read the full text.
  3. upsert_claim — record the claim with a neutral topic and example ask_phrasings.
  4. link_reviews_to_claim — attach the supporting reviews with the quoted sentence and a confidence.
  5. Not enough evidence? set_claim_collection so the AI review chat can ask happy customers about the topic. Enough? permission_preview + request_review_permission before quoting a review in ads.

See the Claims API for field semantics and the API keys guide for creating and revoking keys.