Files
ag-bids-mcp/CHANGELOG.md
T
justin fb50c103d3
CI / test (push) Successful in 17s
CI / build-push (push) Successful in 6s
Location params on best_local_bid / latest_prices (zip / GPS + radius)
Thread zip/lat/lng/radius_miles through the client and both tools; friendly
guard for the zip-XOR-gps rule. Formatters surface distance, the searched
center, and the nearest-source hint when nothing is in range.

- client: best()/latest() take zip/lat/lng/radius_miles
- server: location params + docstrings (note Ohio-concentrated coverage)
- format: distance column + center/nearest rendering
- README + CHANGELOG + advisor prompt library updated
- tests: location formatting cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:51:54 -04:00

267 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
Notes for clients/agents that consume the ag-bids MCP tools and the underlying
`ag-monitor` `/api/data/*` HTTP API. Newest first.
## 2026-05-31 — Location-aware bid lookup (zip / GPS + radius)
`best_local_bid` and `latest_prices` can now be scoped to a farmer's location, so
"where should I haul today" means *near me*, not "across every elevator we
scrape." Filtering is offline (vendored US Census ZIP centroids — no geocoding
API).
### Changed MCP tools
- **`best_local_bid(commodity, zip?, lat?, lng?, radius_miles?)`** — pass a
**`zip`** OR **`lat`+`lng`** (not both) with optional **`radius_miles`**
(default **50**) to restrict to nearby elevators; the result shows the
**distance** to the winner. With no location it behaves as before (all
sources). If nothing is in range it returns no winner **and reports the
nearest elevator + its distance**, so a coverage gap reads as a gap, not an
error.
- **`latest_prices(..., zip?, lat?, lng?, radius_miles?)`** — same location
filter; rows are limited to the radius, annotated with **`distance_miles`**,
and sorted **nearest-first** (adds a Distance column).
GPS input is reverse-labeled to its nearest ZIP for display; only `zip → coord`
is used for the actual distance math.
> **Coverage note:** scraped **cash-bid** sources are currently concentrated in
> **Ohio**. A zip/GPS far from Ohio (e.g. Iowa) will correctly return "nothing in
> range" with the nearest-source hint until more regions are added. (Reference
> data — `price_trend`, `input_cost_trend` — is already national.)
### API
- `GET /api/data/best?commodity=&zip=&lat=&lng=&radius_miles=` — new location
params. Response adds `center` (`{lat,lng,zip,source}`), `radius_miles`, and
(when out of range) `nearest`. `best` now carries `distance_miles`, `city`,
`state`.
- `GET /api/data/latest?...&zip=&lat=&lng=&radius_miles=` — same params; rows
carry `distance_miles` and source `city`/`state`/`latitude`/`longitude`;
response adds `center` + `radius_miles` when a location is given.
- Errors (HTTP 400): zip *and* GPS together, partial GPS, `radius_miles` with no
location, or an unknown zip.
### Example questions → tool calls
| Ask | Call |
|---|---|
| Best corn bid within 40 mi of my zip | `best_local_bid(commodity="corn", zip="45810", radius_miles=40)` |
| Best soybean bid near my coordinates | `best_local_bid(commodity="soy", lat=40.79, lng=-83.81)` |
| Elevators near me, nearest first | `latest_prices(commodity="corn", zip="45810", radius_miles=60)` |
## 2026-05-30 — Regional fertilizer input costs (real $/ton + change)
Real U.S. **regional retail fertilizer prices** now feed the input-cost tools,
so the advisor can reason about fertilizer the same way it does diesel and grain
— actual dollars and the move, not an index. Source: **USDA AgTransport**
(monthly, `$/ton`, history back to 2023).
### Changed MCP tools (fertilizer added to the existing input-cost tools)
- **`input_cost_trend(item, geo?, years=10)`** — `item` now accepts six
fertilizers in addition to `diesel`: **`urea`, `uan`, `anhydrous`** (anhydrous
ammonia), **`dap`, `map`, `potash`**. Returns the latest real `$/ton`,
month-over-month and year-over-year change ($ and %), and seasonal context.
- New optional **`geo`** = AgTransport region; **defaults to `Cornbelt`**.
Other regions: `U.S. Gulf NOLA`, `Northern Plains`, `Southern Plains`,
`Southeast`, `Northeast`, `California`, `Pacific Northwest`, `South Central`,
`Central Florida`, `Tampa` (not every product is published for every region).
- `diesel` is unchanged — national U.S. `$/gal`, weekly; `geo` is ignored for it.
- **`input_cost_series(item, geo?)`** — raw monthly `$/ton` series for any
fertilizer (or the diesel `$/gal` weekly series), region-selectable.
Seasonal percentile for fertilizer is computed over a short history (2023+), so
it deepens over time; the price and YoY/MoM change are solid now.
### API
- `GET /api/data/input-cost-trend?item=&geo=&years=``geo` added.
- `GET /api/data/input-cost-series?item=&geo=``geo` added.
- `GET /api/data/input-cost-geographies?item=`**new**: lists the regions a
given input has data for (plus its `default_geo`).
### Example questions → tool calls
| Ask | Call |
|---|---|
| Cornbelt urea price and how it's moved | `input_cost_trend(item="urea")` |
| Anhydrous ammonia in the Southern Plains | `input_cost_trend(item="anhydrous", geo="Southern Plains")` |
| Potash $/ton history | `input_cost_series(item="potash")` |
| Which regions have DAP prices | (API) `GET /api/data/input-cost-geographies?item=dap` |
## 2026-05-30 — Grain price-received trends (real $ + change + seasonal)
New national/seasonal reference layer (USDA NASS, corn back to 1908) — the
macro benchmark to compare local cash bids against. Real prices and the change,
not an index.
### New MCP tools
- **`price_trend(commodity, geo="US", years=10)`** — monthly price *received by
farmers* ($/bu) with the move: latest real price, **month-over-month and
year-over-year change ($ and %)**, and seasonal context (percentile vs the
same month over the last N years, normal, and range). `geo` is `US` or any
2-letter state. Conclusions, not rows.
- **`price_series(commodity, geo="US", start_year?, end_year?)`** — raw monthly
series ($/bu) for charting/drill-down (defaults to the recent window).
### API
- `GET /api/data/price-trend?commodity=&geo=&years=` — computed trend (cents +
`change_cents`/`change_pct`/`yoy_*` + `seasonal`).
- `GET /api/data/price-series?commodity=&geo=&start_year=&end_year=` — raw series.
- `GET /api/data/price-geographies?commodity=` — which geos (US + states) exist.
All grain: corn/soy/wheat, all 50 states + US.
### Input-cost tools (real $ + change)
- **`input_cost_trend(item, years=10)`** — real input price with the move.
Currently `item="diesel"` (EIA U.S. retail $/gal, weekly, back to 1994):
latest price + week-over-week and year-over-year change + seasonal
percentile/range. (For current fertilizer $/ton, `current_input_price` (DTN)
still applies; more inputs extend this same tool.)
- **`input_cost_series(item)`** — raw historical series for an input.
- API: `GET /api/data/input-cost-trend?item=&years=`,
`GET /api/data/input-cost-series?item=`.
USDA stopped publishing dollar input prices in 2014, so these use real-dollar
sources (EIA) rather than an index.
## 2026-05-30 — Source geo + many more locations
### Per-source geo (API + MCP)
- **`GET /api/data/sources`** now returns location geo on every source:
`city, state, zip, county, latitude, longitude` (any may be `null`). The
AgriCharts-fed elevators carry exact coordinates; smaller sites carry
city/state/zip.
- **MCP `list_sources`** gained a **Location** column (`City, ST ZIP`).
`source_health` is unchanged in shape.
- Sources that are national or non-physical (CBOT futures, USDA AMS, DTN
fertilizer, hedge-to-arrive / "Direct" bid buckets) legitimately have `null`
geo. ~43 of ~51 sources are geo-tagged.
### Many more elevator locations
These flow through every data tool/endpoint automatically (`latest`, `history`,
`best`, `basis_movement`/`basis_detail`, `sources`, `deliveries`). No tool
signatures changed — just far more sources, all named `"<Co-op> — <Location>"`:
- **Heritage Cooperative** — new co-op, **23 central/eastern-Ohio locations**
(corn/soy/wheat).
- **Mercer Landmark** — expanded from 2 to **all ~16 locations**. Note: the two
old source names were renamed for continuity — `"Mercer Landmark — St Henry"`
`"Mercer Landmark — MPS St Henry"`, `"Mercer Landmark — Minster"`
`"Mercer Landmark — Heartland Minster"`.
- **Bambauer** — now both locations incl. **Pemberton**
(`"Bambauer — Jackson Center / New Knoxville"`, `"Bambauer — Pemberton"`);
the old `"Bambauer Jackson Center / New Knoxville, OH"` source was renamed to
the first of those.
If you cache source names, refresh them — several changed and many were added.
## 2026-05-29 — Futures price + change tool
### New MCP tool
- **`futures_quote(commodity, delivery?)`** — CBOT futures price and change for a
grain. Reports the latest price, today's **session open**, the prior day's
close, and **both** moves: **change since open** and **change on the day**
(vs the previous settle). With `delivery` (e.g. `"Jul 2026"`) it resolves the
listed contract that month prices against (e.g. `ZCN26`); without it, the
continuous nearby.
### API changes (`ag-monitor`)
- **`GET /api/data/futures?commodity=<grain>&delivery=<label?>`** — new endpoint.
Returns `{ commodity, delivery, contract, symbol, quote }` where `quote` is
`{ settle_date, open_cents, last_cents, prev_close_cents,
change_since_open_cents, change_on_day_cents, fetched_at }` (or `null` if no
data yet for that contract). `commodity` must be `corn`/`soy`/`wheat`.
- **`futures_quotes` table** gained an `open_cents` column; the futures scraper
now stores the session **Open** alongside the close. Rows captured before this
change have `open_cents = NULL`, so `change_since_open_cents` is `null` for
those until the next session is scraped.
- Futures now also pull at **:30 during the CBOT day session** (on top of the
hourly :00), so `last` and the changes track the session every ~30 min.
### Example questions → tool calls
| Ask | Call |
|---|---|
| Corn Jul futures and how far it's moved today | `futures_quote(commodity="corn", delivery="Jul 2026")` |
| Soy nearby futures, change on the day | `futures_quote(commodity="soy")` |
## 2026-05-29 — Flexible history + basis movement
### New MCP tools
- **`basis_movement(commodity?, source?, delivery?, days=30)`** — aggregated
basis trend, **one headline line per crop**. Averages every matching
(elevator × delivery) series to `avg basis first → last` over the window and
reports how far it moved. This is the cheap "how is basis moving overall"
view. All filters optional; `commodity` (if given) must be `corn`/`soy`/`wheat`.
- Positive move = **cash strengthened** vs futures (basis up); negative = weaker.
- Aggregation is intentional: divergent elevators can net to "flat". When the
aggregate looks flat or surprising, call `basis_detail` to see per-elevator
splits.
- **`basis_detail(commodity?, source?, delivery?, days=30)`** — the drill-down
for `basis_movement`. **One row per (elevator, crop, delivery)** series with
basis `first → last` and the move. Same optional filters.
Both tools do the aggregation **server-side (in the MCP)** and return compact
markdown — they do not dump every raw sample — to keep token usage low. Pattern:
call `basis_movement` first, then `basis_detail` (optionally filtered) only when
you need the breakdown.
### Changed MCP tools
- **`price_history`** — `commodity` is now **optional** (was required). Omit it
to span every crop. Output now groups by **(elevator, crop, delivery)**,
surfaces **basis first → last** in each series summary, and adds a **Futures**
column to the raw points table. Every filter (`commodity`, `source`,
`delivery`, `days`) is optional and AND'd — pivot per elevator, per crop, per
delivery, or any combination.
- Raw per-row points are still only included when the window has ≤ ~60 samples;
wider queries return the per-series summaries only.
- **`latest_prices`** — added the **`kind`** filter (`grain` | `fertilizer`) for
parity with the API. `commodity`, `source`, `delivery`, `kind` all optional.
### API changes (`ag-monitor`)
- **`GET /api/data/history`** — `commodity` query param is now **optional**.
When omitted, returns history across all crops. `source_id`, `delivery`, and
`days` (1730, default 30) remain optional. Response shape is unchanged:
`{ "commodity": <str|null>, "days": <int>, "rows": [...] }`. Each row carries
`fetched_at, source_id, source_name, commodity, delivery, bid_cents,
basis_cents, futures_cents` so callers can pivot client-side.
- `GET /api/data/latest` already supported optional `commodity`, `source`,
`delivery`, `kind` — unchanged.
### Conventions / gotchas
- All money fields are **integer cents**. `bid_cents`/`futures_cents` render as
`$/bu` (4 dp) for grain and `$/ton` (2 dp) for fertilizer; `basis_cents`
renders as a signed dollar value (2 dp), e.g. `-0.14`.
- Basis is only meaningful for grain — the basis tools skip non-grain rows and
any series with no basis on file.
- `source` filtering is by **exact** elevator display name (e.g.
`"Mercer Landmark — Minster"`). Use `list_sources` to get exact names.
### Example questions → tool calls
| Ask | Call |
|---|---|
| Basis movement overall, last 7 days | `basis_movement(days=7)` |
| Corn basis movement across all elevators | `basis_movement(commodity="corn")` |
| Basis movement at one elevator | `basis_movement(source="Mercer Landmark — Minster")` |
| Per-elevator basis breakdown for corn | `basis_detail(commodity="corn")` |
| Last 7 days of history from elevator X for corn | `price_history(commodity="corn", source="…", days=7)` |
| All-crop price history at one elevator | `price_history(source="…")` |
| Latest grain bids only | `latest_prices(kind="grain")` |