airanks-mcp-server
v1.0.1
Published
AIR (airanks) MCP server — AI optimization rankings, AI-file (llms.txt/ai.txt) presence, and site search for any domain, over Model Context Protocol. Same auth and API as the air CLI. https://airanks.net
Downloads
54
Maintainers
Readme
📡 airanks-mcp-server
AIR — the authoritative rankings for AI web content.
An MCP (Model Context Protocol) server that gives Claude, Cursor, and any other MCP client three tools —
air_rank,air_files,air_search— to check how a domain shows up in AI answers, straight from a conversation. 🕵️
🤔 What is AIR?
AIR (Artificial Intelligence Ranking) by airanks is AI optimization made visible — how often, and how well, AI answer engines (ChatGPT and friends) cite a site, expressed as a single 0–10 score. Check any site's AIR score live at airanks.net, or get the same read while you browse with the AIR toolbar.
This package puts that same measurement inside an AI agent itself, over MCP — so the agent can check its own citation footprint (or a competitor's) mid-conversation instead of you tabbing over to a dashboard.
📚 Table of Contents
- What is AIR?
- Architecture
- How a lookup works
- Tools
- Shared authentication
- Install & quickstart
- Configuration
- Example prompts
- Development
- The AIR family
- License
🏗️ Architecture
airanks-mcp-server is a thin, stateless stdio bridge: your MCP client spawns it as a
subprocess, it resolves credentials the same way every AIR client does, and it talks to the one
public AIR API.
flowchart LR
subgraph Client["🖥️ MCP Client"]
CD["Claude Desktop"]
CC["Claude Code"]
CU["Cursor / any MCP client"]
end
subgraph Server["📡 airanks-mcp-server (stdio)"]
T["tools.ts\nair_rank · air_files · air_search"]
A["api.ts\nhostname normalize · fetch · poll"]
AU["auth.ts\ntoken resolution"]
end
AUTH[("~/.config/air/auth.json\nshared by every AIR client")]
API[["airanks.net/api/v1"]]
CD -- stdio --> T
CC -- stdio --> T
CU -- stdio --> T
T --> A
A --> AU
AU -.reads.-> AUTH
A -- "HTTPS + Bearer" --> APIThe server itself has no config file, no database, and no cron — every call is a live HTTPS request to the AIR API. Auth is the only shared state, and it's a file the whole AIR toolchain already writes.
🔁 How a lookup works
air_rank and air_files share one code path (resolveDomain → lookupDomain). A domain seen
for the first time gets hydrated server-side, so the tool polls briefly rather than either
blocking forever or handing back a fake 0/10:
sequenceDiagram
autonumber
participant Agent as 🤖 MCP Agent
participant Srv as airanks-mcp-server
participant API as airanks.net/api/v1
Agent->>Srv: air_rank({ domain: "example.com" })
Srv->>Srv: normalizeHostname()
Srv->>API: GET /domains/example.com
alt first time seen
API-->>Srv: ai_files.status = "pending"
loop poll (3s, 12s budget)
Srv->>API: GET /domains/example.com
end
end
API-->>Srv: 200 { data, meta.dataset_version }
Srv-->>Agent: text summary + structuredContent
Note over Srv,Agent: still pending at the deadline?<br/>returns a "still gathering" note, not a fake 0/10A 429 mid-poll is honored via Retry-After and folded into the same budget rather than treated
as a hard failure.
🧰 Tools
| Tool | Input | Returns | Use it when |
|---|---|---|---|
| air_rank | domain (hostname or URL) | air_score (0–10), percentile, tracked, occurrences, phrases_count, brands_count, summary, dataset_version | "What's the AIR score for stripe.com?" |
| air_files | domain (hostname or URL) | AI-file presence booleans (llms_txt, llms_full_txt, ai_txt, robots_txt, json_ld + json_ld_types) and robots_ai_agents — a map of crawler → allowed|blocked|partial | "Does example.com have an llms.txt? Is GPTBot blocked?" |
| air_search | query (free text) | { domains, brands, phrases }, each hit carrying its own air_score | "Which domains rank for 'best project management software'?" |
All three are readOnlyHint/idempotentHint/openWorldHint — no writes, safe to auto-approve,
and every response ships both a human-readable text block and a structuredContent JSON
payload, so an agent can either read it aloud or parse it.
- Invalid domain (multi-word input, an IP,
localhost) →isError: truewith a message naming exactly what was rejected, no silent guessing. - Still gathering (first-ever lookup, not hydrated yet) → a plain-language note plus
structuredContent: { hostname, pending: true }— re-run the tool in a minute or two. - Zero search hits → a friendly "no matches, try a broader term or
air_rank" message rather than an empty array with no explanation. - Hostname normalization: lowercased,
https://added if no scheme, a leadingwww.stripped, and IPs /localhost/ bracketed IPv6 / no-dot / trailing-dot inputs rejected outright — same rules the AIR API itself enforces, kept in sync viaAPI-CONTRACT.md.
🔐 Shared authentication
One login works across every AIR client — this MCP server, the air CLI (Node/Rust/Go), and
the PHP Composer package — because they all resolve credentials in the same order and read/write
the same file:
flowchart TD
Start(["Every AIR request"]) --> Env{"AIR_API_KEY\nenv var set?"}
Env -- yes --> UseEnv["✅ use it — always attaches"]
Env -- no --> File{"~/.config/air/auth.json\nexists with a token?"}
File -- yes --> Host{"request host ==\nsaved host?"}
Host -- yes --> UseFile["✅ attach Bearer token"]
Host -- no --> Anon["🕵️ token withheld (host mismatch)\n(stops a leak if AIR_API_BASE moves)\n→ request still goes out, 401s server-side"]
File -- no --> Anon2["🚫 anonymous — 401 authentication_required\n(no token available at all)"]AIR_API_KEYenv var — explicit intent, always wins.~/.config/air/auth.json— written byair loginin any AIR client. Host-scoped: a file token only attaches to requests whose host matches the host it was minted for.- Anonymous — rejected. The API now requires a token for every caller except the official
browser toolbar; an anonymous request gets a
401witherror.code: "authentication_required".
A token is required — free account + token at airanks.net/tokens.
# required: paste a key
export AIR_API_KEY=air_xxxxxxxxxxxxxxxx
# or log in once with any AIR client and every other one picks it up
npx air-cli loginAIR_API_BASE overrides the API base (default https://airanks.net/api/v1).
🚀 Install & quickstart
npm install -g airanks-mcp-server...or skip the install entirely — every config block below runs it straight from npx.
A token is required for every config below — free account + token at
airanks.net/tokens, then set it as AIR_API_KEY or run
npx air-cli login once (see Shared authentication).
Edit claude_desktop_config.json (Settings → Developer → Edit Config):
{
"mcpServers": {
"airanks": {
"command": "npx",
"args": ["-y", "airanks-mcp-server"],
"env": {
"AIR_API_KEY": "your-air-api-key"
}
}
}
}claude mcp add airanks -- npx -y airanks-mcp-serverOr drop this straight into .mcp.json in your project:
{
"mcpServers": {
"airanks": {
"command": "npx",
"args": ["-y", "airanks-mcp-server"]
}
}
}Add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"airanks": {
"command": "npx",
"args": ["-y", "airanks-mcp-server"]
}
}
}{
"mcpServers": {
"airanks": {
"command": "npx",
"args": ["-y", "airanks-mcp-server"],
"env": { "AIR_API_KEY": "your-air-api-key" }
}
}
}Or point command straight at a global install: airanks-mcp (after
npm install -g airanks-mcp-server).
⚙️ Configuration
| Env var | Default | What it does |
|---|---|---|
| AIR_API_KEY | (unset) | Bearer token; always wins over the shared auth file. |
| AIR_API_BASE | https://airanks.net/api/v1 | Point the server at a different API base. |
| AIR_MCP_POLL_MS | 3000 | Interval between "still pending" re-polls. |
| AIR_MCP_POLL_MAX_MS | 12000 | Total wall-clock budget before returning "still gathering" instead of blocking. |
💬 Example prompts
- "What's the AIR score for stripe.com?"
- "Does our site have an llms.txt? Is GPTBot blocked?"
- "Search AIR for domains ranking on 'best project management software'."
🛠️ Development
npm install
npm run build # tsc -> dist/
npm run dev # tsx src/index.ts, no build step| File | Responsibility |
|---|---|
| src/auth.ts | Shared AIR token resolution (AIR_API_KEY > auth file > anonymous) + host-scoping. |
| src/api.ts | Hostname normalization, the AIR API client, and the pending-poll loop. |
| src/tools.ts | Wires air_rank / air_files / air_search onto the McpServer. |
| src/index.ts | Boots McpServer on StdioServerTransport. |
🌐 The AIR family
Every client below shares the same auth file, the same hostname rules, and the same API — see
API-CONTRACT.md for the wire contract they all implement.
| Repo | What it is |
|---|---|
| node-cli | air — the reference Node.js terminal client. |
| go-cli | air — Go build, same behavior. |
| rust-cli | air-cli — Rust build, same behavior. |
| composer-package | airanks-net/api-client — PHP API client. |
| js-sdk | @airanks-net/sdk — JS/TS SDK. |
| python-sdk | airanks — Python client. |
| chrome-extension | AIR Toolbar — see a site's AIR score while you browse. |
| agent-toolkit | Universal toolkit for teaching any agent/harness about AIR. |
| acp-agent | AIR exposed over ACP (Agent Connect Protocol). |
| acp-zed | AIR inside the Zed editor via ACP. |
| homebrew-tap | brew install for the air CLI. |
📄 License
MIT — see LICENSE.
Built by airanks — AI optimization, measured. 📡
