MCP server

Reports: get_report and get_answers_report

get_report and get_answers_report are one dimensional aggregator behind two tools. get_report(kind='brands', dimensions=[]) returns exactly get_overview's rows — same numbers, same order — so anything you already know from the console tools about a workspace's standing still holds; these two tools let you slice it by time, engine, topic, tag, prompt, country, answer, brand or location instead of taking the fixed shape.

Arguments

Both tools take the public Window (start_date, end_date, compare_to, engines, country_codes, topic_id, tag_ids, tag_mode, brand_ids, brand_mode, prompt_id, location_id, include_archived, features, source_bucket, gap) plus:

  • kind — 'brands' | 'domains' | 'urls' (get_report only; get_answers_report has no kind)
  • dimensions — up to 3, from date, week, month, engine, topic_id, tag_id, prompt_id, country_code, answer_id, brand_id, location; default []
  • filters — clauses over dimension and identity columns; default []
  • having — clauses over metric columns; default []
  • order_by — { column, direction: 'asc' | 'desc' }[]; default the kind's primary metric descending (visibility for brands, citation_count for domains and urls, chat_count for answers)
  • fields — optional column projection, intersected against the tool's declared columns
  • limit — integer, max 2000, default 200. It counts returned rows; a page can come back smaller than limit when the rows are wide, because a response is capped at 100 KB. page.limit is the size you actually got and page.cursor continues from there — always page by page.cursor, never by adding your own limit to the last offset.
  • cursor — opaque offset string, from a previous call's page.cursor

A clause is {column, op, value} with op one of eq, ne, in, not_in, lt, lte, gt, gte. in and not_in take a non-empty ARRAY of values; the other six take a single scalar (string, number, boolean or null) — the wrong shape is a schema validation error, rejected before the tool runs, rather than a report that silently comes back empty. Every clause column is validated against the columns the request would actually emit before any query runs or any row is folded — an unknown column is an invalid_argument error, not a silent no-op or an empty result.

An engine clause is written in public engine names and mapped to the internal channel ids the column holds, for every op. google_ai covers TWO channels, so it expands to both under eq/ne/in/not_in but is an invalid_argument under lt/lte/gt/gte, which have no pair of ids to compare against — name one channel instead.

order_by on engine sorts by the internal channel id, not the public engine name (chatgpt, gemini, …) — the tool descriptions call this out directly. If you need engines in a specific reader-facing order, sort the returned rows yourself after mapping engine back to the public name you already have in each row.

Dimensions

Eleven dimensions, up to three per call:

  • date — the collection day, YYYY-MM-DD
  • week — ISO week, YYYY-Www (e.g. 2026-W36)
  • month — YYYY-MM
  • engine — the public engine name
  • topic_id — the topic's slug ('general'), the same value list_topics returns as its topic_id column (its id column is the uuid)
  • tag_id — fans a chat out once per tag on its prompt; an untagged prompt groups under tag_id: null
  • prompt_id
  • country_code
  • answer_id
  • brand_id — fans a chat out once per brand it mentions, on kind=domains, kind=urls and get_answers_report; it is a no-op on kind=brands, because the row entity there already is the brand (dropped before columns or keys are computed, so it never appears twice)
  • location — the market location id a VERIFIED geolocated answer was collected from (list_market_locations names it). Null for every other answer: national and explicit-place answers, a geo_local prompt's country-level answers collected before 2026-10-11 (Gemini and Google AI Mode, measurement method country_context_no_auto_locality; local prompts now run only on ChatGPT and Google AI Overview), and answers collected before location tracking. A null-location cell is therefore not "national"; read answered_geolocated / answered_country_context for the split.

Columns per kind

  • kind=brands: the dimension columns, then brand_id, brand, is_self, visibility, share_of_voice, sentiment, position, mention_count, previous_visibility, previous_share_of_voice, previous_sentiment, previous_position (identical to get_overview), then the raw sums visibility_count, visibility_total, sentiment_sum, sentiment_count, position_sum, position_count.
  • kind=domains: dimensions, then domain, classification, is_self, citation_count, chats_cited, chats_total, citation_share, mentioned_brand_count, gap_competitors, previous_citation_count.
  • kind=urls: dimensions, then url, domain, classification, title, citation_count, chats_cited, chats_total, citation_share, mean_citation_position, previous_citation_count.
  • get_answers_report: dimensions, then chat_count, answered_count, web_search_count, shopping_count, product_comparison_count, ad_count, map_count, web_search_share, shopping_share, product_comparison_share, ad_share, map_share. Shares are over answered_count.

Both tools' totals also carry answered_geolocated, answered_explicit_place, answered_country_context and answered_unknown_method — the window's answered chats by measurement method, so geolocated and explicit-place answers are never blended unlabelled — and location_count, the number of locations that answered.

