io.github.cyanheads/openaq-mcp-server
Find air-quality stations and read pollutant observations from government monitors via OpenAQ v3.
What can you do with io.github.cyanheads/openaq-mcp-server?
- One typed client over the OpenAQ v3 REST API:
X-API-Keyauth, retry with backoff, and typed upstream failures (invalid_api_key,rate_limited,upstream_timeout,upstream_error) on every OpenAQ call - Hides the v3 location → sensor → measurement hierarchy: readings join the latest feed to the station's sensor map, and measurements resolve a station plus parameter to its sensor
- Coordinates and bbox corners are range-checked and contradictory search scopes rejected before any request reaches OpenAQ
- Large measurement series stage on a DataCanvas for read-only DuckDB SQL, one table per sensor, so two series on one
canvas_idcan be joined - Measured, not modeled: a search with no station fails as
no_locations_foundorno_station_near_coordinates, stated as no coverage rather than clean air - Units travel with every value and are never converted;
parametersIdselects pollutant and unit together - Freshness and truncation are explicit: per-value timestamps and
datetimeLast, andtotalCount/truncated/noticeon capped results
Install
Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).
claude mcp add --transport http openaq-mcp-server 'https://openaq.caseyjhand.com/mcp'
{
"mcpServers": {
"openaq-mcp-server": {
"type": "http",
"url": "https://openaq.caseyjhand.com/mcp"
}
}
}
@cyanheads/openaq-mcp-serverclaude mcp add openaq-mcp-server -e 'OPENAQ_API_KEY=<your openaq_api_key>' -e 'OPENAQ_API_BASE_URL=https://api.openaq.org/v3' -e 'CANVAS_PROVIDER_TYPE=none' -e 'OPENAQ_ENABLE_CANVAS_DROP=false' -e 'MCP_LOG_LEVEL=info' -- node -y @cyanheads/openaq-mcp-server@0.4.1 run start:stdio
{
"mcpServers": {
"openaq-mcp-server": {
"command": "node",
"args": [
"-y",
"@cyanheads/openaq-mcp-server@0.4.1",
"run",
"start:stdio"
],
"env": {
"OPENAQ_API_KEY": "<your openaq_api_key>",
"OPENAQ_API_BASE_URL": "https://api.openaq.org/v3",
"CANVAS_PROVIDER_TYPE": "none",
"OPENAQ_ENABLE_CANVAS_DROP": "false",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Environment variables
OPENAQ_API_KEYrequired secret OpenAQ v3 API key, sent as the X-API-Key header. Free from openaq.org.OPENAQ_API_BASE_URLOpenAQ v3 API base URL override (testing / proxy).CANVAS_PROVIDER_TYPESet to 'duckdb' to enable DataCanvas SQL over large measurement series.OPENAQ_ENABLE_CANVAS_DROPEnable deletion of complete staged canvases; requires CANVAS_PROVIDER_TYPE=duckdb.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
@cyanheads/openaq-mcp-serverclaude mcp add openaq-mcp-server -e 'OPENAQ_API_KEY=<your openaq_api_key>' -e 'OPENAQ_API_BASE_URL=https://api.openaq.org/v3' -e 'CANVAS_PROVIDER_TYPE=none' -e 'OPENAQ_ENABLE_CANVAS_DROP=false' -e 'MCP_HTTP_HOST=127.0.0.1' -e 'MCP_HTTP_PORT=3010' -e 'MCP_HTTP_ENDPOINT_PATH=/mcp' -e 'MCP_AUTH_MODE=none' -e 'MCP_LOG_LEVEL=info' -- node -y @cyanheads/openaq-mcp-server@0.4.1 run start:http
{
"mcpServers": {
"openaq-mcp-server": {
"command": "node",
"args": [
"-y",
"@cyanheads/openaq-mcp-server@0.4.1",
"run",
"start:http"
],
"env": {
"OPENAQ_API_KEY": "<your openaq_api_key>",
"OPENAQ_API_BASE_URL": "https://api.openaq.org/v3",
"CANVAS_PROVIDER_TYPE": "none",
"OPENAQ_ENABLE_CANVAS_DROP": "false",
"MCP_HTTP_HOST": "127.0.0.1",
"MCP_HTTP_PORT": "3010",
"MCP_HTTP_ENDPOINT_PATH": "/mcp",
"MCP_AUTH_MODE": "none",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Environment variables
OPENAQ_API_KEYrequired secret OpenAQ v3 API key, sent as the X-API-Key header. Free from openaq.org.OPENAQ_API_BASE_URLOpenAQ v3 API base URL override (testing / proxy).CANVAS_PROVIDER_TYPESet to 'duckdb' to enable DataCanvas SQL over large measurement series.OPENAQ_ENABLE_CANVAS_DROPEnable deletion of complete staged canvases; requires CANVAS_PROVIDER_TYPE=duckdb.MCP_HTTP_HOSTThe hostname for the HTTP server.MCP_HTTP_PORTThe port to run the HTTP server on.MCP_HTTP_ENDPOINT_PATHThe endpoint path for the MCP server.MCP_AUTH_MODEAuthentication mode to use: 'none', 'jwt', or 'oauth'.MCP_LOG_LEVELSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
@cyanheads/openaq-mcp-server
Find air-quality monitoring stations, read latest sensor values, and pull historical pollutant series via MCP. STDIO or Streamable HTTP.
8 Tools (7 enabled by default) • 2 Resources
Public Hosted Server: https://openaq.caseyjhand.com/mcp
Overview
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.
Tools
| Tool | Description |
|---|---|
openaq_find_locations | Find monitoring stations near a point, in a bounding box, or by country; the location ids the other tools take |
openaq_get_readings | Latest value for every sensor at a station, labeled with its pollutant and unit |
openaq_get_measurements | Historical series for one pollutant at one station, raw or rolled up hourly/daily |
openaq_list_parameters | Pollutant catalog with canonical units; maps a pollutant and unit to its parameter id |
openaq_list_countries | Country coverage: data span and the parameters measured in each |
openaq_dataframe_describe | List the tables and columns staged on a DataCanvas |
openaq_dataframe_query | Run a read-only SQL SELECT over staged measurement series |
openaq_dataframe_drop | Delete a complete staged canvas; opt-in with OPENAQ_ENABLE_CANVAS_DROP=true |
Resources
| Resource | Description |
|---|---|
openaq://location/{locationId} | Metadata for one station: coordinates, provider, sensors with units, data span |
openaq://parameters | Full pollutant and unit catalog |
Both resources mirror tool output, so tool-only clients lose nothing.
Capability reference
openaq_find_locations tool
- Scope by
coordinateswithradius(metres, 1–25000, default 12000),bbox, and/oriso(a code fromopenaq_list_countries); at least one is required, andcoordinatesandbboxexclude each other.parametersId,monitor,mobile, andprovidersIdnarrow further; up to 100 stations per page (limit, default 20), 1-basedpage - Each station carries
id,distanceMeters(coordinate search only),provider/providerId,isMonitor/isMobile, itsparameterswith units, and thedatetimeFirst/datetimeLastspan;coordinates,country, andproviderare null when OpenAQ lists none totalCountcounts stations through the current page and is a floor whentotalCountIsLowerBoundis set; no match fails asno_locations_found, a page past the end aspage_exhausted
openaq_get_readings tool
- A
locationId, orcoordinatesplusparametersIdto resolve the nearest station within 25 km that measures it (compared across up to 1,000 matching stations; a full 1,000 adds anotice); withlocationId,parametersIdoptionally filters to one parameter - One reading per sensor with
value,unit,sensorId, anddatetimeUtc/datetimeLocal, plus the station'sprovider,providerId,timezone, anddatetimeLast - Misses are typed:
location_not_found,parameter_not_at_location(with the station'savailableids),no_station_near_coordinates,no_recent_values
openaq_get_measurements tool
- Requires
locationIdandparametersId;datetimeFrom/datetimeToaccept UTC timestamps or station-localYYYY-MM-DDdates (UTC days when the station has no timezone).aggregationisraw(default),hourly, ordaily; up to 5,000 rows per call - Returns
series,sensorId,pulledCount,pullComplete,effectiveRange, and stationprovider,providerId, andtimezone.totalCountis a floor whentotalCountIsLowerBoundis set; rollups add summary statistics,gapCount, the first 20 missing intervals asgaps, and a notice when edge buckets are clipped - Past 100 rows,
seriesis a preview (truncated) and the pulled rows stage on a DataCanvas asmeasurements_<sensorId>(canvasId,tableName) whenCANVAS_PROVIDER_TYPE=duckdb; a suppliedcanvas_idstages onto that canvas at any size. One table per sensor: a second sensor adds a table to join against, and re-staging the same sensor overwrites its earlier series
openaq_list_parameters tool
- Optional case-insensitive
queryover code, display name, and description;pollutantsOnlydrops meteorological and particle-count channels - Rows carry
id,name,displayName,unit, anddescription; one pollutant can appear under several ids by unit (CO is 4 in µg/m³, 8 in ppm, 102 in ppb)
openaq_list_countries tool
- Optional
query(a two-letter value matches an ISO 3166-1 alpha-2 code first; otherwise code or name substrings match) andparametersId; up to 100 per page (limit, default 20), 1-basedpage - Rows carry
code(theisovalue foropenaq_find_locations),name, thedatetimeFirst/datetimeLastspan, and theparametersmeasured anywhere in the country;totalCountis the full filtered count
openaq_dataframe_describe tool
- Takes a
canvas_idfromopenaq_get_measurementsand returns each staged table'sname,rowCount, andcolumns. Call it beforeopenaq_dataframe_query: the staged table is flat (min,sd) while the inlineseriesnests those undersummary - Fails as
canvas_unavailableunlessCANVAS_PROVIDER_TYPE=duckdb, orcanvas_not_foundfor an unknown or expired id
openaq_dataframe_query tool
- A
canvas_idand one read-onlySELECT; writes, DDL, and file/network table functions are rejected - At most 200 rows per response, with
truncatedset when the cap cut the result; page withORDER BYplusLIMIT/OFFSET - Fails as
canvas_unavailable,canvas_not_found, ormissing_table
openaq_dataframe_drop tool
- Registered only with
OPENAQ_ENABLE_CANVAS_DROP=true; calls fail ascanvas_unavailableunlessCANVAS_PROVIDER_TYPE=duckdb. When disabled, the HTTP landing page shows the enable hint andtools/listomits the tool - Takes a
canvas_idand deletes that whole canvas, including all staged tables. Other agents sharing the id lose access; OpenAQ's source data is unaffected - Returns the requested
canvasIdanddropped: true when deleted, false when no reachable canvas exists. With authentication off, callers share a tenant: anyone holding an id can delete its canvas when enabled
openaq://location/{locationId} resource
application/json: name, locality, timezone, country, provider,isMonitor/isMobile, coordinates,sensors(each withparameterIdandunit), and thedatetimeFirst/datetimeLastspanlocationIdcomes fromopenaq_find_locations; cached 5 minutes
openaq://parameters resource
application/jsoncatalog, the same rows as an unfilteredopenaq_list_parameters- Cached 1 hour
Features
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:
- One typed client over the OpenAQ v3 REST API:
X-API-Keyauth, retry with backoff, and typed upstream failures (invalid_api_key,rate_limited,upstream_timeout,upstream_error) on every OpenAQ call - Hides the v3 location → sensor → measurement hierarchy: readings join the latest feed to the station's sensor map, and measurements resolve a station plus parameter to its sensor
- Coordinates and bbox corners are range-checked and contradictory search scopes rejected before any request reaches OpenAQ
- Large measurement series stage on a DataCanvas for read-only DuckDB SQL, one table per sensor, so two series on one
canvas_idcan be joined
Agent-friendly output:
- Measured, not modeled: a search with no station fails as
no_locations_foundorno_station_near_coordinates, stated as no coverage rather than clean air - Units travel with every value and are never converted;
parametersIdselects pollutant and unit together - Freshness and truncation are explicit: per-value timestamps and
datetimeLast, andtotalCount/truncated/noticeon capped results
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:
{
"mcpServers": {
"openaq-mcp-server": {
"type": "streamable-http",
"url": "https://openaq.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
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"
]
}
}
}
Add "CANVAS_PROVIDER_TYPE": "duckdb" to env to enable DataCanvas SQL over large measurement series.
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
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- A free OpenAQ v3 API key from an OpenAQ Explorer account.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/openaq-mcp-server.git
- Navigate into the directory:
cd openaq-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env and set OPENAQ_API_KEY
Configuration
| Variable | Description | Default |
|---|---|---|
OPENAQ_API_KEY | Required. OpenAQ v3 API key, sent as the X-API-Key header. | — |
OPENAQ_API_BASE_URL | OpenAQ v3 API base URL, for a proxy or test mirror. | https://api.openaq.org/v3 |
CANVAS_PROVIDER_TYPE | duckdb stages large measurement series for SQL via the dataframe tools; without it, large series return a preview plus a notice. The .mcpb bundle ships without DuckDB, so use npm, npx, or Docker for canvas work. | none |
OPENAQ_ENABLE_CANVAS_DROP | Enable openaq_dataframe_drop to delete entire canvases and release their resources. Requires DuckDB; an id shared with another agent grants deletion access within the same tenant. | false |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. Overrides the server's explicit stateless posture; auto resolves to stateful. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
OTEL_ENABLED | Enable OpenTelemetry. | false |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP base URL; traces use /v1/traces and metrics use /v1/metrics. Signal-specific endpoints override it. | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Opt into OTLP log export with a full endpoint URL. The base endpoint never enables logs. | — |
LOG_TOOL_FAILURE_PAYLOADS | Log failed calls' arguments and results. Redacts by key name only; secrets in free-form values remain. | false |
LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES | UTF-8 byte cap per logged failure payload. | 16384 |
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: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
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. DuckDB 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 |
|---|---|
src/index.ts | createApp() entry point: registers tools and resources, wires the service and canvas. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (definitions/*.tool.ts) and shared input, error, and format helpers (shared/). |
src/mcp-server/resources | Resource definitions (definitions/*.resource.ts). |
src/services | OpenAQ v3 client and domain types (openaq/), plus the DataCanvas accessor. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md / AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — catch only to map a failure to a declared error reason or to keep a partial result
- Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources in the
createApp()arrays - Wrap the OpenAQ API: validate raw → normalize to the domain type → return the output schema; surface units verbatim and never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.
Data comes from the OpenAQ platform, and attribution to OpenAQ is required when using this server's output (Terms of Use). OpenAQ aggregates measurements from government agencies, research institutions, and other networks, each of which may set its own attribution or licensing terms. The provider field on openaq_find_locations, openaq_get_readings, and openaq_get_measurements results and the openaq://location/{locationId} resource names the originating network; review and follow the terms of any provider whose data you use.
Frequently asked questions
What is io.github.cyanheads/openaq-mcp-server?
Find air-quality stations and read pollutant observations from government monitors via OpenAQ v3.
How do I install io.github.cyanheads/openaq-mcp-server?
It runs as a remote server. In Claude Code: claude mcp add --transport http openaq-mcp-server 'https://openaq.caseyjhand.com/mcp'. For other clients, paste the JSON configuration from this page into their MCP settings.
Does io.github.cyanheads/openaq-mcp-server need an API key?
Yes. It expects OPENAQ_API_KEY, which you set in its configuration.
Is io.github.cyanheads/openaq-mcp-server open source?
Yes. Its code is on GitHub under the Apache-2.0 license.
Who maintains io.github.cyanheads/openaq-mcp-server?
It is published by cyanheads. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.