Air Quality Comparison MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Air Quality Comparison MCP ServerCompare air quality between Curitiba and São Paulo"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Air Quality Comparison MCP Server
A NestJS Model Context Protocol server, compatible with the
Athena AI Agent SDK (OpenAI Apps / Skybridge widget contract). It exposes a public /mcp
endpoint over Streamable HTTP, pulls real air quality data from the free
Open-Meteo APIs (no API key), and renders an interactive comparison
dashboard widget inside Athena.
Ask: “Compare air quality between Curitiba and São Paulo” → the agent calls
compare_air_quality→ Athena renders the dashboard with live data → you switch pollutant focus and sorting right in the widget.
What it exposes
Tool
compare_air_quality(cityA, cityB)— geocodes both cities, fetches current hourly pollutant readings, computes which city has cleaner air, and returns structured data for the widget.Resource
ui://widget/air-quality.html— the Skybridge widget (mimeType: text/html+skybridge).Endpoint
POST/GET/DELETE/OPTIONS /mcp— Streamable HTTP transport with permissive CORS.Health
GET /→Air Quality MCP Server is running. MCP endpoint: /mcp
Data source
Geocoding:
https://geocoding-api.open-meteo.com/v1/searchAir Quality:
https://air-quality-api.open-meteo.com/v1/air-quality
No keys, no accounts. All values are fetched live — nothing is hardcoded.
Related MCP server: EPA Air Quality System (AQS) MCP Server
Project structure
src/
main.ts # Nest bootstrap
app.module.ts # root module
config/
app.config.ts # port + CORS options
common/
errors/
domain.errors.ts # CityNotFoundError, UpstreamError
air-quality/ # feature module
air-quality.module.ts # provides + exports AirQualityService
air-quality.service.ts # orchestrator (geocode → fetch → compare)
air-quality.constants.ts # endpoints, hourly fields, source metadata
clients/
open-meteo.client.ts # HTTP client (network only)
domain/
comparator.ts # pure "which city is cleaner" logic
pollutant-extractor.ts # pure parsing of hourly responses
interfaces/
air-quality.types.ts # shared interfaces / types
mcp/ # transport module
mcp.module.ts # imports AirQualityModule, wires controller
mcp.controller.ts # /mcp Streamable HTTP + GET / health + CORS
air-quality-mcp.server.ts # createAirQualityMcpServer(): tool + widget resource
test/
unit/ # unit tests (kept out of src)
comparator.spec.ts
pollutant-extractor.spec.ts
open-meteo.client.spec.ts
air-quality.service.spec.ts
public/
air-quality-widget.html # self-contained Skybridge widgetLayered / feature-based layout. Cross-cutting code lives in common/ and config/; each
feature is a self-contained Nest module (air-quality/, mcp/) that exposes a narrow surface.
Inside the feature, concerns are isolated: networking (clients/open-meteo.client), response
parsing (domain/pollutant-extractor) and business logic (domain/comparator) are pure,
single-responsibility units; the service only orchestrates them. Tests live in test/, separate
from source, so dist/ ships no spec files.
Setup
Requires Node 18+ (uses native fetch).
# 1. install dependencies
pnpm install
# 2. run locally (dev, with watch)
pnpm run start:dev
# or production build
pnpm run build && pnpm run start:prodServer listens on http://localhost:3000 (override with PORT).
Quick sanity check:
curl http://localhost:3000/
# -> Air Quality MCP Server is running. MCP endpoint: /mcpTests
Unit tests (Jest + ts-jest) cover the pure logic and the orchestrator (client mocked, no network):
pnpm test # run once
pnpm run test:watch
pnpm run test:cov # with coverageCovered: pollutant extraction (most-recent / null fallback), the comparison rule (PM2.5 → European AQI fallback, ties, missing data), the HTTP client (mapping, not-found, upstream errors), and the service (full payload assembly + graceful error payload).
End-to-end smoke test
test/e2e/smoke.sh builds the server, boots it, and drives a full Streamable HTTP MCP session
against the real Open-Meteo APIs — asserting the health text, CORS headers, initialize
session id, the widget resource (text/html+skybridge + widgetPrefersBorder), the tool _meta
(openai/outputTemplate, invoking/invoked), the full structuredContent shape with live numbers,
and the graceful error path for an unknown city.
pnpm run test:e2e
# E2E result: 12 passed, 0 failedExpose a public URL
Athena needs a publicly reachable HTTPS MCP URL.
Option A — ngrok (fastest for the demo)
ngrok http 3000Copy the forwarded HTTPS host. Your MCP URL is:
https://<your-ngrok-subdomain>.ngrok-free.app/mcpOption B — deploy
Deploy to any Node host (Render, Railway, Fly.io, a VM, etc.). Build with pnpm run build,
start with pnpm run start:prod, and make sure the platform port maps to the app (PORT env).
Your MCP URL is always:
https://your-domain.com/mcpAthena agent setup
Create a new Athena agent.
Add the MCP server URL:
https://your-domain.com/mcp(or the ngrok URL above).Paste the agent prompt below.
Test with:
Compare air quality between Curitiba and São Paulo
Athena agent prompt
You are AirCompare, an air quality comparison assistant.
Your job is to compare air quality between two cities using real public data through the MCP tool.
When the user asks to compare cities, call compare_air_quality with cityA and cityB.
Always render the interactive air quality dashboard widget.
Briefly explain:
- which city currently has better air quality
- the main reason
- the key pollutant values
Use PM2.5 as the primary comparison metric when available.
If PM2.5 is unavailable, use European AQI.
Encourage the user to interact with the dashboard by switching pollutant focus and changing the sort mode.
Keep answers concise and practical.The widget
public/air-quality-widget.html is a single self-contained file (no build step). It:
Reads initial data from
window.openai.toolOutput.Re-renders on
openai:set_globals(andopenai:tool_output) when Athena pushes new output.Shows two city cards: name + country, PM2.5, PM10, NO₂, O₃, European AQI, US AQI, timestamp, and source attribution.
Highlights the city with cleaner air.
Degrades gracefully: friendly empty state, error state, and defensive null handling so missing values never crash it.
Interactions
Pollutant focus tabs — PM2.5 / PM10 / NO₂ / O₃. Changes the headline value, color band, and comparison bars in both cards.
Sort toggle — Best air first / Worst air first. Reorders the two cards by the focused pollutant.
Swap — locally flips the displayed city order.
Refresh — calls
window.openai.callTool("compare_air_quality", { cityA, cityB })when the Athena bridge is present; otherwise just re-renders local state.
All interactions work standalone (outside Athena) using local state.
Tool output shape
structuredContent returned by compare_air_quality:
{
"subject": "Air quality comparison dashboard",
"cityA": {
"inputName": "Curitiba",
"name": "Curitiba",
"country": "Brazil",
"latitude": -25.42,
"longitude": -49.27,
"timezone": "America/Sao_Paulo",
"timestamp": "2026-06-19T15:00",
"pollutants": {
"pm2_5": 7.3, "pm10": 12.1, "nitrogen_dioxide": 4.2, "ozone": 60.0,
"carbon_monoxide": 120.0, "sulphur_dioxide": 1.1,
"european_aqi": 22, "us_aqi": 30
}
},
"cityB": { "...": "same shape" },
"comparison": {
"betterCity": "Curitiba",
"worseCity": "São Paulo",
"reason": "Curitiba has cleaner air with a lower PM2.5 (7.3 µg/m³ vs 20.4 µg/m³).",
"primaryMetric": "pm2_5",
"primaryMetricLabel": "PM2.5"
},
"source": {
"name": "Open-Meteo Air Quality API",
"url": "https://open-meteo.com/en/docs/air-quality-api",
"retrievedAt": "2026-06-19T15:36:00.000Z"
}
}Comparison rule: lower is better. PM2.5 is the primary metric; if PM2.5 is missing for either
city or tied, it falls back to European AQI. If a city can't be resolved, the payload includes an
error string (and the widget shows a friendly error state).
Demo checklist
Ask Athena: Compare air quality between Curitiba and São Paulo
Confirm the agent retrieves real data (values change over time)
Confirm the widget renders inside Athena
Click the pollutant tabs (PM2.5 / PM10 / NO₂ / O₃)
Toggle Best air first / Worst air first
Show the source attribution
Record a ≤ 1 minute video
Local MCP smoke test (no Athena)
pnpm run start:dev # in one terminal
# initialize → capture the Mcp-Session-Id header, then call the tool
curl -s -D - -o /dev/null -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'Use the returned mcp-session-id on subsequent notifications/initialized and tools/call
requests.
Notes
This SDK version (
@modelcontextprotocol/sdk@^1.29) ships the Streamable HTTP transport at@modelcontextprotocol/sdk/server/streamableHttp.js(camelCase). The MCP server lives at@modelcontextprotocol/sdk/server/mcp.js.dist/is a build artifact and is git-ignored — generate it withpnpm run build.
This server cannot be deployed
Maintenance
Related MCP Connectors
Ask data questions in natural language. Get SQL, insights, and charts from your databases.
Your Databricks Lakehouse in natural language: run SQL on your SQL warehouses, track long-running qu
Query BigQuery, Snowflake, Redshift & Azure Synapse with natural language
Ask business questions in plain English. Get instant answers from your database, no SQL needed.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables interaction with the World Air Quality Index to fetch real-time air quality data for cities and coordinates worldwide via Model Context Protocol (MCP).1MIT
- AlicenseAqualityCmaintenanceProvides access to EPA's Air Quality System API with 31 tools for querying air quality data, monitoring sites, and pollution measurements across the United States through natural language.31MIT
- FlicenseNot gradedqualityNot gradedmaintenanceProvides real-time air quality monitoring and historical data analysis for InBiot MICA sensors with integrated WELL Building Standard compliance checks. It allows users to compare indoor conditions with outdoor weather and receive actionable health recommendations based on global air quality standards.-
- AlicenseNot gradedqualityNot gradedmaintenanceProvides natural language access to environmental data including air quality measurements, greenhouse gas emissions, and facility records. It enables users to perform geographic searches, trend analysis, and proximity-based queries using data from sources like OpenAQ and Climate TRACE.MIT