MCP server
Start the stdio server from the repository:
uv run google-keyword-ai
Installing the wheel puts the same console script, google-keyword-ai, on PATH.
Tools
The server exposes exactly 15 tools:
doctorsuggest_keywordsexpand_keywordsanalyze_trendsrank_keyword_demandget_keyword_metricsanalyze_competitorfind_gsc_opportunitiesresearch_keywordsplan_researchscore_runcluster_runexplain_scoreanalyze_nicheinspect_keyword
Verify the registered list without starting a transport:
uv run --all-extras python -c "from google_keyword_ai.config import Settings; from google_keyword_ai.mcp.server import build_server; print(sorted(t.name for t in build_server(Settings())._tool_manager.list_tools()))"
`rank_keyword_demand(keywords, anchor=None, language=None, country=None,
timeframe="today 12-m")` compares 2–50 unique keywords through a shared Trends
anchor (the first keyword by default). Its synchronous implementation returns
Envelope[DemandData | None], matching gkai demand. Values are percentages of
the anchor, not search volumes; unmeasured keywords, thin coverage and failed
batches carry null plus a status and a reason. Unused-widget messages travel
in data.notices, outside the envelope's warnings, and do not downgrade
completeness. See the method and limitations.
Claude Code
Create .mcp.json in the project root:
{
"mcpServers": {
"google-keyword-ai": {
"type": "stdio",
"command": "uv",
"args": ["run", "google-keyword-ai"]
}
}
}
Start Claude Code in that directory. Pass GKAI_... settings through the parent
environment, and use absolute paths for credential files. Tool structured output
is the same versioned envelope printed by the corresponding
gkai ... --format json command. Planning is a separate tool so MCP never needs a
union return type and remains shape-compatible with CLI JSON.
research_keywords takes save_run, and both research tools take the same
budget ceilings the CLI exposes (max_keywords, max_autocomplete_queries,
max_ads_calls, max_trends_calls, max_runtime_seconds). save_run is what
makes the five run-scoped tools reachable over MCP at all: they are addressed by
run_id, and a run that is not saved has none. There is deliberately no MCP
mirror of gkai run list|show|export|resume|rerun — the run id travels in the
envelope of the research call that created it.
research_keywords also accepts demand: bool = False and
demand_anchor: str | None = None. An explicit anchor implies demand. All three
scenarios rank a budget-limited subset after sorting, exclude the normalized
single-key Trends seed, and choose the top remaining candidate as the default
anchor. An explicit anchor absent from those candidates produces an empty
envelope. The same parameters on plan_research include the optional stage and
call estimate, without fetching or validating candidate membership. No tools
are added; both functions remain synchronous.
Research rows always contain nullable demand_relative, demand_status,
demand_measured_weeks, demand_weeks, and demand_reason. A null status
means the keyword was not ranked in this run, not a sixth status. Ranked keys
use measured, low_coverage, below_resolution, anchor_collapsed, or
batch_failed. stats.demand is null when disabled; otherwise it contains
anchor, requested, ranked, batches, and truncated_by_budget.
Budget truncation and missing demand numbers in ranked rows make research
partial. Demand options survive saved-run replay. See
pipeline integration for the selection
rule, experiment, budget accounting, and status semantics.
An empty answer is still an answer, and so is a refused one. A run that does
not exist, and a request the server refuses — a limit that is not positive, an
unusable date range, an unknown scenario — both come back as the ordinary
envelope with data: null and the reason in completeness_reason. It is the
same envelope, field for field, that gkai ... --format json prints for the
same input, so a caller parses one shape for every outcome.
Every tool therefore declares a nullable payload. The SDK validates a return
value against the declared type, so a tool whose payload cannot be null could
not report its own refusal: the answer would reach the caller as an opaque
Error executing tool <name> with the reason stripped out. Protocol errors are
left to the protocol — an unknown tool name, arguments that fail the input
schema — and never carry a result this project produced.
Edit on GitHub · 2026-09-07