CompetLab MCP Tools Reference
How to read this reference
A few things hold across every tool:
- Authentication isn’t a parameter. Your API key travels on the request (a
CL-API-Keyheader, or?api_key=on the URL) — see Connect. A tool sees exactly what your key can see over the REST API. - Most tools are project-scoped. They need a
projectId, which you get fromlist_projects. IDs are 24-character hex strings. - Read vs. state-changing. 40 of the 48 tools only read. Three —
start_tech_stack_scan,start_trust_signals_scan, andstart_agent_adoption_scan— start a live scan and return ascanIdyou poll. Five —create_ticket,update_ticket,move_ticket,delete_ticket, andadd_ticket_comment— write to the project’s Strategic Tickets board and need aread_writeAPI key; areadkey is refused on them withinsufficient_scope. Nothing writes to your projects, competitors, alerts, schedules, or settings. - Six tools default to a compact view.
get_ai_visibility_dashboard,get_ai_visibility_history,get_ai_visibility_check_detail,get_ai_sources_dashboard,get_ai_sources_check_detail, andget_tech_trust_dashboardtake aviewparameter. Left out, it iscompact: long lists come back one page at a time (the history trims each check instead), your own row — and on the AI Visibility map and history every tracked competitor’s — is always kept, and facts that repeat across rows are stated once.view=fullreturns everything in one response. Each of their responses that carries a summary opens withreadingGuide— the rule for reading each field it carries, keyed by the field’s path — so read it before you quote a figure. A check detail asked for its answers leaves its summary out unless you passincludeSummary=true. - Rate limits. The free-tool routes are limited per API key: most at 1,000 requests
per minute, with
fetch_urlheld tighter at 60 per minute. - Errors. A missing or malformed key is rejected by the server with an HTTP 401
(
api_key_missing/api_key_invalid) before any tool runs. Once a call reaches a tool, failures come back as an MCP tool error (isError: true) with a machine-readable code — for examplescan_not_foundorapi_unreachable— rather than throwing. One worth knowing: aget_<dimension>_run_detailcall for a run that finished but produced no summary answersrun_not_summarized, which is different fromrun_not_found— the run exists, it just has nothing to report.
null means we did not measure it — never zero, never empty, never “no”. A measured 0,
false, or empty list is reported as itself and is a real finding.
This is the most important rule on this page. hasFreePlan: false means we read the pricing
page and there is no free plan; hasFreePlan: null means we could not read the page at all.
advantages: [] means we compared and you lead in none; advantages: null means no
comparison happened. A tool would rather return nothing than return something it never
checked.
Treat a null as “not measured” and say so — or say nothing. Never render it as 0, and
never let it reach a user as a finding about a competitor. Plot a null rate as a break in the
line, not a zero.
One exception: a rank. rankByPresence on the AI Visibility market map and the AI Sources
brand list, and the AI Visibility trend’s rank and rankChange across every model, are null
on a company no answer named. That null is measured: say not named in any answer — never a
place, never a fall, never “not measured”.
Shared parameters
| Parameter | Type | Notes |
|---|---|---|
projectId | string | A project’s 24-character hex ID, from list_projects. Required by every project-scoped tool. |
page | integer | 1-indexed page number. Default 1. |
limit | integer | Items per page. Default 20 (50 on list_tickets), max 100. Check pagination.hasMore to page. |
The tools at a glance
| Tool | Area | Read-only | What it does |
|---|---|---|---|
list_projects | Projects | ✔ | List accessible projects and their status. |
get_project | Projects | ✔ | One project’s per-dimension freshness and prompts. |
list_competitors | Competitors | ✔ | Competitors in a project (incl. your own domain). |
get_competitor | Competitors | ✔ | One competitor’s detail and monitored pages. |
get_ai_visibility_dashboard | AI Visibility | ✔ | The market map, the AI Visibility Score, and the per-engine breakdown. |
get_ai_visibility_history | AI Visibility | ✔ | Paginated AI Visibility check history. |
get_ai_visibility_check_detail | AI Visibility | ✔ | Full detail for one AI Visibility check. |
get_ai_visibility_trend | AI Visibility | ✔ | AI Visibility trend over time. |
get_ai_sources_dashboard | AI Sources | ✔ | Which pages the engines read to answer, and whether you are on them. |
get_ai_sources_history | AI Sources | ✔ | Paginated AI Sources check history. |
get_ai_sources_check_detail | AI Sources | ✔ | Full detail for one AI Sources check. |
get_positioning_dashboard | Positioning | ✔ | Latest homepage-messaging analysis. |
get_positioning_history | Positioning | ✔ | Paginated Positioning run history. |
get_positioning_run_detail | Positioning | ✔ | Full data for one Positioning run. |
get_pricing_dashboard | Pricing | ✔ | Latest structured pricing and gap analysis. |
get_pricing_history | Pricing | ✔ | Paginated Pricing run history. |
get_pricing_run_detail | Pricing | ✔ | Full data for one Pricing run. |
get_content_dashboard | Content | ✔ | Latest content categorization and gap analysis. |
get_content_history | Content | ✔ | Paginated Content run history. |
get_content_run_detail | Content | ✔ | Full data for one Content run. |
get_content_changelog | Content | ✔ | Detected content changes per competitor over time. |
get_tech_trust_dashboard | Tech & Trust | ✔ | Latest security, trust, and tech-stack profile. |
get_tech_trust_history | Tech & Trust | ✔ | Paginated Tech & Trust run history. |
get_tech_trust_run_detail | Tech & Trust | ✔ | Full data for one Tech & Trust run. |
list_alerts | Alerts | ✔ | Competitive alerts across dimensions. |
list_schedules | Schedules | ✔ | Monitoring schedules for the six dimensions. |
get_briefing | Strategic Briefing | ✔ | The synthesized Strategic Briefing. |
get_briefing_history | Strategic Briefing | ✔ | Past briefing editions, newest first. |
get_briefing_edition | Strategic Briefing | ✔ | One past edition in full, by runId. |
list_tickets | Strategic Tickets | ✔ | The project’s tickets, a page at a time, with filters and sorts. |
get_ticket | Strategic Tickets | ✔ | One ticket in full, description included. |
create_ticket | Strategic Tickets | — | Open a ticket on the board. Needs a read_write key. |
update_ticket | Strategic Tickets | — | Change a ticket’s fields, never its column. Needs a read_write key. |
move_ticket | Strategic Tickets | — | Move a ticket to another column, or reorder it. Needs a read_write key. |
delete_ticket | Strategic Tickets | — | Delete a ticket and its thread. Needs a read_write key. |
list_ticket_comments | Strategic Tickets | ✔ | A ticket’s thread, oldest first. |
add_ticket_comment | Strategic Tickets | — | Add an entry to a ticket’s thread. Needs a read_write key. |
list_ticket_labels | Strategic Tickets | ✔ | The labels a ticket in this project may carry. |
list_ticket_assignees | Strategic Tickets | ✔ | The people a ticket can be assigned to. |
check_sitemap | Free Tools | ✔ | Live sitemap analysis for any domain. |
check_ai_crawlers | Free Tools | ✔ | Which AI assistants can fetch any domain’s pages. |
fetch_url | Free Tools | ✔ | Fetch and clean any public URL. |
start_tech_stack_scan | Free Tools | — | Start an async tech-stack scan. |
get_tech_stack_scan | Free Tools | ✔ | Poll a tech-stack scan. |
start_trust_signals_scan | Free Tools | — | Start an async trust-signals scan. |
get_trust_signals_scan | Free Tools | ✔ | Poll a trust-signals scan. |
start_agent_adoption_scan | Free Tools | — | Start an async Agent Adoption Check. |
get_agent_adoption_scan | Free Tools | ✔ | Poll an Agent Adoption Check. |
Projects
list_projects
Lists the projects your key can access, with status, competitor count, and last-monitored
time. This is the starting point — it’s how you discover the projectId values the other
tools need. It takes no parameters.
{ "name": "list_projects", "arguments": {} }Returns the accessible projects, each with status, competitor count, and last-monitored
timestamp. Each project also carries plan: trial, monitor, process, kept (monitoring stopped;
everything measured stays readable), client (a project on an Agency’s monitored slot, run as Monitor) or
pitch (ran everything once and is not monitored).
get_project
Returns one project’s details, including per-dimension monitoring freshness (Tech & Trust,
Content, Positioning, Pricing, AI Visibility), the AI monitoring prompts, and overall
status. Use it to see when each dimension was last updated. The project’s plan is here too, with the
same six values as list_projects.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
Competitors
list_competitors
Lists every competitor monitored for a project, including your own domain (marked
isOwn: true for self-comparison). Each row carries id, domain, isOwn, its preparation
status, and when it was added. There is no display name on this list — the domain is the identity.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
get_competitor
Returns one competitor’s detail, including the pages CompetLab monitors (homepage and pricing-page URLs).
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
competitorId | string | Yes | Competitor ID, from list_competitors. |
AI Visibility
The AI Visibility tools use a checkId rather than a runId: each check asks every prompt
in the project against every engine it asks — today ChatGPT, Claude, Gemini, Perplexity and
Google AI Overviews. Every project asks 8, on every plan and in the trial. Each check records
the ask it actually made, so read the engines off the check rather than assuming five.
get_ai_visibility_dashboard
Returns the market map — every company the answers named, each with its presence and a 95% range — plus the AI Visibility Score (a 0–100 reading of WHERE you land when named, not of who is ahead), the per-engine breakdown, and the prompt-market reading. Read the prompt-market reading first: it says whether these numbers describe the market you set out to watch, and its explanation text is meant to be rendered verbatim.
Then lead with the market: summary.marketMap.coreSize companies make up this market as the AI
models draw it, and your own row (isOwn) says where you sit by how often you are named —
rankByPresence, with ties sharing a rank. A null rankByPresence means not named in any
answer, never a place or a fall.
The map comes a page at a time. In the default compact view summary.marketMap.brands is one
page — the top rows, plus your own row and every tracked competitor’s when they fall outside
them — with
summary.marketMap.brandsPage {offset, limit, total, hasMore}; page on with mapOffset and
mapLimit. Quote brandsPage.total, never the rows on the page, as the size of the map: the
companies the models named, plus your own row when no answer named it. A tracked competitor missing from it was named in no answer; any other company missing
from this page is on another page or was named in no answer. The rows kept on every page repeat on
every page, so de-duplicate by domain when you read more than one. untrackedCoreBrands and
customerStanding are always read off the whole map. Compact runs 20,000–30,000 characters;
view=full returns every row in one response, up to about 200,000.
Set includeAnswers to also get what the models actually said — read
The answers block before you do.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
includeAnswers | boolean | No | Default false. Also return the models’ raw answers. Large payload — see The answers block. |
provider | string | No | One of openai, claude, gemini, perplexity, google_ai_overviews. Return only that engine’s answers. Requires includeAnswers=true. |
brand | string | No | Return only the entries for this domain, across every answer. Requires includeAnswers=true. |
promptIndex | integer | No | Return only the answers for this prompt. Zero-based. Requires includeAnswers=true. |
view | string | No | compact (the default) or full. compact pages the market map and keeps your own row and every tracked competitor’s on every page; full returns every row. A paging parameter beside view=full is refused with paging_requires_compact_view. |
mapOffset | integer | No | Market-map rows to skip in compact view, for the next page. Zero-based. summary.marketMap.brandsPage.hasMore says a next page exists. |
mapLimit | integer | No | Market-map rows per page in compact view. Default 10 or the whole core, whichever is larger; max 200. Your own row and every tracked competitor’s are added when they fall outside the page. |
summary.customer.perPrompt, when present, already breaks your result down per prompt — a
label, which models named you, and a 0–100 position score. It’s on the plain response and
costs nothing, so reach for it before includeAnswers.
get_ai_visibility_history
Paginated history of AI Visibility checks. In the default compact view each row trims its
check: summary.competitorRankings carries the first 10 companies plus every tracked competitor
and your own row, with summary.competitorRankingsPage counting the whole list — quote its
total as how many companies that check named — and summary.promptMarket comes without
perPrompt, which the dashboard and the check detail carry. That runs about 3,600 characters
per check; view=full returns every ranking and perPrompt, about 12,500 per check. The
response opens with readingGuide.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
view | string | No | compact (the default) or full. page and limit work in both. |
get_ai_visibility_check_detail
Full detail for one AI Visibility check — per-competitor comparison under
summary.competitorRankings, mention rates, AI Visibility Scores, per-engine results, and the
market map as it stood at that check. Read the summary exactly as the dashboard’s. In compact
view summary.marketMap.brands is paged as on the dashboard; view=full returns every row.
Takes the same answer parameters as the dashboard; see The answers block.
Asking for answers leaves the summary out. includeSummary defaults to the opposite of
includeAnswers, so a read for the answers comes back without the summary and a filtered answer
read stays small; pass includeSummary=true to get both. Any paging parameter returns the
summary, so paging beside includeSummary=false is refused with paging_requires_summary, and
includeSummary=false without includeAnswers=true leaves nothing to return and is refused with
nothing_to_return. The compact summary runs 20,000–30,000 characters; one model and one prompt
without it, 10,000–20,000.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
checkId | string | Yes | Check ID, from get_ai_visibility_history. |
includeAnswers | boolean | No | Default false. Also return the models’ raw answers. Large payload — see The answers block. |
provider | string | No | One of openai, claude, gemini, perplexity, google_ai_overviews. Return only that engine’s answers. Requires includeAnswers=true. |
brand | string | No | Return only the entries for this domain, across every answer. Requires includeAnswers=true. |
promptIndex | integer | No | Return only the answers for this prompt. Zero-based. Requires includeAnswers=true. |
includeSummary | boolean | No | Whether to return the check’s summary beside the answers. Defaults to the opposite of includeAnswers. Set false with includeAnswers=true when you already hold the summary; set true to get both. No filter changes a number under summary. |
view | string | No | compact (the default) or full, as on the dashboard. A paging parameter beside view=full is refused with paging_requires_compact_view. |
mapOffset | integer | No | Market-map rows to skip in compact view. Zero-based. |
mapLimit | integer | No | Market-map rows per page in compact view. Default 10 or the whole core, whichever is larger; max 200. |
get_ai_visibility_trend
How the market the AI models draw moved over a window: who is recommended more or less often, and whether your standing changed. A move is a change in how the AI models answered, never a fact about a third party’s business.
item.companies holds a row for you (isOwn), for every tracked competitor (isTracked), and
for up to 3 companies the project does not track, ordered by how often each is named on the
latest map — ties stay ties. A company with no reading in the window takes no row. Each row
carries now (the latest map) and start (the earliest in the window), and the difference
between them: presenceChange in points of share, rankChange in places (positive means it
climbed), and scoreChange.
- Every reading carries
checksAnalysed— the checks its map pools.nowis never the latest check alone; that isget_ai_visibility_historywithlimit=1. - A reading is
presence.answersNamingofpresence.answersReceived, never of queries sent, with a 95% range and a zone. CallpresenceChangea rise or a fall only whenpresenceChangeSeparableistrue; otherwise give both shares and say the ranges overlap. startisnullwhen the window holds one reading — no movement to compare, never zero change.- A rank is
nullon a company no answer named, andrankChangeisnullwherever either end has no rank. Say not named in any answer, never a place or a fall. - A
scoreof0is measured, for you and every other company alike: no counted answer named the company in its top 5, whether it was never named or named only below the scoring positions. Readpresencebeside it to tell the two apart.scoreis where a brand lands when named, never who is ahead — standing rests on presence. - An empty
enginesBackingmeans no model named the company on the latest map. Read it before saying a company is named across the market.
item.events carries what happened on the time axis, as facts: standingChanges — your own
standing moving zone and holding for two checks, the alerts you received — and
promptsLastChangedAt, when the questions last changed, so a move across that date is not the
market moving. item.window counts answers and checks: quote those, never days.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
dateFrom | string | No | Start of the window, ISO-8601 (e.g. 2026-01-01). Omit for the whole history. The window reads at most the newest 200 published checks, so on a long history set both dates. |
dateTo | string | No | End of the window, ISO-8601. |
provider | string | No | One of openai, claude, gemini, perplexity, google_ai_overviews. Reads that model’s own slice of every map; omit for every model at once. |
detail | string | No | series adds each company’s share check by check, at most 12 evenly spaced points. Omit it unless the shape between the two ends matters. |
{
"name": "get_ai_visibility_trend",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"dateFrom": "2026-01-01",
"provider": "claude"
}
}Filtering by claude reads Claude’s own slice of every map. rank and score are then null
on every reading and enginesBacking is left off the rows, because one model’s slice cannot
answer them — never read that as “no model named them”.
The answers block
get_ai_visibility_dashboard and get_ai_visibility_check_detail both return numbers by
default. Set includeAnswers=true and you get the evidence underneath them: every
prompt sent, and every brand each model named, in the order it named them, with its stated reasoning,
target audience, pricing signal, messaging keywords and differentiation claims — plus
per-model reporting status. The dashboard returns it beside its summary; the check detail returns
it without the summary unless you pass includeSummary=true.
Google AI Overviews is the exception: it answers in prose, so its entries carry a name and a
domain and nothing else — every profile field is absent, not empty. Its rank is the order of
first mention computed by CompetLab from the answer text; Google assigned no position, so never
report it as one.
Attribute it to the model, never to CompetLab. Every piece of prose in this block — descriptions, stated reasons, audiences, claims — is unverified model output about the brands that model named, including third parties CompetLab does not monitor. It is a record of what the model said, not CompetLab’s assessment of those brands. Report it as what that model said; do not republish it as fact.
A rate is a share of the answers counted, never of the queries asked. Every rate divides
by the answers that came back. A model that named nobody still answered — that’s a measured
absence, and it is not the same as a query that produced no answer at all. There are THREE
arrays because there are three outcomes: answers (the engine answered), noAnswerShown (the
engine was read and had nothing to show — today, a Google results page carrying no AI Overview)
and unansweredQueries (we could not read it). The last two are excluded from every count, and
neither is ever “not mentioned”. Never pair any of them as a fraction, and never report an empty
mentionedBy or a 0 score as “we couldn’t measure.”
Size it before you fetch it. An entry is one brand a model named, at about 1,500 characters each,
so the block grows with three things at once: how many prompts the project asks (an account
setting), how many models answered, and how many companies each answer named. No figure quoted
here can stand in for summary.totalEntries — read it and size the fetch from it. Prefer a
filter over fetching everything.
The three filters narrow differently, and the difference is the point:
| Filter | What it narrows |
|---|---|
provider | The answers array (and the matching unansweredQueries) to one model. |
promptIndex | The answers array (and the matching unansweredQueries) to one prompt. |
brand | Not the answers array. It reduces the brands list inside each answer, so every counted answer is still returned and the ones that didn’t name that domain arrive with an empty brands. |
That last row is what makes brand= the cheapest way to answer “where does this competitor
beat me, and where are they invisible” — it keeps at most one brand row per answer, and none on
the answers that did not name it, so you see both the wins and the silences in one call. It
matches brands[].domain case-insensitively; brand names are the model’s own wording and vary
between answers, so they’re never matched.
No filter changes any number under summary — those are stored, computed over the whole
check, and never recomputed for a filtered view. rank values stay stable under any filter. If
answersTruncated comes back true, the response-size cap fired and whole answers were
dropped from the end — narrow and retry.
{
"name": "get_ai_visibility_check_detail",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"checkId": "65a1b2c3d4e5f6a7b8c9d0e2",
"includeAnswers": true,
"brand": "competitor.com"
}
}AI Sources
AI Visibility asks who the engines recommend. AI Sources asks what they read to decide — and whether your site is among it. Each check puts the project’s 8 buying questions to Perplexity and Google AI Overviews, then records, per engine, which companies it named, which pages it retrieved to answer, and which of those pages name other companies and not you.
Like AI Visibility, these tools take a checkId, not a runId. Three rules govern every
number they return:
Retrieved, never cited. An engine hands back the pages it pulled while answering; it never says which it leaned on. No count here is a citation count.
Never pool the engines. They read different pages, so page counts belong to one engine and are never added together. The only cross-engine object is the core — hosts that at least two engines retrieved.
Counts are counts, never rates. Report 3 of 8 answers, never 38%. The question set is
small by design, and a share computed from it is false precision.
Every count names its universe on the same object, so quote the pair rather than the count
alone: answersNamingCustomer of answersReceived, independentPagesNamingCustomer over
pagesRead.
get_ai_sources_dashboard
Returns the stored summary of the latest published check — the same document the app shows.
It comes a page at a time. In the default compact view summary.brands and summary.pages
are pages, each with its own summary.brandsPage or summary.pagesPage
{offset, limit, total, hasMore}: page the brands with brandsOffset and brandsLimit and the
pages with pagesOffset and pagesLimit, or narrow the pages to one host with pagesHost. Your
own brand row is always on the page. Each summary.coreHosts[] row lists its pages as pageUrls
instead of page rows — read those rows with pagesHost=<host>. Quote summary.brandsPage.total,
never the rows on the page, as the length of the list: the companies the engines named, plus
your own row and any tracked competitor’s that no answer named (answersNaming: 0). summary.pagesPage.total is
a paging figure only: it counts rows of summary.pages across both engines, never how many pages
an engine retrieved — that count is per engine, on summary.perEngine. In summary.brands,
rankByPresence is null on a row no answer named: not named in any answer. Compact runs
30,000–70,000 characters; view=full returns every row, 250,000–450,000.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
includeAnswers | boolean | No | Default false. Also return the engines’ raw answers and the pages each retrieved. Large payload — see Reading an AI Sources answer. |
engine | string | No | One of perplexity, google_ai_overviews. Returns only that engine’s answers. Requires includeAnswers=true. Narrows the answer arrays only — nothing under summary moves. |
promptIndex | integer | No | Return only the answers for this buying question, across every engine. Zero-based. Requires includeAnswers=true. |
view | string | No | compact (the default) or full. compact pages summary.brands and summary.pages and keeps your own brand row on every page; full returns every row. A paging parameter beside view=full is refused with paging_requires_compact_view. |
pagesHost | string | No | Return only the pages on this host, as named on summary.coreHosts[] — the way to see which pages on a core host name you or your competitors. Page counts stay per engine. |
pagesOffset | integer | No | summary.pages rows to skip in compact view. Zero-based. summary.pagesPage.hasMore says a next page exists. |
pagesLimit | integer | No | Rows of summary.pages per page in compact view. Default 10, max 100. |
brandsOffset | integer | No | summary.brands rows to skip in compact view. Zero-based. summary.brandsPage.hasMore says a next page exists. |
brandsLimit | integer | No | Rows of summary.brands per page in compact view. Default 10, max 200. Your own row is always included. |
Lead with summary.funnel, which narrows in four steps: hosts more than one engine read →
those already naming you → those that could not be read → those you are genuinely missing from,
split into third-party hosts and tracked competitors’ own sites. On a strong brand the last step is
small because the core already names you — that reads as “already on 19 of the 25 hosts more
than one engine read”, never as “nothing found”.
The work list is every summary.coreHosts row whose status is missing: at least one page
there was read, and none of them names you. Nothing else is work — unreadable was never read
and so is never a page you are absent from, and already_named is won. A row’s ownership says
whose host it is: competitor_owned is a tracked competitor’s own site, which outreach does
not win; everything else is third_party and addressable — a publisher, a community, a review
site, or a company the engines named that the project does not track. Pages on such a company’s
own site still carry its domain in ownedBy.
summary.verdict is a condition code decided when the summary was built, never a rating and
never re-derived from the numbers: recommended_nowhere (no answer in the window named you),
named_on_most_core_hosts (you are on at least half the core, so a short work list is the
finding), or missing_from_most_core_hosts (named somewhere, absent from most of the hosts more
than one engine read). State it beside the counts it rests on.
summary.limits.sentences and each core host’s actionHint.text are payload — render them
verbatim rather than paraphrasing, and never compose your own sentence from an actionHint.code.
get_ai_sources_history
Paginated history of published AI Sources checks, newest first. Each row carries the four measured
figures per engine — answersReceived, answersNamingCustomer, pagesRead, and
independentPagesNamingCustomer as a floor/ceiling range — plus that check’s funnel.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
Abandoned checks never publish and so never appear. Watch truncated: when true, whole rows were
dropped from the end to fit a size cap and hasMore does not account for them — lower limit
rather than paging forward.
get_ai_sources_check_detail
Full detail for one check: its stored summary — the same shape as the dashboard as of that
check, read the same way and paged the same way in compact view — and, with
includeAnswers=true, the engines’ raw answers and the pages each retrieved. A read for the
answers comes back without the summary unless you pass includeSummary=true, so a filtered
answer read stays small. Paging beside includeSummary=false is refused with
paging_requires_summary, and includeSummary=false without includeAnswers=true with
nothing_to_return. The compact summary runs 30,000–70,000 characters; one engine and one
question without it, 5,000–30,000.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
checkId | string | Yes | Check ID, from get_ai_sources_history. |
includeAnswers | boolean | No | Default false. Also return the raw answers — see below. |
engine | string | No | One of perplexity, google_ai_overviews. Requires includeAnswers=true. |
promptIndex | integer | No | Zero-based buying-question index. Requires includeAnswers=true. |
includeSummary | boolean | No | Whether to return the check’s summary beside the answers. Defaults to the opposite of includeAnswers. Set false with includeAnswers=true when you already hold the summary; set true to get both. |
view | string | No | compact (the default) or full, as on the dashboard. A paging parameter beside view=full is refused with paging_requires_compact_view. |
pagesHost | string | No | Return only the pages on this host, as on the dashboard. |
pagesOffset | integer | No | summary.pages rows to skip in compact view. Zero-based. |
pagesLimit | integer | No | Rows of summary.pages per page in compact view. Default 10, max 100. |
brandsOffset | integer | No | summary.brands rows to skip in compact view. Zero-based. |
brandsLimit | integer | No | Rows of summary.brands per page in compact view. Default 10, max 200. Your own row is always included. |
Three error codes worth telling apart: invalid_check_id (malformed), check_not_found (the id
isn’t this project’s), and run_not_summarized — the check exists but has nothing to report,
because it is still running or was abandoned. Say it produced no data; don’t call it missing, and
don’t fill it in with zeros.
Reading an AI Sources answer
With includeAnswers=true the response carries three arrays that mean three different things,
and collapsing any two of them misreports the measurement:
| Array | What it means | How to report it |
|---|---|---|
answers | The engine answered. | An empty companiesNamed is an answer that recommended nobody — a real finding, not a gap. |
noAnswerShown | The engine was read and showed nothing. | ”Google showed no AI Overview for this question.” Measured, not a failure, in no denominator — and never “not named”. |
unansweredQueries | We could not read the answer. | Not measured, and in no count. Never “not mentioned”. |
The same distinction governs the question matrix, which has three cell states rather than two:
answered, no_answer_shown, and not_measured. An answered cell with pagesRetrieved: 0 is
an answer that reported no page — say “no pages reported”, never “from memory”, because the
engine hasn’t said how it answered.
Two more traps. rank on a named company is the order of first mention computed from the
answer text — the engine assigned no position, so never report it as a rank the engine gave. And
the answer text is the engine’s wording about third parties, unverified: attribute it to the
engine, never to CompetLab.
If answersTruncated is true, whole questions were dropped from the end — never part of one, so
every question still present carries every engine that answered it. Grouping by promptIndex stays
safe; narrow and retry for the rest.
{
"name": "get_ai_sources_check_detail",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"checkId": "65a1b2c3d4e5f6a7b8c9d0e3",
"includeAnswers": true,
"engine": "perplexity"
}
}Positioning
get_positioning_dashboard
Returns the latest homepage-messaging analysis for every competitor: page title, headline, tagline, value proposition, primary and secondary CTAs, key offerings, target audience, main differentiator, pricing mentions, and free-trial info.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
get_positioning_history
Paginated history of Positioning monitoring runs.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
get_positioning_run_detail
Full competitor-by-competitor data for one historical Positioning run.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
runId | string | Yes | Run ID, from get_positioning_history. |
Pricing
get_pricing_dashboard
Returns the latest structured pricing for every competitor — plans (each with a name, a
price such as $49/month, and a summary; up to five per competitor) — plus market pricing
statistics and a pricing gap analysis.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
get_pricing_history
Paginated history of Pricing monitoring runs.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
get_pricing_run_detail
Full competitor-by-competitor data for one historical Pricing run.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
runId | string | Yes | Run ID, from get_pricing_history. |
Content
get_content_dashboard
Returns the latest Content Intelligence for every competitor: sitemap URL counts, strategic URL identification, content categorization across 12 categories, sitemap structure, and a content gap analysis over 9 of them — legal, programmatic and other pages are counted, never assessed.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
get_content_history
Paginated history of Content monitoring runs.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
get_content_run_detail
Full competitor-by-competitor data for one historical Content run.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
runId | string | Yes | Run ID, from get_content_history. |
get_content_changelog
Detected content changes per competitor sitemap over time — URLs added and removed, with
per-category counts and up to three sample URLs per category by default. Filter by
competitor and/or category to scope it. Set allUrlsPerCategory: true for the full URL
list per category; high-activity competitors can produce large responses, so combine it
with filters and watch the truncated flag in the response.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
competitorId | string | No | Filter by competitor ID, from list_competitors. |
category | string | No | One of blog, docs, tools, landing, legal, caseStudies, comparison, integrations, changelog, webinars, programmatic, other. |
allUrlsPerCategory | boolean | No | Default false (up to 3 sample URLs per category). Set true for full URL lists. |
Tech & Trust
get_tech_trust_dashboard
Returns the latest Tech & Trust profile for every competitor: security headers (an A–F
grade, plus HSTS, CSP, X-Frame-Options, and X-Content-Type-Options), trust signals (26 signals
across five categories: compliance, reviews, socialProof, certifications, and
disclosures), the detected technology stack, AI access — which named assistants can obtain the
site’s pages and which model operators may train on it — and DNS infrastructure.
On aiAccess, absent is not empty and not open. Check measurement.status before reading
anything else. On could_not_measure the assistantAccess and modelTrainingAccess keys are
omitted entirely — an empty array would claim we evaluated every assistant and none can reach
the site, which is a different fact. Writing assistantAccess ?? [] reintroduces exactly the bug
this shape exists to prevent.
measured_no_policy_found is the opposite case and a real finding: the site publishes no
robots.txt, which under the standard allows every crawler, so the verdicts render and are all
open.
disclosures is the newest category — today a single signal, a linked privacy policy. Every
check carries all five categories, so the denominators above hold for every response you’ll see.
One category name collides, and it is the trap on this response. socialProof is a field on
both this dimension and the standalone trust-signals scan, spelled identically, and both have
exactly five members — so neither the name nor the count tells you they differ.
Here the five are customer logos, a customer-count claim, case studies, a money-back guarantee and a free trial. In the scan they are customer logos, hero-only logos, customer count, case studies and testimonials.
So a socialProof of 3 from this tool and 4 from a scan is not a change, not an improvement
and not a discrepancy — the two were never measuring the same set. If you hold both numbers,
report them separately or not at all.
The two share three members (logos, customer count, case studies) and differ in the other two
in each direction, which is why the totals track each other closely enough to look like drift.
They are produced by different detectors against different rule sets: this dimension is the
monitored 26-signal taxonomy, start_trust_signals_scan is a separate 34-signal set. Both now
count five categories, so the number of categories no longer tells them apart either — and
socialProof is the one name the two lists share.
It comes compact. By default what each AI crawler is — its purpose, whether it honours
robots.txt, and the evidence for that — is stated once in crawlerCatalog, keyed by the
crawler’s token, and each decidedByCrawlers item keeps the token and the rule that decided it
on that site. An explanation that carries only a code renders explanationCatalog[code]
verbatim. A token or code whose facts differ inside one response keeps them inline instead.
view=full repeats the crawler facts on every item. Compact runs about 5,000 characters per
competitor, and the response opens with readingGuide.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
view | string | No | compact (the default) or full. compact states each crawler’s facts once in crawlerCatalog and each repeated explanation once in explanationCatalog; full repeats them on every item. |
get_tech_trust_history
Paginated history of Tech & Trust runs, with completion timestamps.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
get_tech_trust_run_detail
Full competitor-by-competitor data for one historical Tech & Trust run.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
runId | string | Yes | Run ID, from get_tech_trust_history. |
Alerts
list_alerts
Returns paginated competitive alerts — detected changes across all monitored dimensions, with change diffs and action hints. Filter by dimension, severity, and/or competitor.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
dimension | string | No | One of tech-trust, content, positioning, pricing, ai-visibility, ai-sources. |
severity | string | No | One of critical, high, medium, info. |
competitorId | string | No | Filter by competitor ID, from list_competitors. |
{
"name": "list_alerts",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"dimension": "pricing",
"severity": "critical"
}
}Schedules
list_schedules
Returns the monitoring schedules for all six monitored dimensions: enabled/disabled status, interval in days, and the next- and last-run timestamps for each.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
Strategic Briefing
get_briefing
Returns the project’s Strategic Briefing — the synthesized, prioritized read across all
14 dimensions: what changed and what it means. This is the analyzed,
as-of read, not raw monitoring; for live per-dimension data use the get_<dimension>_dashboard
tools. It’s generated automatically per project: 30 days after the last run on Monitor, two weeks on Process.
What the edition recommends doing is not in item. Every move in the briefing lands on your
Strategic Tickets board — as a new ticket, most important first, or on the ticket already there
for that work. A new ticket opens in the triage column, carrying the edition it came from and, where
the edition gave them, the dimension it belongs to and its estimate of the work. The response’s tickets field
says what this edition did to the board, in one call: opened (the ids of the tickets it opened
that are still on the board), commented (the comments it wrote on tickets already there —
ticketId, commentId, kind, body),
alreadyOnBoard (the tickets its moves matched instead of opening a second one, each
with the move it matched) and recheckedUnchanged (open tickets it measured again and
found where they stood — no comment, because nothing moved). A ticket in neither commented nor
recheckedUnchanged was not measured by this edition: read that as not checked, never as
unchanged. total and byStatus count the opened tickets per column as you read, so they move
as the team works. To read the tickets themselves, call list_tickets
(Strategic Tickets) with origin='briefing' and the edition’s runId as
briefingRunId.
By default it returns the executive hub — a cheap digest (headline, top moves, and a
per-dimension verdict that names the deeper section to open next) that answers most
questions in a single call. Request extra sections only when a question needs them.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
sections | string[] | No | Which sections to return. Default ["hub"]. See the values below. |
includeCharts | boolean | No | Default false. Set true to include full chart series (larger payload). |
Valid sections values (pass any combination):
hub competitors all
deep-ai-visibility deep-ai-sources deep-positioning deep-pricing
deep-content deep-tech-trust deep-agent-readiness deep-ai-ecosystem
deep-customer-voice deep-funding-capital deep-hiring-gtm deep-landscape
deep-product-launches deep-reliability-statusThere’s one deep- section per dimension — the six monitored plus the eight researched
for the briefing, 14 in all. They are listed above in the order the briefing itself uses, so
deep-ai-sources sits second, beside deep-ai-visibility. (deep-agent-readiness is a frozen API
identifier; the dimension is Agent Adoption.) The hub’s verdicts tell you which
deep- section to open, so you rarely need to guess. all returns the entire briefing and
is large — use it for export or a full read only.
{ "name": "get_briefing", "arguments": { "projectId": "65a1b2c3d4e5f6a7b8c9d0e1" } }{
"name": "get_briefing",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"sections": ["competitors", "deep-pricing"]
}
}get_briefing returns the latest run in whatever state it is in. Check meta.status:
on done the briefing is in item; on running it’s being generated now (meta.progress
gives the step; a run finishes within two hours, so read it as running until meta.status
changes); on failed the last attempt ended without producing an edition; on null the
project has never had a briefing at all.
On running or failed, item is null — but an earlier edition is usually still
readable. Only meta.status === null means the project genuinely has nothing. Never tell
a user no briefing is available on the strength of a null item without calling
get_briefing_history first.
get_briefing_history
Lists this project’s past briefing editions, newest first — one cheap metadata row each
(runId, publication date, edition number, status, and that edition’s one-line headline
verdict). It never returns briefing content.
Use it to find which edition to open — “what did we say in April”, “how has the read
changed” — then pass the runId to get_briefing_edition. For the project’s current state
use get_briefing instead.
Runs that failed or are still generating are included too, with a null date and headline, so
a gap between two editions is explained rather than left a mystery. This is also the correct
fallback when get_briefing reports running or failed: the newest readable edition is
the most recent row here with status done.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
page | integer | No | Page number. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
Check pagination.hasMore to fetch additional pages.
get_briefing_edition
Returns one past edition in full, by runId — the same shape as get_briefing, with the
same sections and includeCharts options and the same hub default. Use it to read or
quote a specific past edition, including the last readable one when get_briefing reports
running or failed. Its tickets field says what that edition did to the board — opened,
commented, alreadyOnBoard, recheckedUnchanged — so quote that, rather than the edition’s
prose, when the question is what the team did about it.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
runId | string | Yes | Briefing run ID, from get_briefing_history. |
sections | string[] | No | Which sections to return. Default ["hub"]. Same values as get_briefing. |
includeCharts | boolean | No | Default false. Set true to include full chart series. |
A runId naming a run that failed or is still generating returns successfully with
meta.status set and item null: that run genuinely produced no edition, which is an
answer, not an error. A malformed id returns 400 invalid_run_id; an id that belongs to
another project returns 404 run_not_found.
{
"name": "get_briefing_edition",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"runId": "65a1b2c3d4e5f6a7b8c9d0e3",
"sections": ["hub", "competitors"]
}
}Run vs edition. A run is one attempt of the generator, addressed by runId, with
status running, done, or failed. An edition is the content a run produced — only a
done run has one. editionNumber counts published editions, so a failed or running run
has a runId but no editionNumber and no content.
Strategic Tickets
The Strategic Tickets board is the project’s list of work the team has decided to do — each ticket with an owner, a column, and a thread. These ten tools read and write the same tickets the team sees in the app. Every move in the briefing lands on your Strategic Tickets board — as a new ticket, most important first, or on the ticket already there for that work.
Five of these tools write, and they need a read_write API key. create_ticket,
update_ticket, move_ticket, delete_ticket, and add_ticket_comment change the board; a
read key is refused on them with insufficient_scope and can only list and read. A key’s scope
is fixed when the key is made. Like the board in the app, the ticket tools work on any plan and
after one ends.
A few things hold across the board:
- The columns are fixed:
triage(nobody has decided yet),todo(decided and not started),in_progress(being worked on),done(finished), anddismissed(we will not do this). A project cannot add to the set. - A person names a ticket by its number —
#14in the app, held as the integer14. Every tool that takes aticketIdalso takes the number as a person writes it,"#14", and so domove_ticket’sbeforeIdandafterId. Onlist_ticketsthe same ticket isnumber: 14. - What a key writes is recorded as written by an API key, not by a person, so the board can tell an automation’s tickets and comments from someone’s own.
- A ticket a Strategic Briefing opened cannot be deleted — move it to
dismissedinstead. Readdeletableon the ticket first. - Labels are the project’s own. A ticket names them by ID from
list_ticket_labels. Labels are created in the CompetLab app or over the REST API, not from here. - A ticket’s description and every comment are Markdown.
list_tickets
Lists a project’s tickets a page at a time, as
{ items, pagination: { page, limit, total, totalPages, hasMore }, byStatus }.
pagination.total counts every match across pages — quote it, never the length of items — and
hasMore says a next page exists: ask for page + 1. Every filter narrows total the same way
it narrows items. byStatus counts the matches per column whatever status you passed,
narrowed by every other filter, so limit: 1 with no other filter is the board’s census.
By default (sort: "board") the list is the board flattened: the columns in the order triage,
todo, in_progress, done, dismissed, and inside each column the order the team put them
in. Keep the order you receive — no ticket carries a position of its own. Board order is the only
one to read move_ticket neighbours from; any other sort mixes the columns, and each ticket
carries its status.
Each row is every field of the ticket except its Markdown description, which is not on the row
at all unless you pass include: ["description"] (get_ticket always has it). Narrow before you
read — the parameters carry the one-call recipes: sort: "priority" with
status: ["triage", "todo"] for what to start, maxMinutes for quick wins, dueBefore for
overdue and due this week, activeSince for what changed. A due date is a calendar day with no
time zone, so pass the customer’s own today.
To read what one briefing edition opened, pass origin: "briefing" and that edition’s runId as
briefingRunId. One edition’s tickets come back whole — finished and dismissed ones included —
so the list matches the count the briefing’s tickets field states.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
status | string or string[] | No | Return only the tickets in these columns — one, or several (["todo", "in_progress"]). One of triage, todo, in_progress, done, dismissed; omit it for every column. byStatus ignores this filter. done means the team moved the ticket there — never proof the work was good or that a measurement moved because of it. |
sort | string | No | board (the default), priority, due, or activity. priority puts the highest impact first; among the same impact the least effort, then the fewest minutes of the edition’s estimate, then the earliest due date, then board order — a missing value sorts last at each step. due puts the earliest due date first, undated last; activity puts the most recently touched first. |
page | integer | No | Which page, from 1 (the default). |
limit | integer | No | Tickets per page, 1 to 100; 50 when omitted. Pass 1 when you only need pagination.total and byStatus. |
origin | string | No | Who opened the ticket, never which dimension it is about (that is dimension). One of user, api, briefing, ai_sources (reserved; opens none today). |
dimension | string | No | Return only the tickets a Strategic Briefing opened for one part of its analysis — one of the 14 dimension keys (ai-visibility, ai-sources, positioning, pricing, content, tech-trust, agent-readiness, ai-ecosystem, customer-voice, funding-capital, hiring-gtm, landscape, product-launches, reliability-status) — or none for the tickets no part claims: everything a person or an API key opened, and a briefing’s ticket whose edition named none. agent-readiness is the key for Agent Adoption; the key predates the name and does not change. |
briefingRunId | string | No | Return only the tickets one briefing edition opened — its runId, from get_briefing or get_briefing_history. |
assignee | string | No | Return only the tickets one person owns — their user ID from list_ticket_assignees, or none for the tickets nobody owns. |
labelId | string | No | Return only the tickets carrying one label — its ID from list_ticket_labels. |
number | integer | No | Return the one ticket carrying this number on the board — what a person means by #14. Minimum 1. |
q | string | No | Return only the tickets whose title or description contains this text, compared without regard to case and matched literally; threads are not searched. A match in a description is weaker evidence than one in the title — read the ticket before calling two tickets the same work. |
impactMin | integer | No | Return only the tickets whose impact is at least this, 1 to 4 (1 Minor · 2 Moderate · 3 Significant · 4 Critical; 4 matters most). Tickets nobody sized are left out. |
effort | string or string[] | No | Return only the tickets sized as one of these — one, or several (["low", "medium"]). One of low, medium, high. Tickets nobody sized are left out. This is the ticket’s size word, not its minutes. |
maxMinutes | integer | No | Return only the tickets a Strategic Briefing opened whose edition estimated at most this many minutes of work. A ticket without an edition’s estimate is left out; for those, use effort: ["low"]. |
dueFrom | string | No | Return only the tickets due on or after this calendar day, YYYY-MM-DD. Tickets with no due date are left out. |
dueBefore | string | No | Return only the tickets due strictly before this calendar day, YYYY-MM-DD. Overdue: status: ["triage", "todo", "in_progress"], dueBefore set to today, sort: "due". Tickets with no due date are left out. |
activeSince | string | No | Return only the tickets something happened to at or after this moment — an edit, a move, or a new thread entry (lastActivityAt). ISO 8601 with its offset; a time without one is read as UTC, and YYYY-MM-DD is the start of that day in UTC. |
include | string[] | No | ["description"] to include each ticket’s Markdown description. Left out by default, because descriptions are most of a board’s size. |
{
"name": "list_tickets",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"origin": "briefing",
"briefingRunId": "65a1b2c3d4e5f6a7b8c9d0e3"
}
}Quick wins — the open tickets that matter and are small, most important first:
{
"name": "list_tickets",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"status": ["triage", "todo"],
"impactMin": 3,
"maxMinutes": 120,
"sort": "priority"
}
}get_ticket
Returns one ticket: its description, its labels resolved to name and colour, who owns it, when
it is due, how much work it is, how much it matters, and how many entries its thread holds. On a
ticket a Strategic Briefing opened, briefing names the edition it came from, the dimension it
belongs to, the edition’s estimate of the work (estimatedMinutes), and extendsTicketId — the
ticket already on the board this one builds on with a different piece of work, or null;
briefing is null on every other ticket. effort is the ticket’s size word, not its minutes,
and neither is derived from the other — when a person asks for something quick, filter
list_tickets by maxMinutes. Read deletable before proposing to delete it. A ticket ID
belonging to another project answers not found, exactly as an ID that exists nowhere does.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
ticketId | string | Yes | The ticket’s ID from list_tickets, or its number as a person writes it: "#14". |
create_ticket
writes · read_write key
Opens a ticket on a project’s board. title and status are both required — status names the
column it lands in and has no default, because where a ticket belongs depends on who opened it. A
new ticket lands at the top of its column. A board holds at most 5,000 tickets, and a create past
that is refused. A label not on the project’s list is refused. The ticket is recorded as opened by
an API key rather than by a person.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
title | string | Yes | The ticket’s title. |
status | string | Yes | The column the ticket opens in. One of triage, todo, in_progress, done, dismissed. |
description | string | No | The ticket’s description, in Markdown. |
labelIds | string[] | No | Label IDs from the project’s list, from list_ticket_labels. |
assigneeUserId | string | No | The user ID of a current member of the organization, from list_ticket_assignees. |
dueDate | string | No | The day the ticket is due, as YYYY-MM-DD — a calendar day, with no clock and no time zone. |
effort | string | No | How much work the ticket is. One of low, medium, high. |
impact | integer | No | How much the ticket matters, from 1 to 4 (1 Minor · 2 Moderate · 3 Significant · 4 Critical; 4 matters most). |
{
"name": "create_ticket",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"title": "Answer the new competitor pricing page",
"status": "triage",
"effort": "medium",
"impact": 3
}
}update_ticket
writes · read_write key
Changes a ticket’s title, description, labels, owner, due date, effort, or impact. Omit a field to
leave it as it is, send a value to replace it, and send null to clear it — except the
description, cleared with an empty string, and the labels, cleared with an empty list, because for
those an empty value is a real one. The column is never changed here: use move_ticket, so a
ticket cannot change column as a side effect of an edit.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
ticketId | string | Yes | The ticket’s ID from list_tickets, or its number as a person writes it: "#14". |
title | string | No | The ticket’s title. |
description | string | No | The ticket’s description, in Markdown. An empty string clears it. |
labelIds | string[] | No | Label IDs from the project’s list. The list replaces what the ticket holds; an empty list clears them. |
assigneeUserId | string or null | No | A current member’s user ID, from list_ticket_assignees, or null to leave the ticket unassigned. |
dueDate | string or null | No | The day the ticket is due as YYYY-MM-DD, or null to take it off. |
effort | string or null | No | One of low, medium, high, or null to take it off. |
impact | integer or null | No | From 1 to 4 (1 Minor · 2 Moderate · 3 Significant · 4 Critical; 4 matters most), or null to take it off. |
move_ticket
writes · read_write key
Moves a ticket to another column, or reorders it inside the one it is in. Name the destination
column in status, then where it goes: position — top or bottom — or the two tickets it
will sit between, beforeId directly above it and afterId directly below; name either or
both. A position and a neighbour together are refused. Omitting everything puts the ticket at the
bottom of the column — the opposite of a new ticket, which lands at the top. A neighbour that
has been deleted, or now sits in another column, is not used; if you named both and the one below
now sits above the one above, the one above is kept; if neither can be used, the ticket goes to
the bottom. A move never fails because your view of the board was a moment old.
The answer says where it landed: placement.above and placement.below are its neighbours now,
and placement.ignored names each neighbour you named that was not used, and why
(other_column usually means you read it off another column’s list). An empty ignored means
every neighbour you named was used, not that nothing sits between them: compare above and
below with what you named, and if they differ and the place matters, re-read that column in
board order (list_tickets with sort: "board" and that column alone as status) and move again.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
ticketId | string | Yes | The ticket’s ID from list_tickets, or its number as a person writes it: "#14". |
status | string | Yes | The column the ticket ends up in — always the destination, even when it is the column it already sits in. |
position | string | No | top or bottom of the destination column, without reading it first. Name a position or neighbours, not both. |
beforeId | string | No | The ticket that will sit directly above this one — its ID, or its number: "#14". |
afterId | string | No | The ticket that will sit directly below this one — its ID, or its number: "#14". |
To put a ticket at the top of todo, pass status: "todo" and position: "top" — there is no
need to read the column first:
{
"name": "move_ticket",
"arguments": {
"projectId": "65a1b2c3d4e5f6a7b8c9d0e1",
"ticketId": "#14",
"status": "todo",
"position": "top"
}
}delete_ticket
writes · read_write key
Deletes a ticket and its thread. This cannot be undone. A ticket a Strategic Briefing opened
cannot be deleted at all — move it to dismissed with move_ticket instead, so that what opened
it does not open it again. Read deletable on the ticket first; deleting one that is not
deletable is refused.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
ticketId | string | Yes | The ticket’s ID from list_tickets, or its number as a person writes it: "#14". |
list_ticket_comments
Lists a ticket’s thread, oldest first and whole — nothing pages it, so nothing is counted twice.
Each entry is Markdown and says what wrote it: a person working in the app, an API key, or a
Strategic Briefing. edited says whether an entry was rewritten after it was first written; don’t
work that out from the timestamps.
An entry’s briefing is set only on a comment a Strategic Briefing wrote and null on every
other. runId names the edition; kind says why the comment exists: result — Measured after
close: the check before the ticket opened beside the first check after it closed; basis_weaker —
Reason weaker: what the ticket rests on moved, and its reason is weaker for it; basis_stronger
— Reason stronger: the same, stronger; basis_changed — Reason changed: it moved, and the
edition cannot say whether that makes the reason weaker or stronger; basis_gone — Reason gone:
measured again, and what the ticket rests on is no longer there (for example the page no longer names
any competitor). Never a check that failed — a page we could not read is not a page that names
nobody. The body is dated facts and never a cause: report it as the comment states it, never as the
fix having worked.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
ticketId | string | Yes | The ticket’s ID from list_tickets, or its number as a person writes it: "#14". |
add_ticket_comment
writes · read_write key
Adds an entry to a ticket’s thread, written in Markdown. It is recorded as written by an API key rather than by a person, so a reader can tell it from someone’s own note. A thread holds at most 500 entries.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
ticketId | string | Yes | The ticket’s ID from list_tickets, or its number as a person writes it: "#14". |
body | string | Yes | The entry’s text, in Markdown. |
list_ticket_labels
Lists a project’s ticket labels — each one a name and a colour, in the order the project picks
them. These are the only labels a ticket may carry, and a ticket names them by ID in labelIds, so
read this before creating or updating one. A label is the team’s own vocabulary: nothing about a
ticket is decided by which labels it holds.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
list_ticket_assignees
Lists the people a ticket in this project can be assigned to — everyone who is currently a member
of the organization, each as a userId and fullName. This is where assigneeUserId comes from on
create_ticket and update_ticket: a user ID from anywhere else is refused. Somebody invited but
not yet joined is not here, because a ticket cannot be assigned to them. Nothing about a person
beyond their name and ID is returned.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Project ID, from list_projects. |
Free Tools
The free tools run live against any public domain — they don’t need a project, but they fetch or
scan a site on CompetLab’s side, so the key’s organization needs a plan; without one the call
answers subscription_required and nothing runs. Three of them are async: a start_* call kicks off a scan and returns a scanId, and the matching
get_* call polls for the result.
check_sitemap
Live sitemap analysis for any domain — discovers URLs, categorizes them by section, and
reports depth, freshness, and per-category counts. Discovery reads both the conventional
/sitemap.xml and every sitemap the site’s robots.txt declares, merged and deduplicated, so a
site publishing several gets all of them.
| Parameter | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Domain to scan, e.g. example.com. |
sitemapUrl | string | No | Full URL to a specific sitemap (with http:// or https://). Skips discovery. |
It does not find content gaps. Everything it returns is a measurement of the sitemaps it read — there is no second site in the picture, so nothing here is a gap analysis. (Gap analysis across competitors is the Content Intelligence dimension.)
status: "partial" has two causes and they are not interchangeable. It means the scan did
not cover the whole corpus — never that the site is broken.
- The site’s own defect —
unreadSitemapCount > 0: a sitemap its robots.txt declares that could not be read (a relative path instead of a full URL, a redirect off its own domain, a server that refused us). This is the reportable finding. QuoteunreadSitemapCount, neverunreadSitemaps.length— the list is capped at 20 and omits any value carrying a scheme the scan won’t repeat back, so a scan can report unread sitemaps with an empty list. That’s still the site’s defect, not a gap in the count. - Our own limits —
truncatedalone, with no count: we stopped at a cap. Filing that under “could not be read” blames the site for where we stopped.
A declared sitemap that answers 404 is neither: that is a measured absence, not a look we failed to take. And never report a count from a partial scan as the site’s total.
One thing this tool cannot tell you, however it reads: what any other crawler can reach. We fetch once, from one IP, under one redirect policy — we refuse cross-domain redirects, for one. A crawler operating under different rules may well reach what we couldn’t, so an unread sitemap is a fact about our fetch, not a verdict on the site’s reachability in general. Report what we read.
The programmatic category covers templated pages generated from a database or a
pattern. Two properties of it govern how you may report it:
- It is assigned to a group, never to a single URL — 25 or more sibling URLs under one
parent path, at least 80% of whose slugs look machine-generated. A page cannot look
generated on its own, so a small handful of code-named URLs stays
other. - It says how pages are generated, never why. A URL shape can’t tell a deliberate programmatic-SEO play from a reference database, so this is a count, not a verdict — don’t report it as a content strategy. A tax-code lookup service with 17,000 generated pages is running a product.
Evidence ships with the count: insights.sampleUrlsByCategory.programmatic carries example
URLs deliberately spread across different groups rather than five consecutive siblings, so a
reader can check the label — and so three generated sections don’t read as one. Those samples
are the only URLs an MCP caller ever sees: this tool takes domain and sitemapUrl and
nothing else, so there is no flag that returns the full URL list. Don’t go looking for one.
status is one of ok, partial, not-found, access-denied, or invalid. Two are easy
to misread: partial means a limit stopped the scan early, and it can arrive with zero URLs
(sitemap indexes nested deeper than the walker goes), while invalid means a document was
retrieved and couldn’t be parsed as XML — never that nothing arrived, which is not-found.
check_ai_crawlers
Live check of which AI assistants can fetch a site’s pages, read from its robots.txt.
assistantAccess is the answer — one verdict per assistant (ChatGPT, Claude, Perplexity,
Microsoft Copilot, Google AI Overviews, Gemini Apps), each with the crawlers that decided it named
beside it. Count that array for totals: no count is stored, and there is deliberately no overall
score.
| Parameter | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Domain to scan, e.g. example.com. |
industry | string | No | Industry context for benchmarking. One of news-media, arts-entertainment, law-government, finance-healthcare, saas-tech, ecommerce, other. |
Two things it does not tell you, both of which get misreported:
- It says an assistant is permitted to fetch the site, never that it cites it.
modelTrainingAccessis a separate, neutral fact. Blocking training crawlers costs no visibility and is a legitimate content decision — never report it as a gap or advise undoing it. The one exception is mechanical: where a token undermodelTrainingAccess[].decidedByCrawlersalso appears underassistantAccess[].decidedByCrawlers—Google-Extendedis the documented case — that block does cost visibility. Match onuserAgentTokenbefore applying the general rule.
crawlers[].ruleAudience tells you whether a rule named the crawler or a User-agent: *
catch-all swept it up. The second is usually accidental, and it’s the more actionable finding.
Check robotsTxt.read first. When the file cannot be read the tool returns the read outcome
and no verdict — no assistant access, no crawler list, no advice. A failed read is not an open
site.
{ "name": "check_ai_crawlers", "arguments": { "domain": "example.com" } }fetch_url
Fetches any public URL with automatic JS-rendering and common bot-protection handling, and
returns the body, headers, and clean-up stats. cleanHtml strips HTML noise while keeping
the text — a real token saving when an agent is about to read the page.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Target URL. Must be http(s) and resolve to a public host. |
bodyNeeded | boolean | No | Include body and contentType in the response. |
headersNeeded | boolean | No | Include headers and headersAvailable. Some sites reveal nothing — then headersAvailable is false and headers is {}, present and empty rather than missing. Branch on headersAvailable, never on whether headers exists. |
cleanHtml | boolean | No | For HTML responses, strip scripts/styles/comments and keep text. Requires bodyNeeded. |
maxTimeoutMs | integer | No | Timeout budget in ms. Range 1000–120000. |
bodyMaxBytes | integer | No | Response body cap in bytes. Range 1024–104857600 (1 KiB–100 MiB). |
At least one of bodyNeeded or headersNeeded must be true. This tool has the tighter
rate limit of the free tools — 60 requests per minute per key.
{
"name": "fetch_url",
"arguments": { "url": "https://example.com", "bodyNeeded": true, "cleanHtml": true }
}start_tech_stack_scan · get_tech_stack_scan
starts a scan
start_tech_stack_scan begins an async tech-stack detection on a domain — 117 detection
rules spanning the tech stack (hosting, frameworks, CMS, payments), the growth stack
(analytics, marketing, CRM, advertising), and the engagement stack (support, forms, video,
monitoring). It returns a scanId immediately; scans typically complete in 30–90 seconds.
get_tech_stack_scan returns the current status while running and the detected
technologies (with confidence scores) once complete — poll every 5–10 seconds.
| Tool | Parameter | Type | Required | Description |
|---|---|---|---|---|
start_tech_stack_scan | domain | string | Yes | Domain to scan, e.g. example.com. |
get_tech_stack_scan | scanId | string | Yes | Scan ID, from start_tech_stack_scan. |
{ "name": "start_tech_stack_scan", "arguments": { "domain": "example.com" } }{ "name": "get_tech_stack_scan", "arguments": { "scanId": "65a1b2c3d4e5f6a7b8c9d0e1" } }start_trust_signals_scan · get_trust_signals_scan
starts a scan
start_trust_signals_scan begins an async trust-signals analysis on a domain — 34 signals
across five categories: enterprise readiness, third-party validation, social proof, brand
authority, and risk reversal. It returns a scanId; get_trust_signals_scan returns the per-signal verdicts
and an overall tier verdict once complete. Poll every 5–10 seconds.
| Tool | Parameter | Type | Required | Description |
|---|---|---|---|---|
start_trust_signals_scan | domain | string | Yes | Domain to scan, e.g. example.com. |
get_trust_signals_scan | scanId | string | Yes | Scan ID, from start_trust_signals_scan. |
start_agent_adoption_scan · get_agent_adoption_scan
starts a scan
start_agent_adoption_scan begins an async Agent Adoption Check on a domain — 25 checks
across discoverability, access control, content readability, and agent endpoints, following
the open Agent-Adoption Specification. It returns a scanId; get_agent_adoption_scan
returns the current status while running and the complete results once finished. Poll every
5–10 seconds.
| Tool | Parameter | Type | Required | Description |
|---|---|---|---|---|
start_agent_adoption_scan | domain | string | Yes | Domain to scan, e.g. example.com. |
get_agent_adoption_scan | scanId | string | Yes | Scan ID, from start_agent_adoption_scan. |
Next steps
- Connect to the server → — configs for every client.
- Back to the overview → — how it works, auth, and the FAQ.