IQAir MCP Server
by Nanparam
README.md
# IQAir MCP Server
An MCP server that exposes **real-time air-quality data** from
[IQAir](https://www.iqair.com) — built by reverse-engineering the IQAir
website (no official API key required).
It was created for the Jakarta air-quality page
(`https://www.iqair.com/id/air-quality/indonesia/jakarta/jakarta`) but works
for **any** IQAir location.
## What it returns
For a location you get:
- **US AQI** and the **main pollutant**
- Per-pollutant readings: **PM2.5, PM10, O₃, SO₂** (AQI + concentration)
- Weather: **temperature, humidity, pressure, wind**
- **Reporting station count**
- **7-day** and **72-hour** forecasts
- Coordinates / timezone
## Tools
| Tool | Description |
|------|-------------|
| `get_jakarta_air_quality(locale="id")` | Full Jakarta report (current + forecasts). |
| `get_jakarta_current(locale="id")` | Jakarta current conditions only (lighter). |
| `get_jakarta_forecast(locale="id", kind="daily")` | Jakarta forecast (`daily` or `hourly`). |
| `get_air_quality_by_path(path, locale="en")` | Any location, e.g. `path="indonesia/west-java/bandung"`. |
`path` is the segment after `/<locale>/air-quality/` in an IQAir URL —
`country/state/city`.
## How it works (reverse engineering)
IQAir is a **React Router v7 (Remix)** app on **Vercel**. Every air-quality
page exposes its loader payload at the same URL with a `.data` suffix:
```
GET /id/air-quality/indonesia/jakarta/jakarta.data?_routes=routes%2F%24(locale).air-quality.%24
```
The response is **turbo-stream** encoded (an index-referenced pool with
deferred promise chunks), decoded by [`turbo_stream.py`](turbo_stream.py).
### Anti-bot: the Vercel Security Checkpoint
The site is guarded by Vercel's checkpoint. During reverse engineering we found:
- Plain `requests` / `httpx` → **HTTP 429** (challenge page).
- `curl_cffi` with Chrome TLS impersonation → **still 429** (JS/WASM challenge
is required, not just a matching TLS fingerprint).
- Even a valid `_vcrcs` cookie replayed from a non-browser client → **429**
(TLS/JA3 fingerprint is validated).
- **Headless** Chromium → checkpoint never clears (headless is detected).
- **Headful** Chromium → clears in ~3 s.
So the client (`iqair_client.py`) drives a **headful Chromium with a persistent
profile**: it solves the checkpoint once, caches the `_vcrcs` cookie in the
profile, and issues the `.data` fetch *from inside the page context* so the
browser's real TLS stack + solved cookie are used. The window is parked
off-screen so it doesn't disturb you.
## Setup
```bash
cd iqair-mcp
pip install -r requirements.txt
python -m playwright install chromium
```
> A desktop session / display is required (the browser must run **headful**).
## Run
```bash
python server.py # stdio (for MCP clients)
python server.py --transport sse # SSE on http://localhost:8000
```
### Register with an MCP client
```json
{
"mcpServers": {
"iqair": {
"command": "python",
"args": ["C:\\path-to-mcp\\iqair-mcp\\server.py"]
}
}
}
```
## Files
- `server.py` — FastMCP server exposing the tools.
- `iqair_client.py` — browser-backed client (challenge solving + fetch + normalize).
- `turbo_stream.py` — decoder for React Router's turbo-stream `.data` format.
- `requirements.txt` — dependencies.
## Staying alive across RDP disconnects
The checkpoint forces a **headful** browser, and an off-screen headful Chromium
can lose its rendering surface (and die) when an RDP session disconnects or
locks. The server survives this on two levels:
1. **Auto-recovery** — if a call hits a "target/browser closed" error, the
client tears down the dead browser and relaunches once, transparently.
2. **Watchdog heartbeat** — a background thread pings the browser every
`keepalive_seconds` (default **60 s**); if it's dead/unresponsive it rebuilds
and re-solves the challenge *proactively*, so a disconnect never leaves a
dead browser waiting for the next tool call. Warm calls after a revive
return in ~1–2 s with no re-solve.
Additionally, the screensaver/auto-lock was disabled for the current user
(`HKCU\Control Panel\Desktop` → `ScreenSaveActive=0`, `ScreenSaveTimeOut=0`) so
a connected session doesn't lock itself out from under the browser. This is
per-user and reversible.
> **Fully headless-of-console setups** (keeping a live desktop with *nobody*
> connected) require admin rights — either a machine policy
> `MaxDisconnectionTime=0` under
> `HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services`, or a
> `tscon`-to-console scheduled task. Those aren't applied here (no elevation on
> this domain account); the watchdog + auto-recovery cover the disconnect case
> without them.
## Notes & limitations
- First call per session pays a one-time ~3 s browser launch + challenge solve;
subsequent calls reuse the warm session.
- A connected desktop session is still recommended; the watchdog handles brief
disconnects/locks by rebuilding, but the machine must have a session to render
a headful browser at all.
- Data is whatever IQAir currently publishes (values update in real time).
- This relies on IQAir's internal (undocumented) data endpoint; site changes
may require updating the route id or decoder.
- For personal/educational use — respect IQAir's terms of service.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues