openaq-mcp-server

v0.1.1 pre-1.0

Find air-quality monitoring stations and read measured pollutant observations (PM2.5, PM10, O3, NO2, SO2, CO, and more) from government monitors worldwide via the OpenAQ v3 API, with DataCanvas SQL over historical series.

openaq.caseyjhand.com/mcp
claude mcp add --transport http openaq-mcp-server https://openaq.caseyjhand.com/mcp
codex mcp add openaq-mcp-server --url https://openaq.caseyjhand.com/mcp
{
  "mcpServers": {
    "openaq-mcp-server": {
      "url": "https://openaq.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http openaq-mcp-server https://openaq.caseyjhand.com/mcp
{
  "mcpServers": {
    "openaq-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://openaq.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "openaq-mcp-server": {
      "type": "http",
      "url": "https://openaq.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://openaq.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

7

openaq_find_locations

open-world

Find air-quality monitoring stations (measured by physical sensors, not modeled) near a point, within a bounding box, or by country. Returns each station's id, name, coordinates, distance from the query point (when searching by coordinates), country, provider, the parameters its sensors measure, and the timestamp of its most recent data (datetimeLast). Required first step: openaq_get_readings and openaq_get_measurements key on the location id this returns. Coverage is uneven and real — a station only reports the parameters it measures, and the absence of a nearby station means no monitoring there, not clean air. For dense modeled coverage anywhere on Earth, use open-meteo-mcp-server's air-quality tool instead.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "openaq_find_locations",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "coordinates": {
      "type": "string",
      "pattern": "^-?\\d{1,3}(\\.\\d+)?,-?\\d{1,3}(\\.\\d+)?$",
      "description": "Center point as \"latitude,longitude\" (e.g. \"47.6062,-122.3321\"). Pair with radius for a near-me search. Resolve a place name to coordinates with openstreetmap-mcp-server or open-meteo geocode first. Provide either coordinates+radius OR bbox, not both."
    },
    "radius": {
      "default": 12000,
      "description": "Search radius in metres around coordinates (1–25000; the API hard-caps at 25000). Default 12000 (~12km). Only used with coordinates.",
      "type": "integer",
      "minimum": 1,
      "maximum": 25000
    },
    "bbox": {
      "type": "string",
      "pattern": "^(-?\\d+(\\.\\d+)?,){3}-?\\d+(\\.\\d+)?$",
      "description": "Bounding box as \"minLon,minLat,maxLon,maxLat\" (west,south,east,north). Alternative to coordinates+radius for area sweeps. Results have no distance field (no center point)."
    },
    "iso": {
      "description": "Restrict to a country by ISO 3166-1 alpha-2 code (e.g. \"US\", \"IN\", \"DE\"). Combine with bbox/coordinates to scope, or use alone for a country-wide list. Discover coverage with openaq_list_countries.",
      "type": "string",
      "minLength": 2,
      "maxLength": 2
    },
    "parametersId": {
      "description": "Only return stations that measure this parameter id (e.g. 2 = PM2.5 µg/m³). Get ids from openaq_list_parameters — the same pollutant has several ids for different units. Narrows the station set; each returned station still lists all its sensors.",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "limit": {
      "default": 20,
      "description": "Max stations to return (1–100). Default 20. Results are ordered by distance when searching by coordinates.",
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    }
  },
  "required": [
    "radius",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

openaq_get_readings

open-world

Latest measured value for every sensor at a monitoring station — the current-conditions tool. Returns one record per parameter, each with the value, its unit, the UTC and local timestamp, and the sensor id, joined so every value carries its pollutant and unit (the raw latest feed is keyed only by sensor id). Pass a locationId from openaq_find_locations, or pass coordinates to auto-resolve to the nearest station that measures the requested parametersId. Data recency varies by station reporting cadence — read each value's timestamp to know whether "latest" is minutes or hours old. These are measured observations with coverage gaps, not a modeled grid.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "openaq_get_readings",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "locationId": {
      "description": "Station id from openaq_find_locations. Provide this OR coordinates. When set, returns the latest value for every sensor at this station.",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "coordinates": {
      "type": "string",
      "pattern": "^-?\\d{1,3}(\\.\\d+)?,-?\\d{1,3}(\\.\\d+)?$",
      "description": "Fallback \"latitude,longitude\" when you do not have a locationId — resolves to the nearest station (within 25km) that measures parametersId, then reads its latest values. Requires parametersId."
    },
    "parametersId": {
      "description": "Required with coordinates: which parameter id the nearest station must measure (get ids from openaq_list_parameters). With locationId, optionally filters the returned values to this parameter id; omit to get all sensors.",
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    }
  },
  "additionalProperties": false
}
view source ↗

openaq_get_measurements

open-world

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 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). Choose aggregation: raw (every reported value), hourly, or daily — daily and hourly add a per-bucket statistical summary (min, median, max, mean, sd). Large ranges produce thousands of rows and spill to a DataCanvas: the response returns a preview plus a canvasId and table name you query with openaq_dataframe_query. Values carry their unit; the server never converts between µg/m³, ppm, and ppb.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "openaq_get_measurements",
    "arguments": {
      "locationId": "<locationId>",
      "parametersId": "<parametersId>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "locationId": {
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991,
      "description": "Station id from openaq_find_locations."
    },
    "parametersId": {
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991,
      "description": "Parameter id to pull the series for (e.g. 2 = PM2.5 µg/m³). Get ids from openaq_list_parameters. Must be a parameter the station measures — find_locations lists each station's parameters."
    },
    "datetimeFrom": {
      "description": "Start of the range, inclusive. Date \"YYYY-MM-DD\" or full UTC \"YYYY-MM-DDTHH:MM:SSZ\". Omit to get the most recent values.",
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}Z)?$"
    },
    "datetimeTo": {
      "description": "End of the range, inclusive. Must be on or after datetimeFrom. Omit for \"up to now\".",
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}Z)?$"
    },
    "aggregation": {
      "default": "raw",
      "description": "Time bucketing. \"raw\" = every reported value (often hourly at source). \"hourly\"/\"daily\" = server-side rollups with a statistical summary per bucket. Use \"daily\" for multi-month trends to keep the series small; \"raw\" for fine-grained recent analysis.",
      "type": "string",
      "enum": [
        "raw",
        "hourly",
        "daily"
      ]
    },
    "limit": {
      "default": 1000,
      "description": "Max rows per page from the API (1–1000). Default 1000. The tool pages internally up to the spill threshold.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "canvas_id": {
      "description": "DataCanvas id from a prior call to reuse the same canvas (e.g. to compare two stations' series side by side). Omit to start fresh; the response returns a new canvas_id when the series spills.",
      "type": "string"
    }
  },
  "required": [
    "locationId",
    "parametersId",
    "aggregation",
    "limit"
  ],
  "additionalProperties": false
}
view source ↗

