← All posts

Giving an AI assistant real Google rankings with an MCP tool

· 6 min read

Ask an AI assistant where your site ranks on Google for a keyword and you'll get an answer. It will be fluent, plausible, and made up. The model has no view of today's results page. Even assistants that can browse will usually read a few pages about your topic rather than count down a live results page for a specific market.

This is exactly the kind of question that should be a tool call: there's a single correct number, it changes daily, and the model can't know it. So I wrapped RankJot's rank lookup as a Model Context Protocol tool. Here's the whole thing, and the few decisions that turned out to matter.

The endpoint underneath

The tool is a thin wrapper around one HTTP call:

curl "https://rankjot.com/api/rank?domain=example.com&keyword=best+running+shoes&country=us" \
  -H "Authorization: Bearer rjk_your_key_here"

It returns the organic position (or null), the URL that ranks, and the top ten organic results:

{
  "domain": "example.com",
  "keyword": "best running shoes",
  "country": "us",
  "found": true,
  "position": 7,
  "url": "https://example.com/running-shoes",
  "results": [ ... top 10 organic results ... ],
  "quota": { "used": 214, "limit": 5000 }
}

The MCP server, all of it

It's short enough to paste. Save it anywhere as rankjot_mcp.py and install the two dependencies with pip install "mcp>=2.2" httpx. It's written for version 2 of the Python MCP SDK, where FastMCP was renamed MCPServer — code written for version 1 fails on import there.

import os

import httpx
from mcp.server import MCPServer

API_KEY = os.environ.get("RANKJOT_API_KEY", "")
BASE_URL = os.environ.get("RANKJOT_BASE_URL", "https://rankjot.com").rstrip("/")

mcp = MCPServer("rankjot")


@mcp.tool()
def check_rank(domain: str, keyword: str, country: str = "us") -> dict:
    """Check where a domain ranks on Google for a keyword.

    Args:
        domain:  the site to look up, e.g. "example.com" (scheme/www/path ignored).
        keyword: the search query to rank for.
        country: ISO country code for the Google market (default "us"), e.g. "gb", "de".

    Returns a dict with the 1-based `position` (or null if not found), the ranking
    `url`, the top-10 organic `results`, and your remaining `quota`.
    """
    if not API_KEY:
        return {"error": "config", "message": "Set RANKJOT_API_KEY to your rjk_ key."}
    try:
        resp = httpx.get(
            f"{BASE_URL}/api/rank",
            params={"domain": domain, "keyword": keyword, "country": country},
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=30,
        )
    except httpx.HTTPError as ex:
        return {"error": "network", "message": str(ex)}
    if resp.status_code != 200:
        try:
            return resp.json()
        except Exception:
            return {"error": "http", "status": resp.status_code, "message": resp.text[:200]}
    return resp.json()


if __name__ == "__main__":
    mcp.run()

Then register it with your client. For Claude Desktop, in claude_desktop_config.json, use the absolute path to wherever you saved the file:

{
  "mcpServers": {
    "rankjot": {
      "command": "python",
      "args": ["/absolute/path/to/rankjot_mcp.py"],
      "env": { "RANKJOT_API_KEY": "rjk_your_key_here" }
    }
  }
}

Restart the client and ask it something like “where does example.com rank for best running shoes in the UK?” It should call check_rank with country="gb" instead of guessing.

Decisions that mattered

The docstring is the interface

The model never sees the code, only the tool's name, its parameters and that docstring. The docstring is the only place to tell it that country exists and changes the answer. Without that line the model quietly checks the US market for everyone, which is the single most common way a ranking comes out “wrong” — I wrote up why rank checks disagree separately.

Return the top ten, not just the number

A bare position answers the question and nothing after it. With the ten results that outrank or surround you, the assistant can handle the obvious follow-ups — who's above me, is it the same competitor for every keyword, is the #1 result a forum thread — without spending another call.

Errors are data, not exceptions

Every failure comes back as an ordinary dictionary with an error field. If the tool raised instead, many clients would show the model a generic “tool failed”, and it would either retry pointlessly or apologise vaguely. Given {"error": "quota_exceeded", ...} it can tell you exactly what happened and stop.

One call, one credit — and the model can see the meter

Each call spends one lookup from a monthly budget. Models are cheerful about calling tools in loops: ask about “my top keywords” and you may get twenty calls. That's why every response carries the quota object — the assistant can see it's spending something, and you can see it in the transcript. If you want to be strict, say in your prompt how many lookups it may use.

Or skip the copy-paste

The same server is packaged as rankjot-mcp (open source, MIT). If you have uv, your client can run it directly — no file to save, no dependencies to install:

{
  "mcpServers": {
    "rankjot": {
      "command": "uvx",
      "args": ["rankjot-mcp"],
      "env": { "RANKJOT_API_KEY": "rjk_your_key_here" }
    }
  }
}

Or pip install rankjot-mcp and use rankjot-mcp as the command.

What it costs

Every RankJot account, including a free one, gets 25 API lookups a month — enough to set this up and see whether it's useful. Beyond that, the API plan is $20 a month for 5,000 lookups, and it includes normal rank tracking. The positions are live Google organic results for the market you ask for, page one only for now — so a site sitting at #14 comes back as not found. If you only want to look up a keyword once, the free rank checker does the same lookup with no key, and the full reference is in the API docs.

Written while building RankJot — Google rank tracking that emails you when your positions move. There's a free checker with no signup if you just want a one-off look.