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_reportonly;get_answers_reporthas no kind)dimensions— up to 3, fromdate, 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 (visibilityfor brands,citation_countfor domains and urls,chat_countfor answers)fields— optional column projection, intersected against the tool's declared columnslimit— integer, max 2000, default 200. It counts returned rows; a page can come back smaller thanlimitwhen the rows are wide, because a response is capped at 100 KB.page.limitis the size you actually got andpage.cursorcontinues from there — always page bypage.cursor, never by adding your ownlimitto the last offset.cursor— opaque offset string, from a previous call'spage.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-DDweek— ISO week,YYYY-Www(e.g.2026-W36)month—YYYY-MMengine— the public engine nametopic_id— the topic's slug ('general'), the same valuelist_topicsreturns as itstopic_idcolumn (itsidcolumn is the uuid)tag_id— fans a chat out once per tag on its prompt; an untagged prompt groups undertag_id: nullprompt_idcountry_codeanswer_idbrand_id— fans a chat out once per brand it mentions, onkind=domains,kind=urlsandget_answers_report; it is a no-op onkind=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_locationsnames it). Null for every other answer: national and explicit-place answers, ageo_localprompt's country-level answers collected before 2026-10-11 (Gemini and Google AI Mode, measurement methodcountry_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"; readanswered_geolocated/answered_country_contextfor the split.
Columns per kind
kind=brands: the dimension columns, thenbrand_id, brand, is_self, visibility, share_of_voice, sentiment, position, mention_count, previous_visibility, previous_share_of_voice, previous_sentiment, previous_position(identical toget_overview), then the raw sumsvisibility_count, visibility_total, sentiment_sum, sentiment_count, position_sum, position_count.kind=domains: dimensions, thendomain, classification, is_self, citation_count, chats_cited, chats_total, citation_share, mentioned_brand_count, gap_competitors, previous_citation_count.kind=urls: dimensions, thenurl, domain, classification, title, citation_count, chats_cited, chats_total, citation_share, mean_citation_position, previous_citation_count.get_answers_report: dimensions, thenchat_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 overanswered_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_totalshare_of_voice = mention_count / Σ mention_count in the cellsentiment = (sentiment_sum / sentiment_count / 2 + 0.5) × 100position = position_sum / position_countcitation_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:
| Tool | Equals |
|---|---|
get_visibility_summary | the 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_voice | get_report(kind=brands) narrowed to the share-of-voice columns — same brand set and order as get_overview |
get_brand_mentions | get_report(kind=brands, dimensions=[engine], filters=[{is_self eq true}]) |
get_competitor_mentions | get_report(kind=brands, dimensions=[engine], filters=[{is_self eq false}]) |
get_visibility_trends | get_report(kind=brands, dimensions=[date]) narrowed to the trend columns |
list_citations | get_report(kind=urls) narrowed to the citation columns |
find_citation_gaps | get_report(kind=domains, having=[{gap_competitors gte 1}]) with the is_self rows dropped |
find_source_opportunities | get_report(kind=domains, filters=[{classification in [Editorial, Reference, UGC, Institutional]}, {is_self eq false}], order_by=[{chats_cited desc}]) |
compare_date_ranges | two 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_answers | not a get_report call — search_answers narrowed to one prompt_id, the newest answer per engine |
get_prompt_volatility | not a get_report call — a dedicated reader over daily per-prompt visibility: mean, min, max and standard deviation across the window |
get_winback_targets | not 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.