Skip to main content
Glama

pollenbrief-mcp

A small MCP server and CLI that turns a Google Pollen API forecast into an agent-ready daily brief with alert thresholds you set (for example GRASS>=4), per day and per pollen type or plant, as compact JSON and Markdown.

The problem

Allergy-sensitive people and outdoor operations teams (landscaping, field crews, events) need to know which of the next few days cross their pollen threshold at a given place. The Google Pollen API returns a detailed forecast per day, pollen type and plant on the Universal Pollen Index (UPI, 0 to 5), but an agent still has to geocode the place, page through the response and apply thresholds itself. This lab does that in one tool call.

Related MCP server: Atmospore

Quickstart

bun install --frozen-lockfile

# Keyless demo: synthetic data in the documented response shape (fictional address, invented values)
bun run src/cli.ts brief --address "100 Example Ave, Sampletown" --profile grass-allergy --fixtures
bun run src/cli.ts brief --at 47.25,8.5 --days 3 --alert "GRASS>=3" --format json --fixtures

# Live: put GOOGLE_MAPS_API_KEY in .env (see .env.example), then
bun run src/cli.ts brief --address "<your address>" --alert "GRASS>=4" --alert "BIRCH>=High: keep windows closed"
bun run src/cli.ts brief --at 40.71,-74.01 --profile outdoor-crew --exit-on-alert   # exit 3 when a rule fires

# MCP server (stdio)
bun run src/cli.ts serve            # live
bun run src/cli.ts serve --fixtures # keyless demo

Quote rules in the shell: an unquoted GRASS>=4 is a redirect to a file named =4.

MCP client config (Claude Code .mcp.json, Cursor mcp.json):

{
  "mcpServers": {
    "pollenbrief": {
      "command": "bun",
      "args": ["run", "/path/to/pollenbrief-mcp/src/cli.ts", "serve"],
      "env": { "GOOGLE_MAPS_API_KEY": "${GOOGLE_MAPS_API_KEY}", "POLLENBRIEF_BUDGET": "20" }
    }
  }
}

The key's Cloud project must have the Pollen API (pollen.googleapis.com) enabled, and the Geocoding API for address lookups. Restrict the key to those APIs.

Tools

Tool

What it does

Google calls

pollen_brief

address or lat+lng, days 1-5 (default 5), rules[] and/or a bundled profile, language_code, plants (indexed/all/none), advice, format (json/markdown/both), phrase

1 Geocoding v4 call for an address, plus 1 forecast:lookup call per page

pollen_rules

Rule grammar, valid targets, UPI levels, bundled profiles

none

session_status

Budget used/remaining, refused calls

none

CLI: pollenbrief brief | profiles | serve. Exit codes: 0 ok, 2 error, 3 alert (with --exit-on-alert), 64 usage.

Rules and profiles

TARGET>=LEVEL[: action]. TARGET is GRASS, TREE, WEED, a plant code from the API's Plant enum (ALDER, ASH, BIRCH, COTTONWOOD, ELM, MAPLE, OLIVE, JUNIPER, OAK, PINE, CYPRESS_PINE, HAZEL, GRAMINALES, RAGWEED, MUGWORT), or ANY_TYPE, ANY_PLANT, ANY. LEVEL is 0-5 or a UPI category name (None, Very Low, Low, Moderate, High, Very High).

A rule alerts on a day when that entry's UPI value is at least LEVEL, and puts the day on watch when the value is exactly one below. Entries the API returns without indexInfo (out of season or a low count) never fire; the brief adds a note when a rule cannot fire at all in the forecast.

Bundled profiles in profiles/: grass-allergy, tree-allergy, outdoor-crew, ragweed-season. The CLI also accepts a path to your own profile JSON; the MCP tool accepts bundled names only.

Output

JSON (brief): location, rules, summary (status alert/watch/clear, alertDays, watchDays, firstAlert, peak, text, phrasedBy), daily[] (types, plants, alerts, watch, advice), notes, attribution, source. Markdown: a status line, a day table (Grass / Tree / Weed / Status / Alerts), a plants table, alerts grouped by day with your action text and the API's health recommendations, and the attribution line. A full sample is in reports/pollenbrief-mcp.md.

