| gsc_list_sitesA | List every Search Console property this account can see. Costs one Search Console API call (sites.list) — no per-property
quota is spent. Persists each property into the local store (upserted
by property URL) so other tools can route a page URL to its property
without another network round trip; an existing row's sitemaps are
left untouched here, since this call does not fetch or change them. Returns [{"property", "host", "permission"}, ...] sorted by
property. On a missing, expired, or rejected token, returns
{"ok": False, "error": "auth_required", "fix": ...} instead of
raising; if no OAuth client is configured at all, returns
{"ok": False, "error": "not_configured", "fix": ...} instead — in
either case the caller can surface fix directly rather than parsing
an exception. A Search Console error returns {"ok": False, "error": "api_error", "status": <http status>, "fix": ...}, and anything else
{"ok": False, "error": "unexpected", "detail": <exception type>, "fix": ...}. An EMPTY LIST therefore means what it says — this
account really has no properties — and never a call that was refused. Does not fetch sitemaps, index status, or search analytics; see
gsc_doctor, gsc_check_status, and gsc_performance for those. A
property already known to the store keeps whatever sitemaps a prior
gsc_submit_sitemaps() call recorded against it — this call never
fetches or clears that list, so a routine refresh cannot erase it. |
| gsc_doctorA | Diagnose whether gsc-mcp is set up to talk to Search Console. Runs seven checks in order — oauth_client, token, config, store,
properties, browser, extension — and reports all of them even if one
raises. A check that raises is recorded as ok: False with the
exception's TYPE NAME only in detail; the message is never included,
because it can carry a bearer token, a credentialed URL, a raw response
body, or a filesystem path holding your account name. Every failing
check carries a non-empty fix string with a concrete next step; this
tool diagnoses, it does not repair anything itself. The last two are the local setup for browser-driven submission, and
they come last because they cost nothing and the first five establish
whether anything works at all. browser names the profile to use;
extension reports whether the bridge extension is REGISTERED in that
profile — a green check does not mean its background worker is
running, which needs a live connection and arrives with Milestone 3B.
"Could not be checked" is reported as such, never as "not installed". Costs at most one Search Console API call (sites.list, for the
properties check); the browser and extension checks are local file
reads and make none. Returns {"ok": bool, "checks": [{"name", "ok", "detail", "fix"}, ...]};
ok is true only when every check passed. |
| gsc_check_statusA | Check whether each URL is indexed by Google Search, via the URL
Inspection API. READ-ONLY: this tool inspects current index status and submits
NOTHING. It never requests indexing and never spends a
Request-Indexing slot. No tool here does yet — requesting indexing is
not built, so an assistant asked to get a URL indexed should say so
rather than reaching for this one, which answers a different question
and spends a different budget doing it. This tool DOES spend
URL Inspection quota, a separate per-property budget of 2,000 calls a
day and roughly 600 a minute (see gsc_quota) — one call per URL
inspected. concurrency defaults to the configured inspect_concurrency
(config.load()["inspect_concurrency"]) when omitted.
Properties come from the local store, the same one gsc_list_sites
populates. If the store has never been synced — e.g. this is the very
first call this install has ever made — it is synced automatically
first, so a first-ever gsc_check_status call does not fail with "no
property matches" purely because nothing has been synced yet. Returns {"rows": [...], "checked": int, "skipped_quota": [...], "quota": {...}}. "quota" is per property, and its
daily_free_at_gate/minute_free_at_gate are the headroom measured
BEFORE this call reserved its own budget — they are a record of what
the gate saw, not current headroom, and are already stale by the size
of this batch by the time you read them. gsc_quota's similarly-shaped
daily_free/minute_free are the ones measured now; do not compare
the two pairs or plan a second batch against these. binding_at_gate
(here and on each "skipped_quota" entry) carries the suffix for the
same reason: it names only the exhausted INSPECTION window as seen at
the gate, where gsc_quota's binding covers the submission budget too
and is read now. Each row is
{"url", "status", "detail", "unverified"}. status is one of: indexed, crawled_not_indexed,
discovered_not_indexed, unknown_to_google, redirect, noindex,
duplicate, alternate_canonical, not_found, soft_404, blocked_robots,
no_property, error. no_property means no Search Console property in
this account covers that URL's host. unverified is True when a
concurrent burst produced a suspect result (unknown_to_google or
error) that a sequential re-check could not confirm before quota or
time ran out — treat such a row as UNKNOWN, not as a confirmed
"not indexed". On a missing, expired, or rejected token, returns {"ok": False, "error": "auth_required", "fix": ...} instead of raising; if no
OAuth client is configured at all, returns {"ok": False, "error": "not_configured", "fix": ...}. |
| gsc_quotaA | Report Request-Indexing and URL Inspection budget for every property
the store currently knows about. Local-only: reads the store and the config file, makes no Search
Console API call, and needs no OAuth token — safe to call at any time,
including before signing in. An empty store (nothing synced yet via
gsc_list_sites or gsc_check_status) returns []. One entry per property: {"property", "submission", "inspection", "binding"}. THESE NUMBERS ARE AN ESTIMATE, NOT A MEASUREMENT, and the estimate is
biased in one direction. The store counts only the slots THIS TOOL
spent: a URL submitted by hand in Search Console, from a phone, or on
another machine never reaches it. So used is a lower bound and free
an upper bound — a property reported with headroom can still be refused
by Google, and that is expected behaviour rather than a defect. Do not
tell a user "you have N submissions left" without that caveat, and do
not conclude anything about Google's own rules from these counts.
counts and free_is_upper_bound restate this in the payload so it
survives being read without the docstring. "submission" is the Request-Indexing slot budget: {"free", "spendable_free", "used", "slots", "daily_reserve", "next_free_at", "counts", "free_is_upper_bound", "last_refusal_at"}.
last_refusal_at is the last time Google actually said Quota Exceeded
for this property (ISO-8601, or None) — the only figure in the block
that came from Google rather than from local arithmetic.
free is the RAW free-slot count and ignores daily_reserve.
spendable_free is computed against the RESERVE-ADJUSTED ceiling —
max(0, (slots - daily_reserve) - used) — NOT simply free minus
daily_reserve: that arithmetic breaks at the clamp (slots=11,
daily_reserve=2, used=10 gives free=1, but spendable_free is 0, not
-1). ACT ON spendable_free, NOT free: daily_reserve exists to hold
slots back from every tool, and a caller that submits up to free
instead will be refused once spendable_free runs out. next_free_at
is an ISO-8601 string, or None when a slot is free right now — it is
already computed against the reserve-adjusted ceiling too, so it can
report a wait even while free (the raw count) is nonzero. "inspection" is the URL Inspection API budget: {"daily_free", "minute_free", "daily_limit", "minute_limit"} (2000/day, 600/minute,
per property — the same quota gsc_check_status spends). "binding" names whichever budget is exhausted for that property right
now — "submission" (the Request-Indexing ceiling, reserve applied),
"refused" (Google said Quota Exceeded recently and a short cooldown is
running), "inspection_daily", or "inspection_minute" — or None when
every budget has headroom. On "refused", spendable_free may be
nonzero: the local estimate is being overruled by what Google actually
answered, and next_free_at is when to ask again rather than a moment
a slot is known to exist. |
| gsc_performanceA | Search Analytics performance: clicks, impressions, ctr, position. Three shapes, chosen by what is passed: No site -> one row per property the store knows about (scope: "portfolio", sites, totals aggregated across all of them). site, no dim -> a single aggregate for that site (scope: "site",
plus clicks/impressions/ctr/position).
site and dim -> per-dim rows for that site, sorted by clicks
descending (scope: <dim>, rows, totals aggregated across rows).
dim must be one of perf.VALID_DIMENSIONS ("query", "page", "country",
"device", "date", "searchAppearance").
Date window: pass start_date AND end_date (both "YYYY-MM-DD") for an
explicit range, or leave both unset and get the trailing days calendar
days ending yesterday. start_date without end_date is refused
outright -- {"ok": False, "note": "start_date needs end_date (both YYYY-MM-DD)"} -- rather than guessing an end. IMPORTANT -- data_state defaults to "all", not Google's own API default
of "final". Passing data_state="final" attaches a warning string
explaining why: data_state='final' silently omits the most recent
~perf.FINAL_LAG_DAYS (3) day(s) of data -- Google has not finished
processing them yet, and nothing in the response marks the gap. The
Search Console web UI has no such restriction and shows those days
immediately, so a 'final' query compared against what a human sees in
the UI right now will look like a discrepancy or a missed traffic
change when both are simply answering different questions. Prefer
data_state='all' (the default here) unless you specifically need
finalized-only rows; its trade-off is that the last day or two may
still revise upward on a later query. On a missing, expired, or rejected token, returns {"ok": False, "error": "auth_required", "fix": ...}; if no OAuth client is configured at all,
{"ok": False, "error": "not_configured", "fix": ...}. Any other failure
-- a bad dimension, an unroutable site, a Search Console API error --
comes back as {"ok": False, "start", "end", "note": str(exc)} rather
than raising. |
| gsc_submit_sitemapsA | Submit one or more sitemaps to Search Console (PUT, idempotent --
safe to resubmit an already-known sitemap). sitemaps is an optional list of sitemap URLs; each is routed to its
covering property via the same host-matching gsc_check_status
uses. Omit it to resubmit every sitemap already on
record for every property (the same list gsc_list_sites carries
forward on refresh).
REFUSES TO GUESS: when sitemaps is omitted and the store has no
sitemap recorded for any property, this returns {"ok": False, "note": "No sitemaps known. Pass sitemaps=[...] explicitly."} rather than
trying /sitemap.xml -- submitting a URL nobody named is an
outward-facing action against the caller's Search Console property, and
a wrong guess leaves a permanent failed submission in their sitemap
list for no reason. Returns a list of {"site", "sitemap", "http_status", "ok", "note"}
(api.submit_sitemap's shape) -- one entry per sitemap attempted, plus one
{"site": None, "sitemap", "http_status": None, "ok": False, "note"}
entry for any URL that matched no known property. Every successful
submission is recorded back onto its property's row (merged with,
never replacing, whatever sitemaps were already there), so a later bare
call resubmits it too. On a missing, expired, or rejected token, returns {"ok": False, "error": "auth_required", "fix": ...} instead of raising, once real
work is about to start (nothing has been submitted yet at that point);
if no OAuth client is configured at all, {"ok": False, "error": "not_configured", "fix": ...}. Any other failure comes back as
{"ok": False, "error": "unexpected", "detail": <exception type>, "fix": ...}. |
| gsc_detect_browsersA | List the Chromium browser profiles on this machine and recommend one. Local-only: reads the browsers' own state files, makes no network call,
spends no quota, and needs no token — safe to call before signing in.
Answers "which browser profile should I drive?", nothing else; it opens
no browser and changes no setting. PRIVACY: no email address is returned, in either direction. Not the
account signed in to a profile, not the account that authorised this
server, and not a profile display name that is itself an address. No
filesystem path is returned either — a profile path carries the
operator's account name. What identifies a profile here is its browser
and its profile directory. Returns {"ok": True, "profiles": [...], "recommended": <one of them or None>, "reasons": [...]}. profiles is a FLAT list across every
browser, ranked-flag included, not grouped by browser — the question is
which single profile to use, so each entry carries its own brand
context (browser, browser_key, extensions_url) and stands alone.
Each profile is {"browser", "browser_key", "extensions_url", "profile", "display_name", "account_on_disk", "account_discoverable", "matches_authorised_account", "has_extension", "recommended"}. extensions_url is where that browser's extensions page lives
(chrome://extensions, brave://extensions, ...). Use the value given;
do not build one from the browser key, since Chromium registers no
chromium:// scheme and uses chrome://extensions.
has_extension is TRI-STATE: true means the pairing extension is
installed in that profile, false means every preferences file was read
and it was not among them, and null means the check could not be
PERFORMED — an unreadable preferences file, or no unpacked extension
directory to match against. Read null as "not detected", never as "not
installed"; telling a user with a working install to reinstall it is
the one wrong answer this flag exists to avoid.
account_on_disk says an account address was found in that profile's
files — found, not validated: no token is checked and nobody's live
session is inspected, and on Microsoft Edge the same file records
Microsoft accounts, so treat it as a hint rather than a login check.
account_discoverable is a fact about the BRAND: Brave, Vivaldi, Opera
and plain Chromium record no Google account at all, so account_on_disk: false there means "not discoverable", NOT "nobody is signed in".
matches_authorised_account is true, false, or NULL — null means the
question could not be asked (nothing has authorised yet, or the brand
records nothing), and must not be read as "no". reasons explains the
recommendation in plain sentences.
A machine with no Chromium browser installed returns ok: true with an
empty profiles list, recommended: null, and a note saying what to
install; that is an ordinary state, not a failure. Only an unexpected
fault returns {"ok": False, "error": "unexpected", "detail": <exception type>, "fix": ...}. |
| gsc_use_browserA | Choose which browser profile this server drives, overriding detection. Local-only: writes one preference to this server's config file, makes
no network call, spends no quota, needs no token, and opens no browser.
Use it when gsc_detect_browsers recommends a profile that is not the
one holding your Search Console account — the detector can rank the
profiles it finds, but it cannot know which browser you work in. browser is the browser_key from gsc_detect_browsers ("chrome",
"brave", "edge", "vivaldi", "opera", "chromium") — the key, not the
display label. profile is the profile field from the same entry
(the profile DIRECTORY, e.g. "Default" or "Profile 3"); omit it to take
that browser's default profile. clear=True removes the pin and
returns to the detector's recommendation, and ignores the other two.
The pair is checked against the profiles that actually exist BEFORE it
is saved. A pair that matches nothing is refused with {"ok": False, "error": "browser_not_found", ...} whose fix lists the pairs that
would work, so a typo is answered immediately rather than becoming a
browser that silently never opens. Once pinned, the choice is absolute: gsc_setup, gsc_doctor and every
tool that drives the browser use it, and the ranking is not consulted.
If the pinned profile later disappears — browser uninstalled, profile
deleted — nothing falls back to a different one. Every affected tool
stops and says the pin is dangling, because driving the wrong profile
would submit URLs from whichever account happens to be signed in there. The pin survives restarts. Returns {"ok": True, "pinned": "<browser> / <profile>" or null, "note": ...}. |
| gsc_setupA | Set this server up, one step at a time. Call it, do what it says,
call it again — repeat until ok is true. IDEMPOTENT and SAFE TO CALL REPEATEDLY. It spends no Search Console
indexing quota and makes at most one API call (verifying the stored
sign-in still works). Every call re-reports the whole state from
scratch, so there is no session to resume and no order to get wrong: if
you have lost track of where setup got to, just call it again. There are four steps, checked in order: oauth_client (credentials to
sign in with), consent (the user approves Google's consent screen),
browser (a Chromium browser with a profile exists), extension (the
gsc-mcp bridge extension is loaded in the profile this server
recommends). The FIRST unsatisfied step is returned as next and the
call stops there — later steps are meaningless until it is done. Returns {"ok": bool, "done": [step], "pending": [step], "next": {"step", "action", "url"?, "path"?} | None}. ok is true, and next
is null, only when all four steps are satisfied. next.action is a
plain-English instruction to relay to the user. next.url, when
present, is the Google consent URL to open. next.path, when present,
is the folder to choose in the browser's "Load unpacked" dialog. open_browser (default true) opens the consent URL in the user's
default browser when a NEW consent is started. A repeat call while one
is already pending returns the SAME url and opens nothing — the pending
consent screen is the only one whose redirect will be accepted, so
never assume a second call means a second link.
PRIVACY: no email address, no token field, no PKCE verifier and no
authorization code is ever returned. The only filesystem path returned
is next.path, which is this server's own extension directory. Never raises. An unexpected fault comes back as next.step: "unexpected" with an action to retry. |
| gsc_request_indexingA | Submit up to five URLs to Google's Request Indexing, one at a time,
through the browser extension in your own signed-in profile. BLOCKING and slow by design: submissions are paced 130-180 seconds
apart, so five URLs can take fifteen minutes. Use
gsc_start_indexing_job for anything larger. Quota is per property — roughly eleven slots per property on a rolling
24-hour window, and properties are independent. Call gsc_quota first to
see what is spendable; act on spendable_free, not free. Returns {"ok", "submitted", "skipped", "failed", "stopped_early", "stop_reason", "notes", "results"}, with one entry per URL in
results. A run stops early on the first throttle, captcha, or
signed-out session rather than burning the rest of the batch against a
refusing server. A refusal is {"ok": False, "error", "detail", "fix"}. |
| gsc_start_indexing_jobA | Queue a background submission run over any number of URLs. Returns immediately with {"ok", "job_id", "total", "note"}. Poll
gsc_job_status for progress and gsc_stop_job to end it early. One
submission job runs at a time: the bridge drives a single browser tab
in your real profile, so a second job is refused rather than queued. |
| gsc_job_statusA | Progress and state for one submission job, or the most recent one. States: pending, running, completed, stopped_user, stopped_throttled,
failed. results holds one entry per URL attempted so far, and live
says whether a worker is still on it in this process. stop_reason says why a run ended early — "quota_exceeded", "no_quota",
"stopped_by_user". It is the only trace of a run refused at the gate,
which attempts nothing and so leaves results and error both empty.
|
| gsc_stop_jobA | Ask a running submission job to stop. It stops after the URL currently in flight, never mid-URL: a
submission already sent has spent its quota slot and its ledger row
must settle with the real outcome. |
| gsc_find_unindexedA | Which of a property's URLs are not in Google's index, and why. source chooses where candidate URLs come from: "sitemap" fetches and
parses the property's registered sitemaps fresh, "store" uses only
URLs already seen, "both" (the default) unions them.
limit caps how many URLs are INSPECTED, not how many are returned —
inspection spends a daily budget, so a cap that only trimmed the
output would pay full price for an answer it threw away. Which URLs a
capped run reaches follows the store's url ordering (alphabetical),
not staleness: a capped run is a sample, not a worst-first sweep. The
result reports candidates_total, inspected and limited so you
can tell a truncated answer from a complete one.
limit is not the only thing that can cut a run short. Inspection
quota is per property and roughly eleven slots a day, so on any site
larger than that the run reaches the gate and stops: inspected is
what was handed to the API, checked is what actually reached it,
and skipped_quota lists the URLs accounting for the difference.
Report both numbers rather than inspected alone — a run that
answered for three of forty URLs is not a survey of the property, and
the remaining URLs are answerable tomorrow at no extra cost.
Only URLs whose last inspection is older than inspection_ttl_days
are re-inspected; the rest are reported from their stored status. A
second call the same day therefore costs no budget and still answers
in full. "fresh": true on a row means only that THIS run did not
inspect it — usually because it was within the TTL, but also when
limit cut the run short before reaching it. It is not a promise
that the stored status is within the TTL. Each unindexed row carries reason (one of ten codes), action,
submitting_helps and needs_site_access. Act on
submitting_helps before calling gsc_request_indexing: submitting a
404, a redirect, a noindex, or a page Google crawled and declined
wastes an unrecoverable quota slot. URLs whose state this run did not establish are in undetermined,
never in unindexed. Read each one's status before wording the
answer: it separates two cases a reader acts on differently. "We
looked and could not tell" covers a failed inspection and a result
the burst re-verify pass could not confirm. "skipped_quota" is the
other case and means the opposite: we never got to look, because the
property's daily budget ran out first. That is not a problem with the
URL, needs no investigation, and is answered by running again
tomorrow — say so rather than reporting it as a fault. A refusal is {"ok": false, "error": <code>, "fix": <what to do>},
plus status when Search Console refused the call and detail (an
exception type name) when the failure was unexpected. |
| gsc_auditA | The current indexation position for one property, as structured data. Reads the local store only: no network call, no quota spent. It
reports what the last inspection found, which is why the payload
carries as_at — run gsc_find_unindexed first if the picture is
stale, and check the stale count to see how much of it is. Point-in-time by design. There are no movement numbers — nothing
"moved to indexed", nothing "de-indexed" — because the store keeps no
status history, and a zero in a field like that would read as a
measurement that found no change rather than as an absence of data.
basis says so in the payload. Returns counts (total_known, checked, indexed, unindexed,
undetermined, never_checked, stale), indexed_pct (null when
nothing has been checked), a by_reason histogram over the ten reason
codes, and three action counts: submittable, needs_site_access and
no_action_needed. Every reason code is in exactly one of the three,
so they sum to unindexed. submittable counts URLs a quota slot can
move, not URLs to submit right now: it includes crawled-not-indexed,
usually the largest bucket, where Google has already fetched the page
and passed — a re-crawl can reverse that, but only after the page
changes. Read each row's action before spending slots. Rendering all
this — prose, table, chart — is yours to do. |