Skip to Content

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-Key header, 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 from list_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, and start_agent_adoption_scan — start a live scan and return a scanId you poll. Five — create_ticket, update_ticket, move_ticket, delete_ticket, and add_ticket_comment — write to the project’s Strategic Tickets board and need a read_write API key; a read key is refused on them with insufficient_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, and get_tech_trust_dashboard take a view parameter. Left out, it is compact: 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=full returns everything in one response. Each of their responses that carries a summary opens with readingGuide — 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 pass includeSummary=true.
  • Rate limits. The free-tool routes are limited per API key: most at 1,000 requests per minute, with fetch_url held 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 example scan_not_found or api_unreachable — rather than throwing. One worth knowing: a get_<dimension>_run_detail call for a run that finished but produced no summary answers run_not_summarized, which is different from run_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

ParameterTypeNotes
projectIdstringA project’s 24-character hex ID, from list_projects. Required by every project-scoped tool.
pageinteger1-indexed page number. Default 1.
limitintegerItems per page. Default 20 (50 on list_tickets), max 100. Check pagination.hasMore to page.

The tools at a glance

ToolAreaRead-onlyWhat it does
list_projectsProjects✔List accessible projects and their status.
get_projectProjects✔One project’s per-dimension freshness and prompts.
list_competitorsCompetitors✔Competitors in a project (incl. your own domain).
get_competitorCompetitors✔One competitor’s detail and monitored pages.
get_ai_visibility_dashboardAI Visibility✔The market map, the AI Visibility Score, and the per-engine breakdown.
get_ai_visibility_historyAI Visibility✔Paginated AI Visibility check history.
get_ai_visibility_check_detailAI Visibility✔Full detail for one AI Visibility check.
get_ai_visibility_trendAI Visibility✔AI Visibility trend over time.
get_ai_sources_dashboardAI Sources✔Which pages the engines read to answer, and whether you are on them.
get_ai_sources_historyAI Sources✔Paginated AI Sources check history.
get_ai_sources_check_detailAI Sources✔Full detail for one AI Sources check.
get_positioning_dashboardPositioning✔Latest homepage-messaging analysis.
get_positioning_historyPositioning✔Paginated Positioning run history.
get_positioning_run_detailPositioning✔Full data for one Positioning run.
get_pricing_dashboardPricing✔Latest structured pricing and gap analysis.
get_pricing_historyPricing✔Paginated Pricing run history.
get_pricing_run_detailPricing✔Full data for one Pricing run.
get_content_dashboardContent✔Latest content categorization and gap analysis.
get_content_historyContent✔Paginated Content run history.
get_content_run_detailContent✔Full data for one Content run.
get_content_changelogContent✔Detected content changes per competitor over time.
get_tech_trust_dashboardTech & Trust✔Latest security, trust, and tech-stack profile.
get_tech_trust_historyTech & Trust✔Paginated Tech & Trust run history.
get_tech_trust_run_detailTech & Trust✔Full data for one Tech & Trust run.
list_alertsAlerts✔Competitive alerts across dimensions.
list_schedulesSchedules✔Monitoring schedules for the six dimensions.
get_briefingStrategic Briefing✔The synthesized Strategic Briefing.
get_briefing_historyStrategic Briefing✔Past briefing editions, newest first.
get_briefing_editionStrategic Briefing✔One past edition in full, by runId.
list_ticketsStrategic Tickets✔The project’s tickets, a page at a time, with filters and sorts.
get_ticketStrategic Tickets✔One ticket in full, description included.
create_ticketStrategic Tickets—Open a ticket on the board. Needs a read_write key.
update_ticketStrategic Tickets—Change a ticket’s fields, never its column. Needs a read_write key.
move_ticketStrategic Tickets—Move a ticket to another column, or reorder it. Needs a read_write key.
delete_ticketStrategic Tickets—Delete a ticket and its thread. Needs a read_write key.
list_ticket_commentsStrategic Tickets✔A ticket’s thread, oldest first.
add_ticket_commentStrategic Tickets—Add an entry to a ticket’s thread. Needs a read_write key.
list_ticket_labelsStrategic Tickets✔The labels a ticket in this project may carry.
list_ticket_assigneesStrategic Tickets✔The people a ticket can be assigned to.
check_sitemapFree Tools✔Live sitemap analysis for any domain.
check_ai_crawlersFree Tools✔Which AI assistants can fetch any domain’s pages.
fetch_urlFree Tools✔Fetch and clean any public URL.
start_tech_stack_scanFree Tools—Start an async tech-stack scan.
get_tech_stack_scanFree Tools✔Poll a tech-stack scan.
start_trust_signals_scanFree Tools—Start an async trust-signals scan.
get_trust_signals_scanFree Tools✔Poll a trust-signals scan.
start_agent_adoption_scanFree Tools—Start an async Agent Adoption Check.
get_agent_adoption_scanFree 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.

