openaq-mcp-server
Server Details
Find air-quality stations and read pollutant observations from government monitors via OpenAQ v3.
- Status
- Healthy
- Uptime
- 99.9% over 41 days
- Last Tested
- Transport
- Streamable HTTP · MCP 2025-11-25
- URL
- Repository
- cyanheads/openaq-mcp-server
- GitHub Stars
- 2
- Server Listing
- @cyanheads/openaq-mcp-server
TDQS
Scored across 7 tools
Each tool targets a distinct concern: catalog lookups, station search, current readings, historical measurements, and SQL over staged data. Even the get_readings/get_measurements pair is clearly separated by current vs. historical scope. No two tools are likely to be confused.
Most tools follow a clear openaq_verb_noun pattern (openaq_find_locations, openaq_get_measurements, openaq_list_parameters). The two dataframe tools reverse this to openaq_dataframe_describe/query, creating a minor but noticeable inconsistency.
Seven tools is well-scoped for an air-quality data access server. Each tool serves a necessary step in the workflow: discover catalogs, find stations, fetch current or historical data, and analyze large result sets via SQL.
The surface covers the full read-only data lifecycle: parameter/country catalogs, station discovery, current observations, historical time series, and large-data analysis. There are no obvious dead ends or missing core operations for the domain.
Available Tools
7 toolsopenaq_dataframe_describeopenaq-mcp-server: dataframe describeARead-onlyInspect
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_) with its row count and column names. Requires DataCanvas to be enabled.
| Name | Required | Description | Default |
|---|---|---|---|
| canvas_id | Yes | DataCanvas id returned by openaq_get_measurements — minted when a series overflowed the inline preview, or the canvas_id you passed it. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| notice | No | Guidance when the canvas holds no tables yet. |
| tables | No | Tables currently staged on the canvas. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already mark readOnlyHint=true. The description adds useful behavioral context beyond that: it reports row counts and column names per measurement table, and requires DataCanvas to be enabled. This is adequate for a read-only metadata inspection tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the core purpose and output, with no filler or redundant restatement of the title. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter read-only tool with a full input schema and an output schema, the description covers purpose, prerequisite, and return contents. Nothing an agent needs to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and fully explains canvas_id, including its origin and pattern. The tool description adds no extra parameter-level meaning, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description uses a specific verb ('List') and resource ('tables and columns staged on a DataCanvas'), and explicitly ties its purpose to enabling valid SQL for openaq_dataframe_query. This clearly distinguishes it from the querying sibling by stating it returns metadata rather than data.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Description explains the intended context: use it before openaq_dataframe_query to avoid guessing column names. It also notes the DataCanvas must be enabled. It does not explicitly discuss when not to use it or alternatives, but the use-before-query guidance is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
openaq_dataframe_queryopenaq-mcp-server: dataframe queryARead-onlyInspect
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_). For aggregation (monthly means, exceedance counts) and cross-sensor comparison over series too large to inline. Only SELECT is allowed — writes, DDL, and file/network table functions are rejected. Responses carry at most 200 rows; aggregate in SQL, or page with ORDER BY plus LIMIT/OFFSET, rather than selecting a whole table.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | 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. | |
| canvas_id | Yes | DataCanvas id returned by openaq_get_measurements — minted when a series overflowed the inline preview, or the canvas_id you passed it. |
Output Schema
| Name | Required | Description |
|---|---|---|
| rows | No | Result rows, at most 200. Every row here is also rendered in the text output — the two surfaces carry the same set. |
| error | No | Present when the call failed. Absent on success. |
| notice | No | How to reach the rest of the result when the row cap cut it short. |
| rowCount | No | Rows returned in this response, always equal to rows.length. It is the cap (200) when truncated is set, not the size of the full result. |
| truncated | No | True when the query matched more than 200 rows and the response was cut to the cap. Absent when the whole result fit. Page through the rest with ORDER BY plus LIMIT/OFFSET in your own SQL. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Even though readOnlyHint is already true, the description adds valuable behavioral detail beyond the annotation: only SELECT is allowed, writes/DDL/file/network functions are rejected, and responses cap at 200 rows. It also gives pagination guidance with ORDER BY and LIMIT/OFFSET.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but tightly organized, front-loading the core operation and table-referencing rule before restrictions and row limitations. Every sentence carries operational value with no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With annotations, full schema coverage, and an output schema present, the description covers all essentials an agent needs: table naming convention, SELECT-only restriction, row cap, and paging strategy. Nothing critical is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the input schema already explains both parameters. The description reinforces the canvas_id source but does not add significant meaning beyond the schema's own wording; baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'Run a read-only SQL SELECT against the measurement tables openaq_get_measurements staged on a DataCanvas.' It also distinguishes itself as the SQL/aggregation tool versus the table-describe tool and the raw data access siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It clearly states when to use it: 'for aggregation (monthly means, exceedance counts) and cross-sensor comparison over series too large to inline.' It also points to openaq_dataframe_describe as a precursor step, though it doesn't explicitly name which sibling tools are not appropriate.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
openaq_find_locationsopenaq-mcp-server: find locationsARead-onlyIdempotentInspect
Find air-quality monitoring stations (measured by physical sensors, not modeled) near a point, within a bounding box, or by country, optionally narrowed to one parameter, one station class (reference monitors or low-cost sensors, mobile or fixed), or one provider network. Returns each station's id, name, coordinates, distance from the query point (when searching by coordinates), country, provider name and id, 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.
| Name | Required | Description | Default |
|---|---|---|---|
| iso | No | Restrict to a country by OpenAQ country code: ISO 3166-1 alpha-2 (e.g. "US", "IN", "DE"; either case), or "-99" where OpenAQ lists a country with no ISO code. Take codes from openaq_list_countries. Combine with bbox/coordinates to scope, or use alone for a country-wide list. | |
| bbox | No | Bounding box as "minLon,minLat,maxLon,maxLat" (west,south,east,north), with minLon ≤ maxLon and minLat ≤ maxLat. Alternative to coordinates+radius for area sweeps. Results have no distance field (no center point). | |
| page | No | Which page of results to return (1-based). Default 1. The only way past the 100-station cap: with limit 100, page 2 returns stations 101–200. Distance ordering applies within a page, not across pages, so paging is for iso/bbox sweeps — a near-me coordinates search should stay on page 1. A page past the last one fails with page_exhausted. | |
| limit | No | Max stations to return (1–100). Default 20. Results are ordered by distance when searching by coordinates. | |
| mobile | No | Mobility filter: true returns only mobile stations, false only fixed ones. Omit for both. | |
| radius | No | Search radius in metres around coordinates (1–25000; the API hard-caps at 25000). Default 12000 (~12km). Requires coordinates — a radius sent with only bbox or iso is rejected. | |
| monitor | No | Station class filter: true returns only reference-grade monitors, false only low-cost sensors. Omit for both. | |
| coordinates | No | 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. | |
| providersId | No | Only return stations from this OpenAQ provider (data network) id — read it from a previous result's providerId (e.g. 119 = AirNow). | |
| parametersId | No | 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. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The limit that was applied. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Number of stations returned. |
| notice | No | Guidance on a full page: the next page to request, or how to narrow the search. |
| locations | No | Matching stations on this page, never empty: a query with no match fails with no_locations_found (no monitoring coverage, NOT clean air), and a page past the last with page_exhausted. |
| truncated | No | True when this page came back full (the limit was reached), so the next page may hold more stations. |
| totalCount | No | Stations counted through this page: (page − 1) × limit plus the stations returned. Exact on a page that came back short of the limit (the last page); a floor when totalCountIsLowerBound is true. |
| totalCountIsLowerBound | No | True when this page came back full: at least totalCount stations match, and the next page may hold more. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already signal read-only, idempotent, and open-world behavior, and the description adds meaningful non-obvious context: stations are physical rather than modeled, coverage is uneven, an absence of stations does not mean clean air, and each returned station lists only the parameters it actually measures. These behavioral caveats materially affect how an agent interprets results.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The main description is compact and front-loaded: the primary action and query modes appear first, followed by return contents, workflow hint, and the important coverage caveat. Each sentence adds distinct value, and the alternative-tool routing is placed at the end without bloating the core purpose.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's high complexity (10 optional parameters, multiple search modes, an output schema, and sibling tools), the description plus schema is fully sufficient for an agent to select, invoke, and interpret the tool. It covers the result shape, the station-id handoff to downstream tools, the physical-vs-modeled distinction, and the key caveat about data absence.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already documents all 10 parameters with 100% coverage, including formats, defaults, constraints, and cross-parameter rules. The description's mention of 'one parameter, one station class... or one provider network' is a high-level restatement of parametersId, monitor/mobile, and providersId rather than new semantic information, so it does not exceed the schema-driven baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('Find') with a precise resource ('air-quality monitoring stations') and enumerates the exact query modes: near a point, within a bounding box, or by country. It also differentiates physical-sensor stations from modeled data and names the downstream tools that consume the returned location id, leaving no ambiguity about what this tool is for.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly positions this as the required first step before openaq_get_readings and openaq_get_measurements, and it names an alternative tool (open-meteo-mcp-server air-quality) for dense modeled coverage. This gives an agent clear routing guidance beyond the raw schema.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
openaq_get_measurementsopenaq-mcp-server: get measurementsARead-onlyIdempotentInspect
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 and work in stations — you get the series for that pollutant at that station. Choose aggregation: raw (every reported value), hourly, or daily — daily and hourly add a per-bucket statistical summary (min, median, max, mean, sd). A date-only bound means the station's local calendar day. Large ranges produce thousands of rows and stage on a DataCanvas: the response returns a preview plus a canvasId and table name — call openaq_dataframe_describe on the canvasId for the table's columns, then openaq_dataframe_query to run SQL over it. Passing a canvas_id stages the series there whatever its size, so two stations land on one canvas for a side-by-side comparison. Values carry their unit; the server never converts between µg/m³, ppm, and ppb.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max rows per page from the API (1–1000). Default 1000. The tool pages internally up to the 5000-row pull ceiling. | |
| canvas_id | No | DataCanvas id from a prior openaq_get_measurements call, to put this series on the same canvas (e.g. to compare two stations' series side by side). Supplying it stages the series whatever its size. Reuse stages one table per sensor, so a second sensor adds a table while the same sensor overwrites its earlier series — the response says so when that happens. Omit to start fresh; the response returns a new canvas_id when the series overflows the inline preview. | |
| datetimeTo | No | End of the range, inclusive. A date "YYYY-MM-DD" covers that whole station-local day, closing at the next local midnight, so a DST day spans 23 or 25 hours; a full UTC "YYYY-MM-DDTHH:MM:SSZ" is sent as is. Must land after datetimeFrom — the two forms mix freely, so "2026-06-25" to "2026-06-25" is a valid one-day range. Omit for "up to now". effectiveRange echoes the instant sent. | |
| locationId | Yes | Station id from openaq_find_locations. | |
| aggregation | No | Time bucketing. "raw" = every reported value (often hourly at source). "hourly"/"daily" = server-side rollups with a statistical summary per bucket; an hour is labeled by the time it ends, and a day is the station's local calendar day. Use "daily" for multi-month trends to keep the series small; "raw" for fine-grained recent analysis. | raw |
| datetimeFrom | No | Start of the range, inclusive. A date "YYYY-MM-DD" opens at local midnight of that day in the station's timezone (UTC midnight when OpenAQ lists none); a full UTC "YYYY-MM-DDTHH:MM:SSZ" is sent as is. Omit to start from the sensor's earliest data — the series runs oldest first, so on a long-running station an open start fills the row cap with its oldest values; set datetimeFrom to reach recent ones. effectiveRange echoes the instant sent. | |
| parametersId | Yes | 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. |
Output Schema
| Name | Required | Description |
|---|---|---|
| gaps | No | The first 20 missing intervals, oldest first. Omitted when gapCount is 0. |
| error | No | Present when the call failed. Absent on success. |
| notice | No | What limited this response or where the rest of it lives — the row cap, a failed page, a station with no timezone, an edge bucket clipped by the range, missing intervals, DataCanvas being unavailable, or the canvas table the series was staged on and the tools that read it. |
| series | No | The (possibly previewed) series in the order OpenAQ returns it (oldest first). An hourly/daily series either skips a missing bucket or returns it with a null value — gapCount and gaps report both. Every row here is also rendered in the text output. When truncated, this is a preview of pulledCount rows — query canvasId for the rest. |
| canvasId | No | DataCanvas id holding the staged series — pulledCount rows of it. Call openaq_dataframe_describe on this id for the table's columns, then openaq_dataframe_query to run SQL. Present whenever staging succeeded, which includes a series that fit inline on a canvas_id you supplied. |
| gapCount | No | Missing intervals inside an hourly or daily series — a span between buckets that do not touch, or a bucket with a null value, merged where contiguous — counted over every pulled row, not only the preview. 0 when nothing is missing; absent for raw, whose rows follow no fixed cadence. |
| location | No | Station the series came from |
| rowCount | No | Rows in this response (preview length when spilled) |
| sensorId | No | Resolved sensor id the series was pulled from |
| parameter | No | What was measured, resolved from the station's sensor |
| tableName | No | Canvas table holding the staged series (e.g. "measurements_1701"). openaq_dataframe_describe lists its columns; reference this name in openaq_dataframe_query SQL. One table per sensor, so re-staging the same sensor on this canvas overwrites it. |
| truncated | No | True when the series exceeded the inline limit, so series is a preview of the pulled rows. Absent/false when every pulled row is inline. It describes the preview only — canvasId reports whether the rows were staged, and pullComplete whether the pull itself finished. |
| totalCount | No | Rows in the full series for this range. A floor rather than an exact count when totalCountIsLowerBound is set; never below pulledCount. |
| aggregation | No | Bucketing applied |
| pulledCount | No | Rows pulled from OpenAQ, at most 5000 — the canvas table's row count when canvasId is present. Equals rowCount when the whole series fit inline; larger when series is a preview. |
| pullComplete | No | True when pulledCount is the whole series for the requested range. False when the 5000-row cap or a failed page stopped the pull early — the rows past that point are in neither this response nor the canvas table, and the notice says how to reach them. |
| effectiveRange | No | The range sent to OpenAQ as UTC instants — date-only bounds expanded to the station's local day. |
| totalCountIsLowerBound | No | Set when totalCount is only a floor: the pull stopped early and OpenAQ reported the range total as ">N" instead of an exact number, so more rows exist than totalCount states. Absent when the count is exact. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the annotations (readOnlyHint, openWorldHint, idempotentHint), the description discloses crucial behavior: date-only bounds map to local calendar days, DST variation, unit handling (never converts), aggregation summaries, overwriting behavior on canvas reuse, and the staging threshold with a preview + canvasId. This is far richer than what annotations provide and sets clear expectations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but every sentence carries critical information. It is well-structured: purpose first, then usage, then parameter semantics, then staging behavior. The length is justified by the tool's complexity; it could be slightly tightened but remains efficient.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with 7 parameters, detailed date handling, staging, and a documented output schema, the description covers all essentials: how to specify ranges, what the response contains (preview, canvasId, table), downstream tools to use, and unit behavior. Nothing an agent needs to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Even though schema coverage is 100%, the description adds substantial meaning: it explains date range interpretation (inclusive, local midnight, DST), aggregation semantics (bucket labeling, summary stats), canvas_id behavior (staging, overwrite), and how locationId/parametersId are obtained (from find_locations/list_parameters). This goes well beyond the schema's descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('get') and resource ('measurements series') with clear scope: one pollutant, one station, over a date range. It immediately names the use case (trend analysis) and distinguishes itself from sibling tools like openaq_dataframe_describe/query and openaq_get_readings by focusing on station-level series.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit guidance on when to use this tool (trend analysis, comparing weeks), how to choose aggregation ('daily' for multi-month trends, 'raw' for fine-grained), and how to handle large ranges (staging on DataCanvas with openaq_dataframe_describe/query). It also explains the canvas_id reuse pattern for side-by-side comparisons, covering both usage and alternatives implicitly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
openaq_get_readingsopenaq-mcp-server: get readingsARead-onlyIdempotentInspect
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). The station block names its provider (for attribution) and timezone. 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.
| Name | Required | Description | Default |
|---|---|---|---|
| locationId | No | Station id from openaq_find_locations. Provide this OR coordinates. When set, returns the latest value for every sensor at this station. | |
| coordinates | No | 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 | No | 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. |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| notice | No | Set when coordinate resolution compared a full 1,000-station page: more stations may match, so the station returned is the nearest of the first 1,000 OpenAQ lists, not necessarily the nearest overall. |
| location | No | The station these readings came from |
| readings | No | Latest value per sensor. An old datetime means the station reports infrequently or is stale — not that the value is current. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, openWorldHint, and idempotentHint, lowering the bar. The description goes well beyond this by revealing that the raw feed is keyed only by sensor iderview, that timestamps must be read to assess data recency, and that the data are 'measured observations with coverage gaps, not a modeled grid.' These are non-obvious behavioral and data-quality traits that help an agent interpret results correctly.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Five sentences, each earning its place: what it returns, the join behavior, the station block contents, the two invocation modes, and the recency/data-quality caveats. It is front-loaded with the main purpose and avoids any redundant restatement of the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers return shape, key fields, call patterns, and important data caveats, and an output schema exists. One gap: the schema marks zero parameters as required, and the description does not explicitly state 'provide exactly one of locationId or coordinates,' which could allow an agent to omit both and receive an error. This is a minor completeness issue given the strong surrounding context.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, with detailed explanations for locationId, coordinates, and parametersId. The description tells the agent where to find a locationId (from find_locations) and that coordinates resolve to the nearest station, but it adds little beyond the schema. Baseline 3 fits when the schema already carries parameter semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a precise statement: 'Latest measured value for every sensor at a monitoring station' and labels itself 'the current-conditions tool.' This clearly differentiates it from siblings like openaq_get_measurements and openaq_find_locations, and it names the resource (readings) with an explicit verb (get).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explains the two invocation patterns (locationId vs coordinates), references openaq_find_locations for obtaining a station id, and notes that coordinates need parametersId. The 'current-conditions' label implies a contrast with historical time-series tools, but it does not explicitly state 'do not use this for historical data, use openaq_get_measurements instead,' so the exclusion is not fully spelled out.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
openaq_list_countriesopenaq-mcp-server: list countriesARead-onlyIdempotentInspect
Catalog of country-level coverage: id, OpenAQ country 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. Results come a page at a time (20 countries by default); totalCount is the full filtered count.
| Name | Required | Description | Default |
|---|---|---|---|
| page | No | Which page of the filtered list to return (1-based). Default 1. With limit 20, page 2 returns countries 21–40. A page past the last one returns no countries and a notice naming the last page. | |
| limit | No | Max countries to return (1–100). Default 20. Applied after query and parametersId, in OpenAQ catalog order. | |
| query | No | Case-insensitive filter over the country catalog by code and name. A two-letter query matches an exact ISO 3166-1 alpha-2 code first (e.g. "US" → United States) and falls back to substrings when no code matches; longer queries match as substrings (e.g. "united", "germany"). Omit to page through the whole catalog. | |
| parametersId | No | Only return countries that measure this parameter id somewhere (e.g. 2 = PM2.5 µg/m³) — the one-call answer to "which countries have NO2 monitoring?". Get ids from openaq_list_parameters; the same pollutant has several ids for different units. Composes with query. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | No | The limit that was applied. |
| error | No | Present when the call failed. Absent on success. |
| shown | No | Number of countries returned on this page. |
| notice | No | Guidance when the filters matched nothing, when more pages follow (the next page to request), or when the page is past the last one. |
| countries | No | Matching countries with coverage metadata. |
| truncated | No | True when more matching countries follow on later pages. |
| totalCount | No | Countries matched after query and parametersId, across every page. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and idempotentHint=true, so the description doesn't need to cover safety. It adds behavioral details beyond annotations: pagination behavior ('Results come a page at a time (20 countries by default); totalCount is the full filtered count') and data coverage note ('Coverage is uneven worldwide'). These are useful traits not conveyed by annotations, so the description adds value.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is four sentences, each earning its place: the first defines the output fields, the second gives usage context, the third notes data quality, and the fourth explains pagination. It is front-loaded with the core purpose and contains zero filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's moderate complexity (4 optional params), the presence of a detailed output schema, and annotations covering read-only behavior, the description is complete. It explains what the tool returns, when to use it, pagination, and data quality caveats. An agent has everything needed to decide and invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has 100% coverage with rich, self-contained descriptions for every parameter (page, limit, query, parametersId). For example, parametersId already explains 'Get ids from openaq_list_parameters' and the 'NO2 monitoring' use case. The tool description adds no new parameter-specific meaning beyond what the schema provides, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it returns a catalog of country-level coverage with specific fields (id, code, name, date span, parameters). It uses a specific verb (list/catalog) and resource (countries), and distinguishes itself from siblings by positioning it as an availability check before openaq_find_locations. The purpose is unambiguous and differentiated.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly states when to use this tool: 'The availability check before a regional sweep' and 'tells you whether a country has recent data before you call openaq_find_locations.' It also names the sibling (openaq_find_locations) as the next step, and answers specific questions like 'which countries have NO2 monitoring?' This is explicit usage guidance with a clear alternative.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
openaq_list_parametersopenaq-mcp-server: list parametersARead-onlyIdempotentInspect
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 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.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Case-insensitive filter over the bounded parameter catalog by code, display name, and description (e.g. "pm" for particulates, "ozone", "co"). Omit to list everything. | |
| pollutantsOnly | No | When true, exclude meteorological/auxiliary parameters (temperature, humidity, wind, pressure, particle-count channels) and return only air pollutants. Default false (full catalog). |
Output Schema
| Name | Required | Description |
|---|---|---|
| error | No | Present when the call failed. Absent on success. |
| notice | No | Guidance when the query matched nothing. |
| parameters | No | Matching parameters. Multiple rows can share a name with different ids/units — pick the id whose unit you want. |
| totalCount | No | Total parameters matched after filtering. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint and idempotentHint, so safety is covered. The description adds genuine behavioral context beyond that: the catalog is bounded, fetched live from OpenAQ, and contains multiple ids for the same pollutant with different units. It stops short of mentioning rate limits or expected catalog size, hence 4 rather than 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but every sentence earns its place: scope, fields, canonical-unit pitfall with concrete examples, downstream usage, and live-fetch behavior. It is front-loaded with 'Catalog of every measurable pollutant and its canonical unit' and wastes no words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return values need not be explained in the description. The description covers what the catalog contains, why it matters, when to call it, and the live/bounded nature of the data, which is complete for an optional-parameter, read-only list tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already fully documents the query and pollutantsOnly parameters. The tool description adds useful conceptual framing (unit disambiguation) but does not itself explain the parameters, so it stays at the baseline 3 without adding extra parameter-level meaning.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: listing the parameter catalog, with concrete fields (id, code, display name, unit, description). It also differentiates the tool from siblings by positioning it as the unit-disambiguation reference rather than a data-reading or location tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly tells the agent when to use it: to pick the exact parametersId for openaq_find_locations, openaq_get_readings, and openaq_get_measurements, and to interpret a reading's unit. It names the downstream tools and the decision this tool supports, making the invocation context unmistakable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
- Changed
openaq_find_locations14 fields changed- removed
Output schema / properties / locations / items / properties / coordinates / additionalPropertiesRemoved value: -false - added
Output schema / properties / locations / items / properties / coordinates / anyOfAdded value: +[ + { + "additionalProperties": false, + "properties": { + "latitude": { + "description": "Station latitude (decimal degrees)", + "type": "number" + }, + "longitude": { + "description": "Station longitude (decimal degrees)", + "type": "number" + } + }, + "required": [ + "latitude", + "longitude" + ], + "type": "object" + }, + { + "type": "null" + } +] - changed
Output schema / properties / locations / items / properties / coordinates / descriptionPrevious value: -"Station location"New value: +"Station location. Null when OpenAQ lists no latitude or no longitude." - removed
Output schema / properties / locations / items / properties / coordinates / propertiesRemoved value: -{ - "latitude": { - "description": "Station latitude (decimal degrees)", - "type": "number" - }, - "longitude": { - "description": "Station longitude (decimal degrees)", - "type": "number" - } -} - removed
Output schema / properties / locations / items / properties / coordinates / requiredRemoved value: -[ - "latitude", - "longitude" -] - removed
Output schema / properties / locations / items / properties / coordinates / typeRemoved value: -"object" - removed
Output schema / properties / locations / items / properties / country / additionalPropertiesRemoved value: -false - added
Output schema / properties / locations / items / properties / country / anyOfAdded value: +[ + { + "additionalProperties": false, + "properties": { + "code": { + "description": "OpenAQ country code: ISO 3166-1 alpha-2, or \"-99\" where OpenAQ lists none", + "type": "string" + }, + "name": { + "description": "Country name", + "type": "string" + } + }, + "required": [ + "code", + "name" + ], + "type": "object" + }, + { + "type": "null" + } +] - changed
Output schema / properties / locations / items / properties / country / descriptionPrevious value: -"Country the station is in"New value: +"Country the station is in. Null when OpenAQ lists none." - removed
Output schema / properties / locations / items / properties / country / propertiesRemoved value: -{ - "code": { - "description": "OpenAQ country code: ISO 3166-1 alpha-2, or \"-99\" where OpenAQ lists none", - "type": "string" - }, - "name": { - "description": "Country name", - "type": "string" - } -} - removed
Output schema / properties / locations / items / properties / country / requiredRemoved value: -[ - "code", - "name" -] - removed
Output schema / properties / locations / items / properties / country / typeRemoved value: -"object" - changed
Output schema / properties / locations / items / properties / provider / descriptionPrevious value: -"Data provider / network (e.g. \"AirNow\", \"OpenAQ LCS\")"New value: +"Data provider / network (e.g. \"AirNow\", \"OpenAQ LCS\"). Null when OpenAQ lists none." - changed
Output schema / properties / locations / items / properties / provider / typePrevious value: -"string"New value: +[ + "string", + "null" +]
- Changed
openaq_get_measurements17 fields changed- changed
Input schema / properties / aggregation / descriptionPrevious value: -"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."New value: +"Time bucketing. \"raw\" = every reported value (often hourly at source). \"hourly\"/\"daily\" = server-side rollups with a statistical summary per bucket; an hour is labeled by the time it ends, and a day is the station's local calendar day. Use \"daily\" for multi-month trends to keep the series small; \"raw\" for fine-grained recent analysis." - changed
Input schema / properties / datetimeFrom / descriptionPrevious value: -"Start of the range, inclusive. Date \"YYYY-MM-DD\" (opens at 00:00:00Z that day) or full UTC \"YYYY-MM-DDTHH:MM:SSZ\". Omit to get the most recent values."New value: +"Start of the range, inclusive. A date \"YYYY-MM-DD\" opens at local midnight of that day in the station's timezone (UTC midnight when OpenAQ lists none); a full UTC \"YYYY-MM-DDTHH:MM:SSZ\" is sent as is. Omit to start from the sensor's earliest data — the series runs oldest first, so on a long-running station an open start fills the row cap with its oldest values; set datetimeFrom to reach recent ones. effectiveRange echoes the instant sent." - changed
Input schema / properties / datetimeTo / descriptionPrevious value: -"End of the range, inclusive. Date \"YYYY-MM-DD\" covers that whole day (closes at 23:59:59Z) or full UTC \"YYYY-MM-DDTHH:MM:SSZ\". Must land after datetimeFrom — the two forms mix freely, so \"2026-06-25\" to \"2026-06-25\" is a valid one-day range. Omit for \"up to now\"."New value: +"End of the range, inclusive. A date \"YYYY-MM-DD\" covers that whole station-local day, closing at the next local midnight, so a DST day spans 23 or 25 hours; a full UTC \"YYYY-MM-DDTHH:MM:SSZ\" is sent as is. Must land after datetimeFrom — the two forms mix freely, so \"2026-06-25\" to \"2026-06-25\" is a valid one-day range. Omit for \"up to now\". effectiveRange echoes the instant sent." - changed
Output schema / anyOfPrevious value: -[ - { - "not": { - "required": [ - "error" - ] - }, - "required": [ - "location", - "parameter", - "sensorId", - "aggregation", - "series", - "rowCount", - "pulledCount", - "pullComplete", - "totalCount" - ] - }, - { - "required": [ - "error" - ] - } -]New value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "location", + "parameter", + "sensorId", + "aggregation", + "series", + "rowCount", + "pulledCount", + "pullComplete", + "totalCount", + "effectiveRange" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / effectiveRangeAdded value: +{ + "additionalProperties": false, + "description": "The range sent to OpenAQ as UTC instants — date-only bounds expanded to the station's local day.", + "properties": { + "datetimeFrom": { + "description": "Lower bound sent to OpenAQ, UTC. Null when datetimeFrom was omitted.", + "type": [ + "string", + "null" + ] + }, + "datetimeTo": { + "description": "Upper bound sent to OpenAQ, UTC. Null when datetimeTo was omitted.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "datetimeFrom", + "datetimeTo" + ], + "type": "object" +} - changed
Output schema / properties / error / properties / data / properties / reason / descriptionPrevious value: -"Machine-readable failure mode. Declared by this tool: `location_not_found`: The locationId does not exist. `parameter_not_at_location`: No sensor at the station measures parametersId (often the wrong unit variant was chosen). `no_data_for_range`: The sensor has no measurements in the requested date range. `invalid_date_range`: The range is empty — once both bounds are expanded to full UTC timestamps, datetimeTo does not land after datetimeFrom. `canvas_not_found`: The supplied canvas_id is unknown or has expired, so the series cannot be staged onto it. `upstream_error`: OpenAQ returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler."New value: +"Machine-readable failure mode. Declared by this tool: `location_not_found`: The locationId does not exist. `parameter_not_at_location`: No sensor at the station measures parametersId (often the wrong unit variant was chosen). `no_data_for_range`: The sensor has no measurements in the requested date range. `invalid_date_range`: The range is empty — once date-only bounds are expanded to the station's local day, datetimeTo does not land after datetimeFrom. `canvas_not_found`: The supplied canvas_id is unknown or has expired, so the series cannot be staged onto it. `upstream_error`: OpenAQ returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler." - added
Output schema / properties / gapCountAdded value: +{ + "description": "Missing intervals inside an hourly or daily series — a span between buckets that do not touch, or a bucket with a null value, merged where contiguous — counted over every pulled row, not only the preview. 0 when nothing is missing; absent for raw, whose rows follow no fixed cadence.", + "type": "number" +} - added
Output schema / properties / gapsAdded value: +{ + "description": "The first 20 missing intervals, oldest first. Omitted when gapCount is 0.", + "items": { + "additionalProperties": false, + "description": "One missing interval", + "properties": { + "datetimeFrom": { + "description": "Start of the missing interval, UTC", + "type": "string" + }, + "datetimeTo": { + "description": "End of the missing interval, UTC", + "type": "string" + } + }, + "required": [ + "datetimeFrom", + "datetimeTo" + ], + "type": "object" + }, + "type": "array" +} - added
Output schema / properties / location / properties / providerAdded value: +{ + "description": "Network that operates the station — cite it alongside OpenAQ. Null when OpenAQ lists none.", + "type": [ + "string", + "null" + ] +} - added
Output schema / properties / location / properties / providerIdAdded value: +{ + "description": "Provider id, usable as providersId in openaq_find_locations. Null when OpenAQ lists none.", + "type": [ + "number", + "null" + ] +} - added
Output schema / properties / location / properties / timezoneAdded value: +{ + "description": "IANA timezone of the station (e.g. \"America/Los_Angeles\"). Daily buckets and date-only bounds follow its calendar days. Null when OpenAQ lists none.", + "type": [ + "string", + "null" + ] +} - changed
Output schema / properties / location / requiredPrevious value: -[ - "id", - "name" -]New value: +[ + "id", + "name", + "provider", + "providerId", + "timezone" +] - changed
Output schema / properties / notice / descriptionPrevious value: -"What limited this response or where the rest of it lives — the row cap, a failed page, DataCanvas being unavailable, or the canvas table the series was staged on and the tools that read it."New value: +"What limited this response or where the rest of it lives — the row cap, a failed page, a station with no timezone, an edge bucket clipped by the range, missing intervals, DataCanvas being unavailable, or the canvas table the series was staged on and the tools that read it." - changed
Output schema / properties / pullComplete / descriptionPrevious value: -"True when the pager reached the end of the requested range, so pulledCount is the whole series for it. False when the 5000-row cap or a failed page stopped the pull early — the rows past that point are in neither this response nor the canvas table, and the notice says how to reach them."New value: +"True when pulledCount is the whole series for the requested range. False when the 5000-row cap or a failed page stopped the pull early — the rows past that point are in neither this response nor the canvas table, and the notice says how to reach them." - changed
Output schema / properties / pulledCount / descriptionPrevious value: -"Rows pulled from OpenAQ — the canvas table's row count when canvasId is present. Equals rowCount when the whole series fit inline; larger when series is a preview."New value: +"Rows pulled from OpenAQ, at most 5000 — the canvas table's row count when canvasId is present. Equals rowCount when the whole series fit inline; larger when series is a preview." - changed
Output schema / properties / series / descriptionPrevious value: -"The (possibly previewed) series, newest or oldest first per the API. Every row here is also rendered in the text output. When truncated, this is a preview of pulledCount rows — query canvasId for the rest."New value: +"The (possibly previewed) series in the order OpenAQ returns it (oldest first). An hourly/daily series either skips a missing bucket or returns it with a null value — gapCount and gaps report both. Every row here is also rendered in the text output. When truncated, this is a preview of pulledCount rows — query canvasId for the rest." - changed
Output schema / properties / series / items / properties / percentComplete / descriptionPrevious value: -"Coverage of the bucket (0–100); low values flag gappy data"New value: +"Coverage of the bucket as OpenAQ reports it — observed readings as a percentage of expected ones. Low values flag gappy data. Usually 0–100, but it exceeds 100 when a bucket holds more readings than expected, e.g. 200 on the hour a DST fall-back repeats"
- Changed
openaq_get_readings10 fields changed- removed
Output schema / properties / location / properties / coordinates / additionalPropertiesRemoved value: -false - added
Output schema / properties / location / properties / coordinates / anyOfAdded value: +[ + { + "additionalProperties": false, + "properties": { + "latitude": { + "description": "Station latitude (decimal degrees)", + "type": "number" + }, + "longitude": { + "description": "Station longitude (decimal degrees)", + "type": "number" + } + }, + "required": [ + "latitude", + "longitude" + ], + "type": "object" + }, + { + "type": "null" + } +] - changed
Output schema / properties / location / properties / coordinates / descriptionPrevious value: -"Station coordinates"New value: +"Station coordinates. Null when OpenAQ lists no latitude or no longitude." - removed
Output schema / properties / location / properties / coordinates / propertiesRemoved value: -{ - "latitude": { - "description": "Station latitude (decimal degrees)", - "type": "number" - }, - "longitude": { - "description": "Station longitude (decimal degrees)", - "type": "number" - } -} - removed
Output schema / properties / location / properties / coordinates / requiredRemoved value: -[ - "latitude", - "longitude" -] - removed
Output schema / properties / location / properties / coordinates / typeRemoved value: -"object" - added
Output schema / properties / location / properties / providerAdded value: +{ + "description": "Network that operates the station (e.g. \"AirNow\") — cite it alongside OpenAQ. Null when OpenAQ lists none.", + "type": [ + "string", + "null" + ] +} - added
Output schema / properties / location / properties / providerIdAdded value: +{ + "description": "Provider id, usable as providersId in openaq_find_locations. Null when OpenAQ lists none.", + "type": [ + "number", + "null" + ] +} - changed
Output schema / properties / location / requiredPrevious value: -[ - "id", - "name", - "coordinates", - "timezone", - "distanceMeters", - "datetimeLast" -]New value: +[ + "id", + "name", + "coordinates", + "provider", + "providerId", + "timezone", + "distanceMeters", + "datetimeLast" +] - changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance when the station resolved but returned no recent values."New value: +"Set when coordinate resolution compared a full 1,000-station page: more stations may match, so the station returned is the nearest of the first 1,000 OpenAQ lists, not necessarily the nearest overall."
- Changed
openaq_list_countries1 field changed- changed
Input schema / properties / query / descriptionPrevious value: -"Case-insensitive filter over the country catalog by code and name. A two-letter query is treated as an exact ISO 3166-1 alpha-2 code (e.g. \"US\" → United States); longer queries match as substrings (e.g. \"united\", \"germany\"). Omit to page through the whole catalog."New value: +"Case-insensitive filter over the country catalog by code and name. A two-letter query matches an exact ISO 3166-1 alpha-2 code first (e.g. \"US\" → United States) and falls back to substrings when no code matches; longer queries match as substrings (e.g. \"united\", \"germany\"). Omit to page through the whole catalog."
5 tool updates
- Changed
openaq_find_locations23 fields changed- changed
Input schema / properties / bbox / descriptionPrevious value: -"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)."New value: +"Bounding box as \"minLon,minLat,maxLon,maxLat\" (west,south,east,north), with minLon ≤ maxLon and minLat ≤ maxLat. Alternative to coordinates+radius for area sweeps. Results have no distance field (no center point)." - changed
Input schema / properties / iso / descriptionPrevious value: -"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."New value: +"Restrict to a country by OpenAQ country code: ISO 3166-1 alpha-2 (e.g. \"US\", \"IN\", \"DE\"; either case), or \"-99\" where OpenAQ lists a country with no ISO code. Take codes from openaq_list_countries. Combine with bbox/coordinates to scope, or use alone for a country-wide list." - removed
Input schema / properties / iso / maxLengthRemoved value: -2 - removed
Input schema / properties / iso / minLengthRemoved value: -2 - added
Input schema / properties / iso / patternAdded value: +"^(?:[A-Za-z]{2}|-99)$" - added
Input schema / properties / mobileAdded value: +{ + "description": "Mobility filter: true returns only mobile stations, false only fixed ones. Omit for both.", + "type": "boolean" +} - added
Input schema / properties / monitorAdded value: +{ + "description": "Station class filter: true returns only reference-grade monitors, false only low-cost sensors. Omit for both.", + "type": "boolean" +} - changed
Input schema / properties / page / descriptionPrevious value: -"Which page of results to return (1-based). Default 1. The only way past the 100-station cap: with limit 100, page 2 returns stations 101–200. Distance ordering applies within a page, not across pages, so paging is for iso/bbox sweeps — a near-me coordinates search should stay on page 1."New value: +"Which page of results to return (1-based). Default 1. The only way past the 100-station cap: with limit 100, page 2 returns stations 101–200. Distance ordering applies within a page, not across pages, so paging is for iso/bbox sweeps — a near-me coordinates search should stay on page 1. A page past the last one fails with page_exhausted." - added
Input schema / properties / parametersId / exclusiveMinimumAdded value: +0 - removed
Input schema / properties / parametersId / minimumRemoved value: --9007199254740991 - added
Input schema / properties / providersIdAdded value: +{ + "description": "Only return stations from this OpenAQ provider (data network) id — read it from a previous result's providerId (e.g. 119 = AirNow).", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "type": "integer" +} - removed
Input schema / properties / radius / defaultRemoved value: -12000 - changed
Input schema / properties / radius / descriptionPrevious value: -"Search radius in metres around coordinates (1–25000; the API hard-caps at 25000). Default 12000 (~12km). Only used with coordinates."New value: +"Search radius in metres around coordinates (1–25000; the API hard-caps at 25000). Default 12000 (~12km). Requires coordinates — a radius sent with only bbox or iso is rejected." - changed
Output schema / properties / error / properties / data / properties / reason / descriptionPrevious value: -"Machine-readable failure mode. Declared by this tool: `no_locations_found`: No monitoring stations match the given area or filters. `no_search_scope`: None of coordinates, bbox, or iso was provided. `upstream_error`: OpenAQ returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler."New value: +"Machine-readable failure mode. Declared by this tool: `no_locations_found`: No monitoring stations match the given area or filters. `page_exhausted`: A page past the first returned no stations — the results end before it. `no_search_scope`: None of coordinates, bbox, or iso was provided. `invalid_search_scope`: coordinates and bbox were both provided, or radius was provided without coordinates. `upstream_error`: OpenAQ returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler." - changed
Output schema / properties / error / properties / data / properties / reason / examplesPrevious value: -[ - "no_locations_found", - "no_search_scope", - "upstream_error", - "rate_limited", - "upstream_timeout", - "invalid_api_key" -]New value: +[ + "no_locations_found", + "page_exhausted", + "no_search_scope", + "invalid_search_scope", + "upstream_error", + "rate_limited", + "upstream_timeout", + "invalid_api_key" +] - changed
Output schema / properties / locations / descriptionPrevious value: -"Matching stations. Empty array means no monitoring coverage for the query — NOT clean air. Widen the radius, try openaq_list_countries, or use the modeled open-meteo air-quality tool."New value: +"Matching stations on this page, never empty: a query with no match fails with no_locations_found (no monitoring coverage, NOT clean air), and a page past the last with page_exhausted." - changed
Output schema / properties / locations / items / properties / country / properties / code / descriptionPrevious value: -"ISO 3166-1 alpha-2 country code"New value: +"OpenAQ country code: ISO 3166-1 alpha-2, or \"-99\" where OpenAQ lists none" - added
Output schema / properties / locations / items / properties / providerIdAdded value: +{ + "description": "OpenAQ provider id — pass as providersId to restrict a search to this network. Null when OpenAQ lists no provider.", + "type": [ + "number", + "null" + ] +} - changed
Output schema / properties / locations / items / requiredPrevious value: -[ - "id", - "name", - "locality", - "country", - "coordinates", - "distanceMeters", - "provider", - "isMonitor", - "isMobile", - "parameters", - "datetimeLast", - "datetimeFirst" -]New value: +[ + "id", + "name", + "locality", + "country", + "coordinates", + "distanceMeters", + "provider", + "providerId", + "isMonitor", + "isMobile", + "parameters", + "datetimeLast", + "datetimeFirst" +] - changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance when OpenAQ reports a lower-bound total without the result set hitting the limit."New value: +"Guidance on a full page: the next page to request, or how to narrow the search." - changed
Output schema / properties / totalCount / descriptionPrevious value: -"Total matching stations before the limit. A floor (not an exact count) when totalCountIsLowerBound is true."New value: +"Stations counted through this page: (page − 1) × limit plus the stations returned. Exact on a page that came back short of the limit (the last page); a floor when totalCountIsLowerBound is true." - changed
Output schema / properties / totalCountIsLowerBound / descriptionPrevious value: -"True when OpenAQ reported a lower bound (\">N\"): totalCount is a floor and more stations match than the count shown."New value: +"True when this page came back full: at least totalCount stations match, and the next page may hold more." - changed
Output schema / properties / truncated / descriptionPrevious value: -"True when the station list was capped at the limit."New value: +"True when this page came back full (the limit was reached), so the next page may hold more stations."
- Changed
openaq_get_measurements4 fields changed- added
Input schema / properties / locationId / exclusiveMinimumAdded value: +0 - removed
Input schema / properties / locationId / minimumRemoved value: --9007199254740991 - added
Input schema / properties / parametersId / exclusiveMinimumAdded value: +0 - removed
Input schema / properties / parametersId / minimumRemoved value: --9007199254740991
- Changed
openaq_get_readings4 fields changed- added
Input schema / properties / locationId / exclusiveMinimumAdded value: +0 - removed
Input schema / properties / locationId / minimumRemoved value: --9007199254740991 - added
Input schema / properties / parametersId / exclusiveMinimumAdded value: +0 - removed
Input schema / properties / parametersId / minimumRemoved value: --9007199254740991
- Changed
openaq_list_countries11 fields changed- added
Input schema / properties / limitAdded value: +{ + "default": 20, + "description": "Max countries to return (1–100). Default 20. Applied after query and parametersId, in OpenAQ catalog order.", + "maximum": 100, + "minimum": 1, + "type": "integer" +} - added
Input schema / properties / pageAdded value: +{ + "default": 1, + "description": "Which page of the filtered list to return (1-based). Default 1. With limit 20, page 2 returns countries 21–40. A page past the last one returns no countries and a notice naming the last page.", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" +} - added
Input schema / properties / parametersId / exclusiveMinimumAdded value: +0 - removed
Input schema / properties / parametersId / minimumRemoved value: --9007199254740991 - changed
Input schema / properties / query / descriptionPrevious value: -"Case-insensitive filter over the bounded country catalog (~153) by code and name. A two-letter query is treated as an exact ISO 3166-1 alpha-2 code (e.g. \"US\" → United States); longer queries match as substrings (e.g. \"united\", \"germany\"). Omit to list all."New value: +"Case-insensitive filter over the country catalog by code and name. A two-letter query is treated as an exact ISO 3166-1 alpha-2 code (e.g. \"US\" → United States); longer queries match as substrings (e.g. \"united\", \"germany\"). Omit to page through the whole catalog." - added
Output schema / properties / capAdded value: +{ + "description": "The limit that was applied.", + "type": "number" +} - changed
Output schema / properties / countries / items / properties / code / descriptionPrevious value: -"ISO 3166-1 alpha-2 code — pass as iso to openaq_find_locations"New value: +"OpenAQ country code: ISO 3166-1 alpha-2, or \"-99\" where OpenAQ has none — pass as iso to openaq_find_locations" - changed
Output schema / properties / notice / descriptionPrevious value: -"Guidance when the query matched nothing."New value: +"Guidance when the filters matched nothing, when more pages follow (the next page to request), or when the page is past the last one." - added
Output schema / properties / shownAdded value: +{ + "description": "Number of countries returned on this page.", + "type": "number" +} - changed
Output schema / properties / totalCount / descriptionPrevious value: -"Total countries matched after filtering."New value: +"Countries matched after query and parametersId, across every page." - added
Output schema / properties / truncatedAdded value: +{ + "description": "True when more matching countries follow on later pages.", + "type": "boolean" +}
- Changed
openaq_list_parameters1 field changed- changed
Input schema / properties / query / descriptionPrevious value: -"Case-insensitive filter over the bounded parameter catalog (~44) by code, display name, and description (e.g. \"pm\" for particulates, \"ozone\", \"co\"). Omit to list everything."New value: +"Case-insensitive filter over the bounded parameter catalog by code, display name, and description (e.g. \"pm\" for particulates, \"ozone\", \"co\"). Omit to list everything."
3 tool updates
- Changed
openaq_dataframe_describe1 field changed- changed
Input schema / properties / canvas_id / descriptionPrevious value: -"DataCanvas id returned by openaq_get_measurements when a series spilled."New value: +"DataCanvas id returned by openaq_get_measurements — minted when a series overflowed the inline preview, or the canvas_id you passed it."
- Changed
openaq_dataframe_query5 fields changed- changed
Input schema / properties / canvas_id / descriptionPrevious value: -"DataCanvas id returned by openaq_get_measurements when a series spilled."New value: +"DataCanvas id returned by openaq_get_measurements — minted when a series overflowed the inline preview, or the canvas_id you passed it." - added
Output schema / properties / noticeAdded value: +{ + "description": "How to reach the rest of the result when the row cap cut it short.", + "type": "string" +} - changed
Output schema / properties / rowCount / descriptionPrevious value: -"Full result count before the row cap."New value: +"Rows returned in this response, always equal to rows.length. It is the cap (200) when truncated is set, not the size of the full result." - changed
Output schema / properties / rows / descriptionPrevious value: -"Result rows (capped at the canvas row limit)."New value: +"Result rows, at most 200. Every row here is also rendered in the text output — the two surfaces carry the same set." - added
Output schema / properties / truncatedAdded value: +{ + "description": "True when the query matched more than 200 rows and the response was cut to the cap. Absent when the whole result fit. Page through the rest with ORDER BY plus LIMIT/OFFSET in your own SQL.", + "type": "boolean" +}
- Changed
openaq_get_measurements12 fields changed- changed
Input schema / properties / canvas_id / descriptionPrevious value: -"DataCanvas id from a prior openaq_get_measurements 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."New value: +"DataCanvas id from a prior openaq_get_measurements call, to put this series on the same canvas (e.g. to compare two stations' series side by side). Supplying it stages the series whatever its size. Reuse stages one table per sensor, so a second sensor adds a table while the same sensor overwrites its earlier series — the response says so when that happens. Omit to start fresh; the response returns a new canvas_id when the series overflows the inline preview." - changed
Input schema / properties / limit / descriptionPrevious value: -"Max rows per page from the API (1–1000). Default 1000. The tool pages internally up to the spill threshold."New value: +"Max rows per page from the API (1–1000). Default 1000. The tool pages internally up to the 5000-row pull ceiling." - changed
Output schema / anyOfPrevious value: -[ - { - "not": { - "required": [ - "error" - ] - }, - "required": [ - "location", - "parameter", - "sensorId", - "aggregation", - "series", - "rowCount", - "totalCount" - ] - }, - { - "required": [ - "error" - ] - } -]New value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "location", + "parameter", + "sensorId", + "aggregation", + "series", + "rowCount", + "pulledCount", + "pullComplete", + "totalCount" + ] + }, + { + "required": [ + "error" + ] + } +] - changed
Output schema / properties / canvasId / descriptionPrevious value: -"DataCanvas id holding the pulled series. Query with openaq_dataframe_query. The pull stops at 5000 rows, so this is the whole series only when totalCount is at or below that — read the notice, which says so when the cap or a failed page cut the pull short."New value: +"DataCanvas id holding the staged series — pulledCount rows of it. Call openaq_dataframe_describe on this id for the table's columns, then openaq_dataframe_query to run SQL. Present whenever staging succeeded, which includes a series that fit inline on a canvas_id you supplied." - changed
Output schema / properties / notice / descriptionPrevious value: -"What limited this response, when something did — the row cap, a failed page, or DataCanvas being unavailable — plus how to reach the rest."New value: +"What limited this response or where the rest of it lives — the row cap, a failed page, DataCanvas being unavailable, or the canvas table the series was staged on and the tools that read it." - added
Output schema / properties / pullCompleteAdded value: +{ + "description": "True when the pager reached the end of the requested range, so pulledCount is the whole series for it. False when the 5000-row cap or a failed page stopped the pull early — the rows past that point are in neither this response nor the canvas table, and the notice says how to reach them.", + "type": "boolean" +} - added
Output schema / properties / pulledCountAdded value: +{ + "description": "Rows pulled from OpenAQ — the canvas table's row count when canvasId is present. Equals rowCount when the whole series fit inline; larger when series is a preview.", + "type": "number" +} - changed
Output schema / properties / series / descriptionPrevious value: -"The (possibly previewed) series, newest or oldest first per the API. When truncated, this is a preview — query canvasId for the rows staged there."New value: +"The (possibly previewed) series, newest or oldest first per the API. Every row here is also rendered in the text output. When truncated, this is a preview of pulledCount rows — query canvasId for the rest." - changed
Output schema / properties / tableName / descriptionPrevious value: -"Canvas table name for the staged series (e.g. \"measurements_1701\"). Reference it in SQL."New value: +"Canvas table holding the staged series (e.g. \"measurements_1701\"). openaq_dataframe_describe lists its columns; reference this name in openaq_dataframe_query SQL. One table per sensor, so re-staging the same sensor on this canvas overwrites it." - changed
Output schema / properties / totalCount / descriptionPrevious value: -"Total rows in the full series."New value: +"Rows in the full series for this range. A floor rather than an exact count when totalCountIsLowerBound is set; never below pulledCount." - added
Output schema / properties / totalCountIsLowerBoundAdded value: +{ + "description": "Set when totalCount is only a floor: the pull stopped early and OpenAQ reported the range total as \">N\" instead of an exact number, so more rows exist than totalCount states. Absent when the count is exact.", + "type": "boolean" +} - changed
Output schema / properties / truncated / descriptionPrevious value: -"True when the series exceeded the inline limit, so series is a preview and the pulled rows were staged on canvasId. Absent/false when everything fit inline. It says nothing about whether the pull itself was complete — compare rowCount and totalCount, and read the notice."New value: +"True when the series exceeded the inline limit, so series is a preview of the pulled rows. Absent/false when every pulled row is inline. It describes the preview only — canvasId reports whether the rows were staged, and pullComplete whether the pull itself finished."
7 tool updates
- Changed
openaq_dataframe_describe1 field changed- added
Input schema / properties / canvas_id / patternAdded value: +"^[A-Za-z0-9_-]{10}$"
- Changed
openaq_dataframe_query1 field changed- added
Input schema / properties / canvas_id / patternAdded value: +"^[A-Za-z0-9_-]{10}$"
- Changed
openaq_find_locations6 fields changed- removed
Output schema / properties / locations / items / properties / distanceMeters / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / locations / items / properties / distanceMeters / typeAdded value: +[ + "number", + "null" +] - removed
Output schema / properties / locations / items / properties / locality / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / locations / items / properties / locality / typeAdded value: +[ + "string", + "null" +] - removed
Output schema / properties / locations / items / properties / parameters / items / properties / displayName / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / locations / items / properties / parameters / items / properties / displayName / typeAdded value: +[ + "string", + "null" +]
- Changed
openaq_get_measurements9 fields changed- changed
Input schema / properties / canvas_id / descriptionPrevious value: -"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."New value: +"DataCanvas id from a prior openaq_get_measurements 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." - added
Input schema / properties / canvas_id / patternAdded value: +"^[A-Za-z0-9_-]{10}$" - removed
Output schema / properties / parameter / properties / displayName / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / parameter / properties / displayName / typeAdded value: +[ + "string", + "null" +] - removed
Output schema / properties / series / items / properties / percentComplete / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / series / items / properties / percentComplete / typeAdded value: +[ + "number", + "null" +] - changed
Output schema / properties / series / items / properties / summary / anyOfPrevious value: -[ - { - "additionalProperties": false, - "properties": { - "avg": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "Mean reading in the bucket" - }, - "max": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "Maximum reading in the bucket" - }, - "median": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "Median reading in the bucket" - }, - "min": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "Minimum reading in the bucket" - }, - "sd": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "description": "Standard deviation — null when only one reading in the bucket" - } - }, - "required": [ - "min", - "median", - "max", - "avg", - "sd" - ], - "type": "object" - }, - { - "type": "null" - } -]New value: +[ + { + "additionalProperties": false, + "properties": { + "avg": { + "description": "Mean reading in the bucket", + "type": [ + "number", + "null" + ] + }, + "max": { + "description": "Maximum reading in the bucket", + "type": [ + "number", + "null" + ] + }, + "median": { + "description": "Median reading in the bucket", + "type": [ + "number", + "null" + ] + }, + "min": { + "description": "Minimum reading in the bucket", + "type": [ + "number", + "null" + ] + }, + "sd": { + "description": "Standard deviation — null when only one reading in the bucket", + "type": [ + "number", + "null" + ] + } + }, + "required": [ + "min", + "median", + "max", + "avg", + "sd" + ], + "type": "object" + }, + { + "type": "null" + } +] - removed
Output schema / properties / series / items / properties / value / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / series / items / properties / value / typeAdded value: +[ + "number", + "null" +]
- Changed
openaq_get_readings6 fields changed- removed
Output schema / properties / location / properties / distanceMeters / anyOfRemoved value: -[ - { - "type": "number" - }, - { - "type": "null" - } -] - added
Output schema / properties / location / properties / distanceMeters / typeAdded value: +[ + "number", + "null" +] - removed
Output schema / properties / location / properties / timezone / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / location / properties / timezone / typeAdded value: +[ + "string", + "null" +] - removed
Output schema / properties / readings / items / properties / parameter / properties / displayName / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / readings / items / properties / parameter / properties / displayName / typeAdded value: +[ + "string", + "null" +]
- Changed
openaq_list_countries4 fields changed- removed
Output schema / properties / countries / items / properties / datetimeFirst / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / countries / items / properties / datetimeFirst / typeAdded value: +[ + "string", + "null" +] - removed
Output schema / properties / countries / items / properties / datetimeLast / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / countries / items / properties / datetimeLast / typeAdded value: +[ + "string", + "null" +]
- Changed
openaq_list_parameters4 fields changed- removed
Output schema / properties / parameters / items / properties / description / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / parameters / items / properties / description / typeAdded value: +[ + "string", + "null" +] - removed
Output schema / properties / parameters / items / properties / displayName / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / parameters / items / properties / displayName / typeAdded value: +[ + "string", + "null" +]
7 tool updates
- Changed
openaq_dataframe_describe6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "tables" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `canvas_unavailable`: DataCanvas is not enabled (CANVAS_PROVIDER_TYPE is not duckdb). `canvas_not_found`: The canvas_id is unknown or its canvas has expired. Other values are possible when a failure originates below the handler.", + "examples": [ + "canvas_unavailable", + "canvas_not_found" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "tables" -]
- Changed
openaq_dataframe_query6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "rows", + "rowCount" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `canvas_unavailable`: DataCanvas is not enabled (CANVAS_PROVIDER_TYPE is not duckdb). `canvas_not_found`: The canvas_id is unknown or its canvas has expired. `missing_table`: The SQL references a table that is not staged on this canvas (dropped, expired, or misspelled). Other values are possible when a failure originates below the handler.", + "examples": [ + "canvas_unavailable", + "canvas_not_found", + "missing_table" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "rows", - "rowCount" -]
- Changed
openaq_find_locations6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "locations", + "totalCount" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `no_locations_found`: No monitoring stations match the given area or filters. `no_search_scope`: None of coordinates, bbox, or iso was provided. `upstream_error`: OpenAQ returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler.", + "examples": [ + "no_locations_found", + "no_search_scope", + "upstream_error", + "rate_limited", + "upstream_timeout", + "invalid_api_key" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "locations", - "totalCount" -]
- Changed
openaq_get_measurements6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "location", + "parameter", + "sensorId", + "aggregation", + "series", + "rowCount", + "totalCount" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `location_not_found`: The locationId does not exist. `parameter_not_at_location`: No sensor at the station measures parametersId (often the wrong unit variant was chosen). `no_data_for_range`: The sensor has no measurements in the requested date range. `invalid_date_range`: The range is empty — once both bounds are expanded to full UTC timestamps, datetimeTo does not land after datetimeFrom. `canvas_not_found`: The supplied canvas_id is unknown or has expired, so the series cannot be staged onto it. `upstream_error`: OpenAQ returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler.", + "examples": [ + "location_not_found", + "parameter_not_at_location", + "no_data_for_range", + "invalid_date_range", + "canvas_not_found", + "upstream_error", + "rate_limited", + "upstream_timeout", + "invalid_api_key" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "location", - "parameter", - "sensorId", - "aggregation", - "series", - "rowCount", - "totalCount" -]
- Changed
openaq_get_readings6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "location", + "readings" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `location_not_found`: The locationId does not exist (API returns {\"detail\":\"Location not found\"}). `parameter_not_at_location`: No sensor at the resolved station measures parametersId (often the wrong unit variant was chosen). `no_station_near_coordinates`: The 25km auto-resolution sweep found no station measuring the requested parametersId. `no_recent_values`: The station has the requested sensors but its latest feed carried no values for them. `invalid_location_scope`: Both locationId and coordinates were provided, or neither was. `missing_coordinates_parameter`: coordinates was provided without parametersId. `upstream_error`: OpenAQ returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler.", + "examples": [ + "location_not_found", + "parameter_not_at_location", + "no_station_near_coordinates", + "no_recent_values", + "invalid_location_scope", + "missing_coordinates_parameter", + "upstream_error", + "rate_limited", + "upstream_timeout", + "invalid_api_key" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "location", - "readings" -]
- Changed
openaq_list_countries6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "countries", + "totalCount" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `upstream_error`: OpenAQ /countries returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ /countries did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler.", + "examples": [ + "upstream_error", + "rate_limited", + "upstream_timeout", + "invalid_api_key" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "countries", - "totalCount" -]
- Changed
openaq_list_parameters6 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Input schema / additionalPropertiesAdded value: +false - changed
Output schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - added
Output schema / anyOfAdded value: +[ + { + "not": { + "required": [ + "error" + ] + }, + "required": [ + "parameters", + "totalCount" + ] + }, + { + "required": [ + "error" + ] + } +] - added
Output schema / properties / errorAdded value: +{ + "additionalProperties": {}, + "description": "Present when the call failed. Absent on success.", + "properties": { + "code": { + "description": "JSON-RPC error code for this failure.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "data": { + "additionalProperties": {}, + "properties": { + "reason": { + "description": "Machine-readable failure mode. Declared by this tool: `upstream_error`: OpenAQ /parameters returned 5xx or an unreadable body on every retry. `rate_limited`: OpenAQ returned 429 — the request budget for this key is exhausted. `upstream_timeout`: OpenAQ /parameters did not respond within the request timeout on every retry. `invalid_api_key`: OpenAQ returned 401 — the configured OPENAQ_API_KEY is missing, invalid, or revoked. Other values are possible when a failure originates below the handler.", + "examples": [ + "upstream_error", + "rate_limited", + "upstream_timeout", + "invalid_api_key" + ], + "type": "string" + }, + "recovery": { + "additionalProperties": {}, + "description": "Actionable next step for the caller.", + "properties": { + "hint": { + "type": "string" + } + }, + "required": [ + "hint" + ], + "type": "object" + }, + "retryable": { + "description": "Whether retrying may succeed.", + "type": "boolean" + } + }, + "type": "object" + }, + "message": { + "description": "Human-readable description of what went wrong.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" +} - removed
Output schema / requiredRemoved value: -[ - "parameters", - "totalCount" -]
1 tool update
- Changed
openaq_get_measurements11 fields changed- changed
Input schema / properties / datetimeFrom / descriptionPrevious value: -"Start of the range, inclusive. Date \"YYYY-MM-DD\" or full UTC \"YYYY-MM-DDTHH:MM:SSZ\". Omit to get the most recent values."New value: +"Start of the range, inclusive. Date \"YYYY-MM-DD\" (opens at 00:00:00Z that day) or full UTC \"YYYY-MM-DDTHH:MM:SSZ\". Omit to get the most recent values." - changed
Input schema / properties / datetimeTo / descriptionPrevious value: -"End of the range, inclusive. Must be on or after datetimeFrom. Omit for \"up to now\"."New value: +"End of the range, inclusive. Date \"YYYY-MM-DD\" covers that whole day (closes at 23:59:59Z) or full UTC \"YYYY-MM-DDTHH:MM:SSZ\". Must land after datetimeFrom — the two forms mix freely, so \"2026-06-25\" to \"2026-06-25\" is a valid one-day range. Omit for \"up to now\"." - changed
Output schema / properties / canvasId / descriptionPrevious value: -"DataCanvas id holding the full series. Query with openaq_dataframe_query."New value: +"DataCanvas id holding the pulled series. Query with openaq_dataframe_query. The pull stops at 5000 rows, so this is the whole series only when totalCount is at or below that — read the notice, which says so when the cap or a failed page cut the pull short." - changed
Output schema / properties / notice / descriptionPrevious value: -"Degraded-mode hint when the series was truncated but DataCanvas is unavailable."New value: +"What limited this response, when something did — the row cap, a failed page, or DataCanvas being unavailable — plus how to reach the rest." - changed
Output schema / properties / series / descriptionPrevious value: -"The (possibly previewed) series, newest or oldest first per the API. When truncated, this is a preview — query canvasId for the full set."New value: +"The (possibly previewed) series, newest or oldest first per the API. When truncated, this is a preview — query canvasId for the rows staged there." - changed
Output schema / properties / series / items / properties / summary / descriptionPrevious value: -"Per-bucket statistics — present for hourly/daily, null for raw"New value: +"Per-bucket statistics — present for hourly/daily, null for raw. Every field is null in a gap bucket" - added
Output schema / properties / series / items / properties / value / anyOfAdded value: +[ + { + "type": "number" + }, + { + "type": "null" + } +] - changed
Output schema / properties / series / items / properties / value / descriptionPrevious value: -"Value for the bucket (the measurement for raw; the bucket aggregate for hourly/daily)"New value: +"Value for the bucket (the measurement for raw; the bucket aggregate for hourly/daily). Null for a gap bucket the sensor reported nothing into — the bucket is kept so the series stays evenly spaced on the time axis" - removed
Output schema / properties / series / items / properties / value / typeRemoved value: -"number" - changed
Output schema / properties / tableName / descriptionPrevious value: -"Canvas table name for the full series (e.g. \"measurements_1701\"). Reference it in SQL."New value: +"Canvas table name for the staged series (e.g. \"measurements_1701\"). Reference it in SQL." - changed
Output schema / properties / truncated / descriptionPrevious value: -"True when the series exceeded the inline limit and the full set was staged on canvasId. Absent/false when everything fit inline."New value: +"True when the series exceeded the inline limit, so series is a preview and the pulled rows were staged on canvasId. Absent/false when everything fit inline. It says nothing about whether the pull itself was complete — compare rowCount and totalCount, and read the notice."
2 tool updates
- Changed
openaq_find_locations1 field changed- added
Input schema / properties / pageAdded value: +{ + "default": 1, + "description": "Which page of results to return (1-based). Default 1. The only way past the 100-station cap: with limit 100, page 2 returns stations 101–200. Distance ordering applies within a page, not across pages, so paging is for iso/bbox sweeps — a near-me coordinates search should stay on page 1.", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" +}
- Changed
openaq_list_countries1 field changed- added
Input schema / properties / parametersIdAdded value: +{ + "description": "Only return countries that measure this parameter id somewhere (e.g. 2 = PM2.5 µg/m³) — the one-call answer to \"which countries have NO2 monitoring?\". Get ids from openaq_list_parameters; the same pollutant has several ids for different units. Composes with query.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" +}
3 tool updates
- Changed
openaq_find_locations3 fields changed- added
Output schema / properties / noticeAdded value: +{ + "description": "Guidance when OpenAQ reports a lower-bound total without the result set hitting the limit.", + "type": "string" +} - changed
Output schema / properties / totalCount / descriptionPrevious value: -"Total matching stations before the limit."New value: +"Total matching stations before the limit. A floor (not an exact count) when totalCountIsLowerBound is true." - added
Output schema / properties / totalCountIsLowerBoundAdded value: +{ + "description": "True when OpenAQ reported a lower bound (\">N\"): totalCount is a floor and more stations match than the count shown.", + "type": "boolean" +}
- Changed
openaq_list_countries1 field changed- changed
Input schema / properties / query / descriptionPrevious value: -"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."New value: +"Case-insensitive filter over the bounded country catalog (~153) by code and name. A two-letter query is treated as an exact ISO 3166-1 alpha-2 code (e.g. \"US\" → United States); longer queries match as substrings (e.g. \"united\", \"germany\"). Omit to list all."
- Changed
openaq_list_parameters1 field changed- changed
Input schema / properties / query / descriptionPrevious value: -"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."New value: +"Case-insensitive filter over the bounded parameter catalog (~44) by code, display name, and description (e.g. \"pm\" for particulates, \"ozone\", \"co\"). Omit to list everything."
7 tool updates
- First observed
openaq_dataframe_describe - First observed
openaq_dataframe_query - First observed
openaq_find_locations - First observed
openaq_get_measurements - First observed
openaq_get_readings - First observed
openaq_list_countries - First observed
openaq_list_parameters
Related MCP Connectors
OpenAQ MCP — global air-quality measurements via the OpenAQ v3 API.
Search NOAA climate stations and datasets, fetch historical weather observations.
Search NOAA CDO stations and datasets, fetch historical weather observations.
Access UK air quality data, monitoring sites, and hourly pollutant measurements across regions
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables querying global air-quality data from the OpenAQ API, including nearest-station readings, station discovery, latest pollutant values, and historical time series for sensors.3 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables querying EPA's Air Quality System for regulatory-grade ambient air data, including parameters, sample measurements, annual summaries, and monitor metadata.213 npmMIT
- AlicenseNot gradedqualityBmaintenanceAccess air quality data from Open-Meteo API, free and without authentication.131 npmMIT
- AlicenseAqualityBmaintenanceEnables querying Polish air-quality data from GIOŚ stations by providing tools to list stations, get sensor readings, and retrieve the composite air-quality index.4MIT
Glama MCP Gateway
Add one secure layer between your agents and this server.