phrase: true (CLI --phrase) asks Gemini to reword summary.text. Gemini receives only dates, codes, UPI values, categories and rule ids, never the API's free text or your action notes. A reply containing any number that is not in that input is rejected and the template wording stays. Without GEMINI_API_KEY, or on HTTP 402, the template is used and phrasedBy says why.

Measured

Keyless run of the real MCP server (in-memory MCP client) over fixtures/synthetic/: invented values in the response shape documented in Google's API reference. Byte counts depend on the invented values, so read them as illustrative. "MCP = service" checks that the tool's JSON and Markdown equal the service output for the same input. Produced by bun run report; full output in reports/pollenbrief-mcp.md and reports/pollenbrief-mcp.json.

Scenario

Input

Days

Upstream calls

Pollen response bytes (synthetic)

Brief JSON bytes

Markdown bytes

Alert days

Watch days

Status

MCP = service

levels only

address, no rules

5

2

11782

2679

1235

0

0

clear

true

grass-allergy

address, profile

5

2

11782

4861

2764

2

2

alert

true

tree-allergy

address, profile

5

2

11782

5032

2995

3

1

alert

true

outdoor-crew

address, profile

5

2

11782

6020

3530

4

1

alert

true

ragweed-season

address, profile (weeds out of season)

5

2

11782

3379

1851

0

0

clear

true

calm point

lat/lng, 3 days, GRASS>=3

3

1

4628

1569

974

0

1

watch

true

  • Pagination: pageSize=2 over 5 days took 3 pages / 4 upstream calls (geocode included) and produced the same days as one page: true.

  • plantsDescription: the brief asks for false by default; the same synthetic forecast with descriptions is 25437 bytes vs 11782 (invented description text, so the ratio is illustrative).

  • 403 SERVICE_DISABLED (synthetic copy of the error shape): tool error google_PERMISSION_DENIED, reason SERVICE_DISABLED.

  • Budget 2, two address briefs (2 calls each): ok, budget_exceeded.

  • Gemini mocked 402: template (gemini gemini-3.1-flash-lite skipped: HTTP 402 RESOURCE_EXHAUSTED).

  • Gemini mocked reply with invented numbers: template (gemini gemini-3.1-flash-lite skipped: rejected: numbers not in the input (7, 10)).

  • ragweed-season note: "WEED has no index in this forecast (out of season or a low count), so WEED>=3 cannot fire".

Live status (2026-10-21)

Calls made with our Maps Platform key in project dev-test-461117 while building this lab:

Call

Result