openaq_list_parameters

Catalog of every measurable pollutant and its canonical unit: id, code, display name, unit, and a one-line description (pm25, pm10, o3, no2, so2, co, bc, and ~38 more). This is the unit-disambiguation reference — the same pollutant exists under several ids with different units (CO is id 4 in µg/m³, id 8 in ppm, id 102 in ppb), so use this to pick the exact parametersId for openaq_find_locations / openaq_get_readings / openaq_get_measurements and to interpret a reading's unit. A small bounded catalog fetched live from OpenAQ.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "openaq_list_parameters",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "description": "Local case-insensitive filter on code, display name, and description (e.g. \"pm\" for particulates, \"ozone\", \"co\"). The full catalog is small (~44 entries); omit to list everything. This filters the fetched list on our side — it is not an upstream search.",
      "type": "string"
    },
    "pollutantsOnly": {
      "default": false,
      "description": "When true, exclude meteorological/auxiliary parameters (temperature, humidity, wind, pressure, particle-count channels) and return only air pollutants. Default false (full catalog).",
      "type": "boolean"
    }
  },
  "required": [
    "pollutantsOnly"
  ],
  "additionalProperties": false
}
view source ↗

openaq_list_countries

Catalog of country-level coverage: id, ISO code, name, the date span of available station data (datetimeFirst/datetimeLast), and which parameters are measured anywhere in that country. The availability check before a regional sweep — answers "which countries have NO2 monitoring?" and tells you whether a country has recent data before you call openaq_find_locations. Coverage is uneven worldwide; this surfaces where measured data exists.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "openaq_list_countries",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "description": "Local case-insensitive filter on country code and name (e.g. \"united\", \"IN\", \"germany\"). The list is bounded (~153 countries); omit to list all. Filters the fetched list on our side, not an upstream search.",
      "type": "string"
    }
  },
  "additionalProperties": false
}
view source ↗

openaq_dataframe_query

Run a read-only SQL SELECT against the measurement tables openaq_get_measurements staged on a DataCanvas. Reference tables by the name the measurements call returned (measurements_<sensorId>). For aggregation (monthly means, exceedance counts) and cross-sensor comparison over series too large to inline. Only SELECT is allowed — a four-layer gate rejects writes, DDL, and file/network table functions.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "openaq_dataframe_query",
    "arguments": {
      "canvas_id": "<canvas_id>",
      "sql": "<sql>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "description": "DataCanvas id returned by openaq_get_measurements when a series spilled."
    },
    "sql": {
      "type": "string",
      "description": "Read-only SELECT. Reference tables by the names openaq_get_measurements returned (e.g. measurements_1701). Use openaq_dataframe_describe first to see table and column names."
    }
  },
  "required": [
    "canvas_id",
    "sql"
  ],
  "additionalProperties": false
}
view source ↗

openaq_dataframe_describe

List the tables and columns staged on a DataCanvas so you can write valid SQL for openaq_dataframe_query without guessing column names. Returns each measurement table (measurements_<sensorId>) with its row count and column names. Throws canvas_unavailable when DuckDB is off.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "openaq_dataframe_describe",
    "arguments": {
      "canvas_id": "<canvas_id>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "description": "DataCanvas id returned by openaq_get_measurements when a series spilled."
    }
  },
  "required": [
    "canvas_id"
  ],
  "additionalProperties": false
}
view source ↗

Resources

2

Location metadata for a known OpenAQ location id: name, coordinates, country, provider, the sensors it carries (each with parameter + unit), and the datetimeFirst/datetimeLast data span. Mirror of openaq_find_locations output for a single station.

uri openaq://location/{locationId} mime application/json

Full catalog of measurable pollutants and their canonical units (id, code, display name, unit, description). Same data as openaq_list_parameters. The unit-disambiguation reference — the same pollutant appears under several ids with different units (CO is id 4 µg/m³, id 8 ppm, id 102 ppb).

uri openaq://parameters mime application/json