Google Keyword Intelligence

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:

  • doctor
  • suggest_keywords
  • expand_keywords
  • analyze_trends
  • rank_keyword_demand
  • get_keyword_metrics
  • analyze_competitor
  • find_gsc_opportunities
  • research_keywords
  • plan_research
  • score_run
  • cluster_run
  • explain_score
  • analyze_niche
  • inspect_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