Peec's gap and mentioned_brand_count numeric filters on the domain report are having clauses over gap_competitors and mentioned_brand_count here.

Formulas

  • visibility = visibility_count / visibility_total
  • share_of_voice = mention_count / Σ mention_count in the cell
  • sentiment = (sentiment_sum / sentiment_count / 2 + 0.5) × 100
  • position = position_sum / position_count
  • citation_share = citation_count / citations in the cell

Each is null when its denominator is 0 — a cell nothing was measured in is not a cell measured at zero.

Previous period rule

When compare_to = 'previous_period' (the default) and none of date, week, month is a requested dimension, the equal-length prior period is computed the same way and its four presented metrics are attached as previous_visibility, previous_share_of_voice, previous_sentiment, previous_position for kind=brands, or previous_citation_count for kind=domains/kind=urls. A time dimension already splits the window by time, so a second period would double-count it — no previous_* values are computed when one is present, and the columns stay in the shape but hold null. A previous cell that answered nothing at all leaves previous_* null (a delta from nothing is not a delta from zero); a previous cell that answered chats but simply never mentioned the brand or cited the domain/url reports 0 — that IS a measurement.

Paging

limit (max 2000, default 200) and cursor (from the previous call's page.cursor) page through the result the same way every other list tool does. A page may come back smaller than the limit you asked for when the full page would exceed 100 KB serialized — the response's own limit says how many rows actually came back, and page.cursor still continues correctly from there. Reports (this page) and list_fanout_queries report row_count as the total row count across every page. search_answers and list_prompt_candidates instead report row_count as the rows on just the page you got back, and rely on page.has_more to say whether another page exists.

Worked calls

Brands by engine for a week:

{
  "workspace_id": "…",
  "start_date": "2026-09-01",
  "end_date": "2026-09-07",
  "kind": "brands",
  "dimensions": ["engine"]
}

Returns one row per (engine × brand): visibility, share of voice, sentiment, position, mention count and the raw sums, ordered by visibility descending (the default for kind=brands).

Domains with a competitor gap:

{
  "workspace_id": "…",
  "start_date": "2026-08-01",
  "end_date": "2026-08-31",
  "kind": "domains",
  "having": [{ "column": "gap_competitors", "op": "gte", "value": 1 }],
  "order_by": [{ "column": "chats_cited", "direction": "desc" }]
}

Returns domains cited in at least one chat where a tracked competitor was also cited — Peec's gap filter on the domain report — ordered by the number of distinct chats citing them.

Presets

Twelve narrower analytics tools sit on top of get_report/get_overview so you do not have to hand-build the same dimensions/filters/having every time you want a common shape — a visibility summary, share of voice by brand, a citation-gap scan. Each one is a fixed-shape projection over the same aggregator this page describes (or, for the three marked below, over another read this server already exposes), never a computation of its own:

ToolEquals
get_visibility_summarythe is_self row of get_report(kind=brands) (== get_overview), one row, plus answered_chats/total_chats (raw answer rows) and answered_observations (the pooled denominator)
get_share_of_voiceget_report(kind=brands) narrowed to the share-of-voice columns — same brand set and order as get_overview
get_brand_mentionsget_report(kind=brands, dimensions=[engine], filters=[{is_self eq true}])
get_competitor_mentionsget_report(kind=brands, dimensions=[engine], filters=[{is_self eq false}])
get_visibility_trendsget_report(kind=brands, dimensions=[date]) narrowed to the trend columns
list_citationsget_report(kind=urls) narrowed to the citation columns
find_citation_gapsget_report(kind=domains, having=[{gap_competitors gte 1}]) with the is_self rows dropped
find_source_opportunitiesget_report(kind=domains, filters=[{classification in [Editorial, Reference, UGC, Institutional]}, {is_self eq false}], order_by=[{chats_cited desc}])
compare_date_rangestwo independent get_report(kind=brands) calls, one per date range, joined on brand_id; compare_to is forced to none since the tool IS the comparison
compare_engine_answersnot a get_report call — search_answers narrowed to one prompt_id, the newest answer per engine
get_prompt_volatilitynot a get_report call — a dedicated reader over daily per-prompt visibility: mean, min, max and standard deviation across the window
get_winback_targetsnot a get_report call — a dedicated reader over a fixed 30-day standings window: prompts a competitor currently leads

The nine rows above that name a get_report call are guaranteed to agree with it — an invariant test asserts several of them equal get_report/ get_overview row for row on their shared columns, and every other preset has its own test against the exact aggregator or reader it projects, so none of the twelve can silently drift from the compute underneath it.

← All docs