ParameterTypeRequiredDescription
projectIdstringYesProject 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.

get_competitor

Returns one competitor’s detail, including the pages CompetLab monitors (homepage and pricing-page URLs).

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
competitorIdstringYesCompetitor 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
includeAnswersbooleanNoDefault false. Also return the models’ raw answers. Large payload — see The answers block.
providerstringNoOne of openai, claude, gemini, perplexity, google_ai_overviews. Return only that engine’s answers. Requires includeAnswers=true.
brandstringNoReturn only the entries for this domain, across every answer. Requires includeAnswers=true.
promptIndexintegerNoReturn only the answers for this prompt. Zero-based. Requires includeAnswers=true.
viewstringNocompact (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.
mapOffsetintegerNoMarket-map rows to skip in compact view, for the next page. Zero-based. summary.marketMap.brandsPage.hasMore says a next page exists.
mapLimitintegerNoMarket-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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems per page. Default 20, max 100.
viewstringNocompact (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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
checkIdstringYesCheck ID, from get_ai_visibility_history.
includeAnswersbooleanNoDefault false. Also return the models’ raw answers. Large payload — see The answers block.
providerstringNoOne of openai, claude, gemini, perplexity, google_ai_overviews. Return only that engine’s answers. Requires includeAnswers=true.
brandstringNoReturn only the entries for this domain, across every answer. Requires includeAnswers=true.
promptIndexintegerNoReturn only the answers for this prompt. Zero-based. Requires includeAnswers=true.
includeSummarybooleanNoWhether 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.
viewstringNocompact (the default) or full, as on the dashboard. A paging parameter beside view=full is refused with paging_requires_compact_view.
mapOffsetintegerNoMarket-map rows to skip in compact view. Zero-based.
mapLimitintegerNoMarket-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. now is never the latest check alone; that is get_ai_visibility_history with limit=1.
  • A reading is presence.answersNaming of presence.answersReceived, never of queries sent, with a 95% range and a zone. Call presenceChange a rise or a fall only when presenceChangeSeparable is true; otherwise give both shares and say the ranges overlap.
  • start is null when the window holds one reading — no movement to compare, never zero change.
  • A rank is null on a company no answer named, and rankChange is null wherever either end has no rank. Say not named in any answer, never a place or a fall.
  • A score of 0 is 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. Read presence beside it to tell the two apart. score is where a brand lands when named, never who is ahead — standing rests on presence.
  • An empty enginesBacking means 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
dateFromstringNoStart 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.
dateTostringNoEnd of the window, ISO-8601.
providerstringNoOne of openai, claude, gemini, perplexity, google_ai_overviews. Reads that model’s own slice of every map; omit for every model at once.
detailstringNoseries 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:

FilterWhat it narrows
providerThe answers array (and the matching unansweredQueries) to one model.
promptIndexThe answers array (and the matching unansweredQueries) to one prompt.
brandNot 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
includeAnswersbooleanNoDefault false. Also return the engines’ raw answers and the pages each retrieved. Large payload — see Reading an AI Sources answer.
enginestringNoOne of perplexity, google_ai_overviews. Returns only that engine’s answers. Requires includeAnswers=true. Narrows the answer arrays only — nothing under summary moves.
promptIndexintegerNoReturn only the answers for this buying question, across every engine. Zero-based. Requires includeAnswers=true.
viewstringNocompact (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.
pagesHoststringNoReturn 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.
pagesOffsetintegerNosummary.pages rows to skip in compact view. Zero-based. summary.pagesPage.hasMore says a next page exists.
pagesLimitintegerNoRows of summary.pages per page in compact view. Default 10, max 100.
brandsOffsetintegerNosummary.brands rows to skip in compact view. Zero-based. summary.brandsPage.hasMore says a next page exists.
brandsLimitintegerNoRows 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
checkIdstringYesCheck ID, from get_ai_sources_history.
includeAnswersbooleanNoDefault false. Also return the raw answers — see below.
enginestringNoOne of perplexity, google_ai_overviews. Requires includeAnswers=true.
promptIndexintegerNoZero-based buying-question index. Requires includeAnswers=true.
includeSummarybooleanNoWhether 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.
viewstringNocompact (the default) or full, as on the dashboard. A paging parameter beside view=full is refused with paging_requires_compact_view.
pagesHoststringNoReturn only the pages on this host, as on the dashboard.
pagesOffsetintegerNosummary.pages rows to skip in compact view. Zero-based.
pagesLimitintegerNoRows of summary.pages per page in compact view. Default 10, max 100.
brandsOffsetintegerNosummary.brands rows to skip in compact view. Zero-based.
brandsLimitintegerNoRows 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:

ArrayWhat it meansHow to report it
answersThe engine answered.An empty companiesNamed is an answer that recommended nobody — a real finding, not a gap.
noAnswerShownThe 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”.
unansweredQueriesWe 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.

get_positioning_history

Paginated history of Positioning monitoring runs.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems per page. Default 20, max 100.

get_positioning_run_detail

Full competitor-by-competitor data for one historical Positioning run.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
runIdstringYesRun 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.

get_pricing_history

Paginated history of Pricing monitoring runs.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems per page. Default 20, max 100.

get_pricing_run_detail

Full competitor-by-competitor data for one historical Pricing run.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
runIdstringYesRun 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.

get_content_history

Paginated history of Content monitoring runs.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems per page. Default 20, max 100.

get_content_run_detail

Full competitor-by-competitor data for one historical Content run.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
runIdstringYesRun 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems per page. Default 20, max 100.
competitorIdstringNoFilter by competitor ID, from list_competitors.
categorystringNoOne of blog, docs, tools, landing, legal, caseStudies, comparison, integrations, changelog, webinars, programmatic, other.
allUrlsPerCategorybooleanNoDefault 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
viewstringNocompact (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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems per page. Default 20, max 100.

get_tech_trust_run_detail

Full competitor-by-competitor data for one historical Tech & Trust run.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
runIdstringYesRun 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems per page. Default 20, max 100.
dimensionstringNoOne of tech-trust, content, positioning, pricing, ai-visibility, ai-sources.
severitystringNoOne of critical, high, medium, info.
competitorIdstringNoFilter 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.

ParameterTypeRequiredDescription
projectIdstringYesProject 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
sectionsstring[]NoWhich sections to return. Default ["hub"]. See the values below.
includeChartsbooleanNoDefault 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-status

There’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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
pageintegerNoPage number. Default 1.
limitintegerNoItems 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
runIdstringYesBriefing run ID, from get_briefing_history.
sectionsstring[]NoWhich sections to return. Default ["hub"]. Same values as get_briefing.
includeChartsbooleanNoDefault 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), and dismissed (we will not do this). A project cannot add to the set.
  • A person names a ticket by its number — #14 in the app, held as the integer 14. Every tool that takes a ticketId also takes the number as a person writes it, "#14", and so do move_ticket’s beforeId and afterId. On list_tickets the same ticket is number: 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 dismissed instead. Read deletable on 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
statusstring or string[]NoReturn 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.
sortstringNoboard (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.
pageintegerNoWhich page, from 1 (the default).
limitintegerNoTickets per page, 1 to 100; 50 when omitted. Pass 1 when you only need pagination.total and byStatus.
originstringNoWho opened the ticket, never which dimension it is about (that is dimension). One of user, api, briefing, ai_sources (reserved; opens none today).
dimensionstringNoReturn 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.
briefingRunIdstringNoReturn only the tickets one briefing edition opened — its runId, from get_briefing or get_briefing_history.
assigneestringNoReturn only the tickets one person owns — their user ID from list_ticket_assignees, or none for the tickets nobody owns.
labelIdstringNoReturn only the tickets carrying one label — its ID from list_ticket_labels.
numberintegerNoReturn the one ticket carrying this number on the board — what a person means by #14. Minimum 1.
qstringNoReturn 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.
impactMinintegerNoReturn 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.
effortstring or string[]NoReturn 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.
maxMinutesintegerNoReturn 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"].
dueFromstringNoReturn only the tickets due on or after this calendar day, YYYY-MM-DD. Tickets with no due date are left out.
dueBeforestringNoReturn 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.
activeSincestringNoReturn 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.
includestring[]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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
ticketIdstringYesThe 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
titlestringYesThe ticket’s title.
statusstringYesThe column the ticket opens in. One of triage, todo, in_progress, done, dismissed.
descriptionstringNoThe ticket’s description, in Markdown.
labelIdsstring[]NoLabel IDs from the project’s list, from list_ticket_labels.
assigneeUserIdstringNoThe user ID of a current member of the organization, from list_ticket_assignees.
dueDatestringNoThe day the ticket is due, as YYYY-MM-DD — a calendar day, with no clock and no time zone.
effortstringNoHow much work the ticket is. One of low, medium, high.
impactintegerNoHow 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
ticketIdstringYesThe ticket’s ID from list_tickets, or its number as a person writes it: "#14".
titlestringNoThe ticket’s title.
descriptionstringNoThe ticket’s description, in Markdown. An empty string clears it.
labelIdsstring[]NoLabel IDs from the project’s list. The list replaces what the ticket holds; an empty list clears them.
assigneeUserIdstring or nullNoA current member’s user ID, from list_ticket_assignees, or null to leave the ticket unassigned.
dueDatestring or nullNoThe day the ticket is due as YYYY-MM-DD, or null to take it off.
effortstring or nullNoOne of low, medium, high, or null to take it off.
impactinteger or nullNoFrom 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
ticketIdstringYesThe ticket’s ID from list_tickets, or its number as a person writes it: "#14".
statusstringYesThe column the ticket ends up in — always the destination, even when it is the column it already sits in.
positionstringNotop or bottom of the destination column, without reading it first. Name a position or neighbours, not both.
beforeIdstringNoThe ticket that will sit directly above this one — its ID, or its number: "#14".
afterIdstringNoThe 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
ticketIdstringYesThe 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
ticketIdstringYesThe 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.

ParameterTypeRequiredDescription
projectIdstringYesProject ID, from list_projects.
ticketIdstringYesThe ticket’s ID from list_tickets, or its number as a person writes it: "#14".
bodystringYesThe 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.

ParameterTypeRequiredDescription
projectIdstringYesProject 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.

ParameterTypeRequiredDescription
projectIdstringYesProject 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.

ParameterTypeRequiredDescription
domainstringYesDomain to scan, e.g. example.com.
sitemapUrlstringNoFull 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. Quote unreadSitemapCount, never unreadSitemaps.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 — truncated alone, 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.

ParameterTypeRequiredDescription
domainstringYesDomain to scan, e.g. example.com.
industrystringNoIndustry 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.
  • modelTrainingAccess is 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 under modelTrainingAccess[].decidedByCrawlers also appears under assistantAccess[].decidedByCrawlers — Google-Extended is the documented case — that block does cost visibility. Match on userAgentToken before 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.

ParameterTypeRequiredDescription
urlstringYesTarget URL. Must be http(s) and resolve to a public host.
bodyNeededbooleanNoInclude body and contentType in the response.
headersNeededbooleanNoInclude 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.
cleanHtmlbooleanNoFor HTML responses, strip scripts/styles/comments and keep text. Requires bodyNeeded.
maxTimeoutMsintegerNoTimeout budget in ms. Range 1000–120000.
bodyMaxBytesintegerNoResponse 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.

ToolParameterTypeRequiredDescription
start_tech_stack_scandomainstringYesDomain to scan, e.g. example.com.
get_tech_stack_scanscanIdstringYesScan 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.

ToolParameterTypeRequiredDescription
start_trust_signals_scandomainstringYesDomain to scan, e.g. example.com.
get_trust_signals_scanscanIdstringYesScan 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.

ToolParameterTypeRequiredDescription
start_agent_adoption_scandomainstringYesDomain to scan, e.g. example.com.
get_agent_adoption_scanscanIdstringYesScan ID, from start_agent_adoption_scan.

Next steps

Last updated on