Geocoding API v4 geocode/address (public address of Google's HQ)

HTTP 200; tests/live passes

Pollen API forecast:lookup

HTTP 403 PERMISSION_DENIED, reason SERVICE_DISABLED: the API is not enabled in that project, and we did not enable it. tests/live prints a skipped: line

Gemini generateContent (gemini-3.1-flash-lite)

HTTP 402 RESOURCE_EXHAUSTED (no prepaid credit). The model id is listed by the Gemini models endpoint for the key

So no real Pollen API response has been observed by this lab, and no Gemini-written brief has been produced. The Pollen path is tested only against synthetic responses built from the reference. When the API is enabled, bun run record saves one real response to the gitignored .recorded-local/, and tests/live then checks it against src/schema.ts.

What we checked in Google's docs

Checked on 2026-10-21 (the policies page showed "Last updated 2026-10-20 UTC").

Item

What the docs say

Source

Endpoint

GET https://pollen.googleapis.com/v1/forecast:lookup, empty body

forecast.lookup reference

Parameters

location.latitude, location.longitude, days (1-5, required), pageSize (default and max 5), pageToken, languageCode (BCP-47, default en), plantsDescription (default true)

same

Response

regionCode, dailyInfo[] (date, pollenTypeInfo[] up to 3, plantInfo[] up to 15), nextPageToken

same

Index

indexInfo: code (UPI), displayName, category, indexDescription, color (RGBA 0-1), value 0-5

same

UPI categories

0 None, 1 Very Low, 2 Low, 3 Moderate, 4 High, 5 Very High

Pollen index

Omitted fields

indexInfo omitted when out of season with a low count; plantDescription omitted when plantsDescription=false; color channels optional

Forecast guide

Pagination

pageSize + pageToken examples

same

Geocoding v4

geocode.googleapis.com/v4/geocode/address/{address} with X-Goog-FieldMask

Geocoding v4 overview

"Agents lack a small MCP for this"

Not verified: market claim from the idea, not measured

none

The forecast guide's examples pass the key as a key= query parameter. This lab sends it in the X-Goog-Api-Key header instead; the Pollen API accepted that header far enough to answer SERVICE_DISABLED for our project, but we have not seen a successful response with it.

Attribution and usage requirements

From the Pollen API policies:

  • Attribution. Data shown as is, without modifications, uses the Google Maps logo or text attribution. Content that aggregates or interprets the data (a custom score, thresholds, statuses) is attributed with "Includes data from Google Maps". The page also lists the older string "Source: Includes pollen data from Google" and says Google will publish timelines for moving existing "Google" attributions to "Google Maps". This brief interprets the data, so every JSON brief carries "attribution": "Includes data from Google Maps" and every Markdown brief ends with that line. Keep it next to the numbers wherever you show them. The text attribution rules also cover font, size, color and placement, and ask for translate="no" in HTML.

  • No caching. Content pre-fetching, caching or storage is generally prohibited (place IDs excepted). This server keeps nothing between calls, and the repo commits no Google responses.

  • Maps. If you put Pollen results on a map, it must be a Google Map.

  • Terms. Your app needs public Terms of Use and a Privacy Policy that meet Google's requirements; use is governed by your agreement with Google, and EEA billing addresses get different terms.

Limits

  • Pollen responses unverified live. See Live status: the Pollen API is not enabled in our project. Synthetic fixtures follow the reference field by field (src/types.ts, strict check in src/schema.ts), but real responses may carry fields or value formats we have not seen.

  • Not medical advice. The brief repeats the API's health recommendations and your own action text; it adds no advice of its own. The UPI is Google's index, not a pollen count.

  • Thresholds are integer UPI levels. No per-hour data (the API gives one value per day) and no >/<= operators.

  • Budget scope. One upstream call budget per process / MCP connection (default 20), not per key or per day. Use Cloud quotas and budget alerts for hard limits. No prices are quoted here.

  • Keyless mode answers only the demo inputs in fixtures/synthetic/INDEX.json; anything else returns google_NO_FIXTURE. Fixture output is never a real forecast and says so in the Markdown footer.

  • Gemini guard is numeric only. It rejects invented numbers, not a wrong pairing of real ones.

  • Not included: any Pollen API endpoint other than forecast:lookup, multi-location batches, scheduling or notifications (use --exit-on-alert from cron or CI), and storage of past briefs.

Development

bun run check               # install, skills layout, strict YAML, biome, tsc, unit + e2e tests, live tests (skip
                            # cleanly), report, bun audit, actionlint (if installed), gitleaks
bun test tests/unit tests/e2e
bun test tests/live         # needs keys; prints "skipped: ..." otherwise (POLLENBRIEF_SKIP_LIVE=1 forces skip)
bun run fixtures:synthetic  # regenerate fixtures/synthetic from src/synthetic.ts
bun run record              # live: save real responses to gitignored .recorded-local/ for the shape check

Not affiliated

Not affiliated with, endorsed by, or sponsored by Google LLC. Google Maps Platform, Pollen API and Gemini are trademarks of Google LLC.


Built by SkilledDragon LLC. Questions: hello@skilleddragon.com. Apache-2.0 (see LICENSE and NOTICE).

Related MCP Connectors

Related MCP Servers