.codex-plugin — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited .codex-plugin (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<div align="center"> <h1>@cyanheads/openaq-mcp-server</h1> <p><b>Find air-quality monitoring stations, read latest sensor values, and pull historical pollutant series via MCP. STDIO & Streamable HTTP.</b> <div>7 Tools (2 opt-in) • 2 Resources</div> </p> </div>
<div align="center">
</div>
<div align="center">
</div>
<div align="center">
Public Hosted Server: https://openaq.caseyjhand.com/mcp
</div>
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.
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 |
|---|---|
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, each 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. 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. |
openaq_find_locationsFind air-quality monitoring stations (measured by physical sensors, not modeled) and the parameters each one reports.
coordinates + 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 sensors)isMonitor/isMobile, the parameters its sensors measure with units, and the datetimeFirst/datetimeLast data spanopenaq_list_countries, or fall back to the modeled open-meteo air-quality toolopenaq_get_readingsLatest value per sensor at a station — the current-conditions tool.
locationId 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 station, so "latest" may be minutes or hours oldopenaq_get_measurementsHistorical measurement series for one pollutant at one station over a date range — for trend analysis and "was last week worse than the monthly average?".
locationId and a parametersId; the tool resolves the station's sensor for that parameter internally (v3 series are sensor-scoped, but you think in stations)aggregation: raw (every reported value), hourly, or daily — hourly/daily add a per-bucket statistical summary (min, median, max, mean, sd)datetimeFrom/datetimeTo accept a date (YYYY-MM-DD) or full UTC timestamp (YYYY-MM-DDTHH:MM:SSZ); omit for the most recent valuesA 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 full set on a DuckDB-backed DataCanvas and returns:
series, capped at 100 rows) plus rowCount and the totalCount enrichment,truncated: true, canvasId, and a tableName of the form measurements_<sensorId>.You then query the full set with the two consumer tools:
| Tool | Use |
|---|---|
openaq_dataframe_describe | List staged tables and their columns (value, datetimeFrom, datetimeTo, min, median, max, avg, sd, percentComplete, flagged) — call this first to write SQL without guessing names. |
openaq_dataframe_query | Run a read-only SELECT for monthly means, exceedance counts, percentiles, or cross-sensor comparisons. |
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.
openaq_dataframe_query is read-only by design — a four-layer SQL gate rejects writes, DDL, and file/network table functions; only a single SELECT runs.
| Type | Name | Description |
|---|---|---|
| Resource | openaq://location/{locationId} | Location metadata for a known location id — name, coordinates, country, provider, sensors (each with parameter + unit), and data span. |
| Resource | 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. 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).
Built on @cyanheads/mcp-ts-core:
reason/code/when/recovery, so failures carry a concrete next movenone, jwt, oauth) and structured, request-scoped logging with optional OpenTelemetry tracingOpenAQ-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:
co is id 4 µg/m³, id 8 ppm, id 102 ppb), so 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/mcpgit clone https://github.com/cyanheads/openaq-mcp-server.gitcd openaq-mcp-serverbun installcp .env.example .env
# edit .env and set OPENAQ_API_KEYAll 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.
bun run rebuild
bun run start:http # or start:stdio bun run devcheck # Lint, format, typecheck, security, changelog sync
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against specdocker 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.
| 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 and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testApache-2.0 — see LICENSE for details.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.