@cyanheads/openaq-mcp-server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@cyanheads/openaq-mcp-serverShow me the latest air quality readings in Berlin"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Public Hosted Server: https://openaq.caseyjhand.com/mcp
openaq-mcp-server wraps the OpenAQ v3 API to expose measured air quality — physical-sensor observations from government reference monitors and research-grade sensors worldwide. It is the ground-truth counterpart to a modeled air-quality grid: where a model gives a concentration anywhere, OpenAQ gives an actual reading from a physical monitor — sparser, unevenly distributed, but real.
Coverage is uneven and honest. An empty result means there is no monitoring there, not that the air is clean — every discovery tool says so, and points to a modeled fallback (open-meteo-mcp-server's air-quality tool) for anywhere-coverage.
Tools
Five domain tools cover the workflow — discover stations, read current values, pull history, and resolve the two catalogs (pollutant units, country coverage) — plus two DataCanvas tools for SQL over historical series too large to inline. The data model is location → sensor → parameter; the server hides the sensor layer so you think in stations and parameters, never sensor ids.
Tool | Description |
| 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. |
| Latest measured value for every sensor at a station, each joined with its pollutant and unit. The current-conditions tool. |
| Historical series for one pollutant at one station over a date range, with |
| Catalog of measurable pollutants and their canonical units. The unit-disambiguation reference. |
| Catalog of country-level coverage — data span and parameters measured, filterable by |
| List the tables and columns staged on a DataCanvas so you can write valid SQL. |
| Run a read-only |
openaq_find_locations
Find air-quality monitoring stations (measured by physical sensors, not modeled) and the parameters each one reports.
Three search scopes —
coordinates+radius(near-me),bbox(area sweep), orisocountry code; at least one is requiredradiusis in metres, 1–25000 (the API hard-caps at 25000); larger areas needbbox, which returns no distanceparametersIdnarrows to stations that measure a given parameter (each returned station still lists all its sensors)limitcaps at 100 stations per page;page(1-based) reaches the rest — distance ordering applies within a page, not across pages, so paging is foriso/bboxsweeps rather than near-me searchescoordinatesandbboxaccept whitespace around the commas —"47.6062, -122.3321"is normalized to its space-free formReturns each station's id, name, coordinates, distance (when searching by coordinates), country, provider,
isMonitor/isMobile, the parameters its sensors measure with units, and thedatetimeFirst/datetimeLastdata spanEmpty result means no coverage, not clean air — widen the radius, check
openaq_list_countries, or fall back to the modeledopen-meteoair-quality tool
openaq_get_readings
Latest value per sensor at a station — the current-conditions tool.
Pass a
locationIdfromopenaq_find_locations, orcoordinates+parametersIdto auto-resolve the nearest station (within 25km) that measures that parameterThe raw OpenAQ latest feed is keyed only by sensor id; this tool joins it against the station's sensor → parameter → unit map, so every value carries its pollutant and unit
With
locationId,parametersIdoptionally filters the returned values to one parameter; omit it for all sensorsEach value carries its UTC and local timestamp plus the station's
datetimeLast— recency varies by station, so "latest" may be minutes or hours old
openaq_get_measurements
Historical measurement series for one pollutant at one station over a date range — for trend analysis and "was last week worse than the monthly average?".
Pass a
locationIdand aparametersIdand work in stations — the server maps station + parameter to the underlying sensor (v3 series are sensor-scoped), so you get the series for that pollutant at that stationaggregation:raw(every reported value),hourly, ordaily—hourly/dailyadd a per-bucket statistical summary (min, median, max, mean, sd)datetimeFrom/datetimeToaccept a date (YYYY-MM-DD) or full UTC timestamp (YYYY-MM-DDTHH:MM:SSZ); omit for the most recent valuesValues carry their unit; the server never converts between µg/m³, ppm, and ppb (the conversion is gas- and temperature-dependent)
Large ranges spill to a DataCanvas — see below
DataCanvas spill workflow
A multi-month raw series can be thousands of rows — too large to inline without blowing context, and a fixed slice would blind the agent to the rest. When a series exceeds the inline preview (100 rows), openaq_get_measurements stages the pulled rows on a DuckDB-backed DataCanvas and returns:
a preview (
series, capped at 100 rows) plusrowCountand thetotalCountenrichment,truncated: true,canvasId, and atableNameof the formmeasurements_<sensorId>.
The internal pager stops at 5000 rows, so the canvas holds the whole series only when totalCount is at or below that. Above it — or when a page fails partway and the rows already pulled are kept — the response carries a notice saying so and how to reach the rest (shorter date windows, or hourly/daily aggregation).
You then query the full set with the two consumer tools:
Tool | Use |
| List staged tables and their columns ( |
| Run a read-only |
Pass a prior canvas_id back into openaq_get_measurements to stage a second station's series on the same canvas (as measurements_<otherSensorId>), then JOIN/UNION the two in one query to compare stations.
Requires CANVAS_PROVIDER_TYPE=duckdb. Without it, openaq_get_measurements still returns the truncated preview plus a notice (it does not fail), and the two dataframe tools return a canvas_unavailable error directing you to enable DuckDB. The same holds when the canvas is set to duckdb but cannot start — a missing DuckDB native in a .mcpb bundle, say: openaq_get_measurements returns the preview and names the staging failure in its notice rather than dropping a series it already fetched.
Not available in the .mcpb bundle. The Claude Desktop bundle ships without DuckDB's platform-specific native binding — including it would lock the bundle to the OS it was packed on and push it far past the size registries accept. Leave CANVAS_PROVIDER_TYPE at none there; use the npm, npx, or Docker install for canvas work.
openaq_dataframe_query is read-only by design — writes, DDL, and file/network table functions are rejected; only a single SELECT runs.
Related MCP server: Wellness Air
Resources and prompts
Type | Name | Description |
Resource |
| Location metadata for a known location id — name, coordinates, country, provider, sensors (each with parameter + unit), and data span. |
Resource |
| Full pollutant + unit catalog (same data as |
All resource data is also reachable via tools — both resources mirror tool output, so tool-only MCP clients lose nothing. There are no prompts: this is a data-lookup domain with no recurring analysis template that earns one (a WHO-guideline health snapshot is a cross-server workflow, not localized here).
Features
Built on @cyanheads/mcp-ts-core:
Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
Unified error handling — handlers throw, framework catches, classifies, and formats
Typed error contracts per tool — each network tool declares
reason/code/when/recovery, so failures carry a concrete next movePluggable auth (
none,jwt,oauth) and structured, request-scoped logging with optional OpenTelemetry tracingSTDIO and Streamable HTTP transports from one codebase
OpenAQ-specific:
Single typed client over the OpenAQ v3 REST API with
X-API-Keyauth, 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 → transientServiceUnavailable)Hides the v3
location → sensor → measurementhierarchy —openaq_get_measurementsresolves a station + parameter to the underlying sensor;openaq_get_readingsjoins the latest feed against the sensor map so every value is labeledDataCanvas spillover for large measurement series, queryable with read-only DuckDB SQL
Coordinates and radius are bounded in Zod at the edge — OpenAQ returns an opaque plain-text 500 for out-of-range input, so the server rejects it cleanly before the call
Agent-friendly output:
Measured-vs-modeled framing in every discovery tool — an empty result is stated as no coverage, not clean air, with a pointer to the modeled fallback, so an agent never misreads sparse data as a clean reading
Units travel with every value, never converted — the same pollutant has multiple parameter ids for different units (
cois id 4 µg/m³, id 8 ppm, id 102 ppb), soparametersIdis the precise selector andopenaq_list_parametersmaps pollutant + unit → idChainable ids and staleness signals — location id → readings/measurements, sensor id → history;
datetimeLastand per-value timestamps expose how fresh "latest" actually isCapped lists disclose truncation (
totalCount,truncated) via framework enrichment, reaching both the structured and text output surfaces
Getting started
Public Hosted Instance
A 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"
}
}
}Self-hosted
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/mcpPrerequisites
Bun v1.3.0 or higher (Node.js v24+ also works at runtime).
A free OpenAQ v3 API key — sign up at explore.openaq.org.
Installation
Clone the repository:
git clone https://github.com/cyanheads/openaq-mcp-server.gitNavigate into the directory:
cd openaq-mcp-serverInstall dependencies:
bun installConfigure environment:
cp .env.example .env
# edit .env and set OPENAQ_API_KEYConfiguration
All configuration is validated at startup via Zod schemas. Key environment variables:
Variable | Description | Default |
| Required. OpenAQ v3 API key, sent as the | — |
| OpenAQ v3 API base URL. Override for a proxy or test mirror. |
|
| Set to |
|
| Transport: |
|
| Port for the HTTP server. |
|
| Auth mode: |
|
| Log level (RFC 5424). |
|
| Directory for log files (Node.js only). |
|
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run:
bun run rebuild bun run start:http # or start:stdioRun 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
docker build -t openaq-mcp-server .
docker run --rm -e OPENAQ_API_KEY=your-api-key -p 3010:3010 openaq-mcp-serverThe 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.
Project structure
Directory | Purpose |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| Resource definitions ( |
| OpenAQ v3 API client, request/auth/retry, and domain types. |
| Unit and integration tests mirroring |
Development guide
See CLAUDE.md / AGENTS.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storageRegister new tools and resources in the
createApp()arraysWrap the OpenAQ API: validate raw → normalize to the domain type → return the output schema; surface units verbatim and never fabricate missing fields
Data & licensing
Air 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.
Contributing
Issues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceEnables interaction with the World Air Quality Index to fetch real-time air quality data for cities and coordinates worldwide via Model Context Protocol (MCP).Last updated1MIT
- AlicenseAqualityAmaintenanceLocal-first air-quality MCP for AI agents: AirGradient, AirThings, PurpleAir, IQAir and Awair.Last updated1999MIT
- Alicense-qualityCmaintenanceEnables querying of official Netherlands air quality data from the RIVM Luchtmeetnet API via MCP tools.Last updatedMIT
- Alicense-qualityAmaintenanceSearch and query government open-data portals (Socrata SODA API) via MCP.Last updated1002Apache 2.0
Related MCP Connectors
Air Quality MCP — wraps air-quality-api.open-meteo.com (free, no auth)
EPA AirNow MCP — official US real-time AQI + forecast (free key)
openSenseMap MCP — citizen-science environmental sensor network (opensensemap.org)
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/openaq-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server