Find air-quality stations and read pollutant observations from government monitors via OpenAQ v3.
Find air-quality monitoring stations, read latest sensor values, and pull historical pollutant series via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://openaq.caseyjhand.com/mcp
Measured air quality from the OpenAQ v3 API — physical-sensor observations from government reference monitors and research-grade sensors worldwide. Find monitoring stations, read current values, and pull historical pollutant series from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|---|
openaq_find_locations | Find monitoring stations near a point, in a bounding box, or by country. The required first step — readings and measurements key on the location id this returns. |
openaq_get_readings | Latest measured value for every sensor at a station, joined with its pollutant and unit. The current-conditions tool. |
openaq_get_measurements | Historical series for one pollutant at one station over a date range, with raw/hourly/daily aggregation. Large ranges spill to a DataCanvas. |
openaq_list_parameters | Catalog of measurable pollutants and their canonical units. The unit-disambiguation reference. |
openaq_list_countries | Catalog of country-level coverage — data span and parameters measured, filterable by parametersId. An availability check before a regional sweep. |
openaq_dataframe_describe | List the tables and columns staged on a DataCanvas so you can write valid SQL. |
openaq_dataframe_query | Run a read-only SELECT over staged measurement series. |
| Resource | Description |
|---|---|
openaq://location/{locationId} | Location metadata for a known location id — name, coordinates, country, provider, sensors (each with parameter + unit), and data span. |
openaq://parameters | Full pollutant + unit catalog (same data as openaq_list_parameters). |
All resource data is also reachable via tools — both resources mirror tool output, so tool-only MCP clients lose nothing.
openaq_find_locations toolcoordinates + radius (near-me), bbox (area sweep), or iso country code; at least one is requiredradius is in metres, 1–25000 (the API hard-caps at 25000); larger areas need bbox, which returns no distanceparametersId narrows to stations that measure a given parameter; each returned station still lists all its sensorslimit caps at 100 stations per page; page (1-based) reaches further pages — distance ordering applies within a page, not across pagesisMonitor/isMobile, its parameters with units, and the datetimeFirst/datetimeLast data spanopenaq_list_countries, or fall back to the modeled open-meteo-mcp-server air-quality toolopenaq_get_readings toollocationId from openaq_find_locations, or coordinates + parametersId to auto-resolve the nearest station (within 25km) that measures that parameterlocationId, parametersId optionally filters the returned values to one parameter; omit it for all sensorsdatetimeLast — recency varies by stationopenaq_get_measurements toollocationId and parametersId; the server resolves the underlying sensor internally (v3 series are sensor-scoped)aggregation: raw (every reported value), hourly, or daily — rollups add a per-bucket min/median/max/mean/sddatetimeFrom/datetimeTo accept a date (YYYY-MM-DD) or full UTC timestamp; omit either for the most recent values or "up to now"pulledCount and pullComplete say what was actually collected, and totalCount is published as a floor (flagged by totalCountIsLowerBound) when OpenAQ answers the range with a ">N" bound instead of an exact countseries is a preview and the pulled rows stage on a DataCanvas (canvasId + tableName) when CANVAS_PROVIDER_TYPE=duckdb — without it, the response still returns the preview plus a notice. Every row the response carries is rendered in the text output too, so a text-only client sees the same setcanvas_id to put this series on that canvas whatever its size, for cross-station JOIN/UNION queries. Reuse stages one table per sensor: a different sensor adds a table, while re-staging the same sensor overwrites its earlier series and the response says soopenaq_list_parameters toolquery filters the ~44-parameter catalog by code, display name, or description (case-insensitive); pollutantsOnly excludes meteorological/particle-count channels (temperature, humidity, wind, pressure)openaq_list_countries toolquery matches a two-letter input as an exact ISO 3166-1 alpha-2 code, longer input as a substring of code or name; parametersId filters to countries measuring that parameter anywheredatetimeFirst/datetimeLast data span, and the parameters measured anywhere within itopenaq_find_locations sweep — answers "which countries have NO2 monitoring?"openaq_dataframe_describe toolcanvas_id returned by a prior openaq_get_measurements callmeasurements_<sensorId> table with its row count and column namescanvas_unavailable when CANVAS_PROVIDER_TYPE is not duckdbopenaq_dataframe_query toolcanvas_id and a read-only SQL SELECT against the staged measurement tablesSELECT runstruncated reports that the cap bit, and the notice names ORDER BY <column> LIMIT 200 OFFSET <n> as the way to page the rest. rowCount is the rows returned, not the size of the full resultcanvas_unavailable when DuckDB is off, or missing_table when the SQL references a table not staged on that canvasopenaq://location/{locationId} resourceisMonitor/isMobile, coordinates, sensors (each with parameter id/name/unit), and the datetimeFirst/datetimeLast spanlocationId comes from openaq_find_locationsdatetimeLast advances as measurements landopenaq://parameters resourceopenaq_list_parameters with no query or filter — the full catalogA multi-month raw series can be thousands of rows — too large to inline without blowing context. When openaq_get_measurements stages a series, read the staged table with the two consumer tools, in this order:
| Tool | Use |
|---|---|
openaq_dataframe_describe | List staged tables and their columns (value, datetimeFrom, datetimeTo, min, median, max, avg, sd, percentComplete, flagged) — call first. The staged table is flat while the inline series is nested (summary.min), so SQL written from the response shape alone names columns that do not exist. |
openaq_dataframe_query | Run a read-only SELECT for monthly means, exceedance counts, percentiles, or cross-sensor comparisons. Capped at 200 rows per response — aggregate in SQL, or page with ORDER BY plus LIMIT/OFFSET. |
measurements_<sensorId>): reuse a canvas_id across two sensors to JOIN/UNION their series, and re-staging the same sensor overwrites its earlier table.CANVAS_PROVIDER_TYPE=duckdb. Without it — or when a configured canvas fails to start — openaq_get_measurements still returns the preview plus a notice rather than dropping data already fetched..mcpb bundle — the Claude Desktop bundle ships without DuckDB's platform-specific native binding, since bundling it would lock the bundle to the OS it was packed on. Use the npm, npx, or Docker install for canvas work.Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
OpenAQ-specific:
X-API-Key auth, retry with rate-limit-calibrated backoff, and OpenAQ-specific error classification (clean-JSON 404 → NotFound; the Python-repr 422 body → ValidationError; the plain-text 500 on bad coordinates → transient ServiceUnavailable)location → sensor → measurement hierarchy — openaq_get_measurements resolves a station + parameter to the underlying sensor; openaq_get_readings joins the latest feed against the sensor map so every value is labeledAgent-friendly output:
parametersId is the precise selector and openaq_list_parameters maps pollutant + unit → iddatetimeLast and per-value timestamps expose how fresh "latest" actually istotalCount, truncated) via framework enrichment, reaching both the structured and text output surfacesA public instance is available at https://openaq.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP, with this client config:
{
"mcpServers": {
"openaq-mcp-server": {
"type": "streamable-http",
"url": "https://openaq.caseyjhand.com/mcp"
}
}
}
An OpenAQ v3 API key is required — sent as the X-API-Key header on every request. Get a free key from your OpenAQ Explorer account.
Add the following to your MCP client configuration file.
{
"mcpServers": {
"openaq-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/openaq-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"OPENAQ_API_KEY": "your-api-key"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"openaq-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/openaq-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"OPENAQ_API_KEY": "your-api-key"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"openaq-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "OPENAQ_API_KEY=your-api-key",
"ghcr.io/cyanheads/openaq-mcp-server:latest"
]
}
}
}
To enable DataCanvas SQL over large measurement series, add "CANVAS_PROVIDER_TYPE": "duckdb" to env.
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 OPENAQ_API_KEY=your-api-key bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/openaq-mcp-server.git
cd openaq-mcp-server
bun install
cp .env.example .env
# edit .env and set OPENAQ_API_KEY
All configuration is validated at startup via Zod schemas. Key environment variables:
| Variable | Description | Default |
|---|---|---|
OPENAQ_API_KEY | Required. OpenAQ v3 API key, sent as the X-API-Key header. A missing key surfaces as a clean startup error. | — |
OPENAQ_API_BASE_URL | OpenAQ v3 API base URL. Override for a proxy or test mirror. | https://api.openaq.org/v3 |
CANVAS_PROVIDER_TYPE | Set to duckdb to enable DataCanvas SQL over large measurement series. Without it, large series return a truncated preview and the dataframe tools are inert. | none |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
See .env.example for the full list of optional overrides.
Build and run:
bun run rebuild
bun run start:http # or start:stdio
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security, changelog sync
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
docker build -t openaq-mcp-server .
docker run --rm -e OPENAQ_API_KEY=your-api-key -p 3010:3010 openaq-mcp-server
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/openaq-mcp-server. The @duckdb/node-api runtime dependency ships in the image, so DataCanvas works once CANVAS_PROVIDER_TYPE=duckdb is set. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources and inits the service + canvas. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts) — five OpenAQ tools plus two dataframe_* tools. |
src/mcp-server/resources/definitions | Resource definitions (*.resource.ts) — location and parameters mirrors. |
src/services/openaq | OpenAQ v3 API client, request/auth/retry, and domain types. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md / AGENTS.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagecreateApp() arraysAir quality data served by this MCP server is sourced from the OpenAQ platform. Attribution to OpenAQ as the data source is required when using this server's output (OpenAQ Terms of Use).
OpenAQ aggregates measurements from hundreds of government agencies, research institutions, and other monitoring networks worldwide. Each of those upstream providers may publish its own attribution or licensing terms. The provider field returned by openaq_find_locations, openaq_get_readings, and the openaq://location/{locationId} resource identifies the originating network for each station. Downstream users are responsible for reviewing and complying with the terms of any provider whose data they use.
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/openaq-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-openaq-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/openaq-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/openaq-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.