From 03c6c540efa7ecc5a34ea631d4926b7b04b6c453 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Sat, 30 May 2026 15:59:33 -0400 Subject: [PATCH] Add fertilizer to input-cost tools (regional $/ton, USDA AgTransport) input_cost_trend/input_cost_series now accept six fertilizers (urea, uan, anhydrous, dap, map, potash) alongside diesel, with an optional `geo` region (default Cornbelt). Real $/ton + MoM/YoY change + seasonal context. - client: pass geo through; add input_cost_geographies - server: expand VALID_INPUTS; geo param + docstrings - format already unit-aware ($/ton) and geo-aware - README tools table now lists the reference/trend + input-cost tools - CHANGELOG: regional fertilizer input-cost release notes - tests: fertilizer $/ton + region formatting Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++ README.md | 11 ++++++++--- ag_bids_mcp/client.py | 12 ++++++++---- ag_bids_mcp/server.py | 45 +++++++++++++++++++++++++++++++------------ tests/test_format.py | 15 +++++++++++++++ 5 files changed, 104 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aafae9..0b120bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,46 @@ Notes for clients/agents that consume the ag-bids MCP tools and the underlying `ag-monitor` `/api/data/*` HTTP API. Newest first. +## 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 diff --git a/README.md b/README.md index 694fb16..f0ff66c 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,9 @@ ag-bids-mcp ── X-API-Key ─► https://agbids.paul.farm/api/data/* ``` Endpoints used: `/api/data/latest`, `/history`, `/futures`, `/best`, `/inputs`, -`/sources`, `/deliveries`. See the [ag-monitor source](https://git.jpaul.io/justin/ag-bids) -for the contract. +`/sources`, `/deliveries`, `/price-trend`, `/price-series`, `/price-geographies`, +`/input-cost-trend`, `/input-cost-series`, `/input-cost-geographies`. See the +[ag-monitor source](https://git.jpaul.io/justin/ag-bids) for the contract. ## Authentication @@ -77,7 +78,11 @@ auto-updates it every 5 minutes. | `best_local_bid(commodity)` | Where to sell `corn`, `soy`, or `wheat` for this month's delivery — markdown one-liner + table | | `futures_quote(commodity, delivery?)` | CBOT futures price + change since open + change on the day. With a delivery month it resolves the listed contract; without it, the continuous nearby | | `current_lime_price()` | Latest lime quotes across all manual-entry sources | -| `current_input_price(product?)` | MAP / Potash / Lime — all three or one | +| `current_input_price(product?)` | MAP / Potash / Lime — all three or one (local DTN dealer feed) | +| `price_trend(commodity, geo?, years?)` | USDA NASS monthly price *received* ($/bu) + MoM/YoY change + seasonal context. `geo` = `US` or a state (corn back to 1908) | +| `price_series(commodity, geo?, start_year?, end_year?)` | Raw monthly price-received series for charting | +| `input_cost_trend(item, geo?, years?)` | Real input cost + change. `item` = `diesel` (U.S. `$/gal`, weekly, EIA) or a fertilizer `$/ton` (`urea`/`uan`/`anhydrous`/`dap`/`map`/`potash`, monthly, USDA AgTransport); `geo` selects the fertilizer region (default `Cornbelt`) | +| `input_cost_series(item, geo?)` | Raw historical series for an input cost (diesel `$/gal` or fertilizer `$/ton`, region-selectable) | | `latest_prices(commodity?, source?, delivery?, kind?)` | Live snapshot table; every filter optional (`kind` = `grain`/`fertilizer`) | | `price_history(commodity?, source?, delivery?, days?)` | Time series per (elevator, crop, delivery); **every filter optional** — omit `commodity` to span all crops. Shows bid + basis trend + futures | | `basis_movement(commodity?, source?, delivery?, days?)` | Aggregated basis trend, one headline line per crop (the cheap overview) | diff --git a/ag_bids_mcp/client.py b/ag_bids_mcp/client.py index 7de688f..b57dfb6 100644 --- a/ag_bids_mcp/client.py +++ b/ag_bids_mcp/client.py @@ -80,12 +80,16 @@ def price_series(commodity: str, geo: str = "US", start_year=start_year, end_year=end_year) -def input_cost_trend(item: str, years: int = 10) -> dict: - return _get("/api/data/input-cost-trend", item=item, years=years) +def input_cost_trend(item: str, years: int = 10, geo: str | None = None) -> dict: + return _get("/api/data/input-cost-trend", item=item, years=years, geo=geo) -def input_cost_series(item: str) -> dict: - return _get("/api/data/input-cost-series", item=item) +def input_cost_series(item: str, geo: str | None = None) -> dict: + return _get("/api/data/input-cost-series", item=item, geo=geo) + + +def input_cost_geographies(item: str) -> dict: + return _get("/api/data/input-cost-geographies", item=item) def futures(commodity: str, delivery: str | None = None) -> dict: diff --git a/ag_bids_mcp/server.py b/ag_bids_mcp/server.py index d135ab0..f37eeb4 100644 --- a/ag_bids_mcp/server.py +++ b/ag_bids_mcp/server.py @@ -303,40 +303,61 @@ def price_series( return fmt.fmt_price_series(payload) -VALID_INPUTS = {"diesel"} +# Fuel is national (US); fertilizer is regional ($/ton, USDA AgTransport). +VALID_INPUTS = {"diesel", "urea", "uan", "anhydrous", "dap", "map", "potash"} +_FERT_INPUTS = {"urea", "uan", "anhydrous", "dap", "map", "potash"} @mcp.tool() def input_cost_trend( item: Annotated[ - str, Field(description="Input to price. Currently: 'diesel' (U.S. retail $/gal).") + str, + Field(description="Input to price: 'diesel' (U.S. retail $/gal) or a fertilizer " + "($/ton) — 'urea', 'uan', 'anhydrous', 'dap', 'map', 'potash'."), ], + geo: Annotated[ + str | None, + Field(description="Fertilizer region (default 'Cornbelt'). Other regions: " + "'U.S. Gulf NOLA', 'Northern Plains', 'Southern Plains', " + "'Southeast', 'Northeast', 'California', 'Pacific Northwest', " + "'South Central', 'Central Florida', 'Tampa'. Ignored for diesel (US)."), + ] = None, years: Annotated[ int, Field(ge=1, le=120, description="Baseline window for the seasonal normal/percentile."), ] = 10, ) -> str: - """Real input cost with the change — e.g. U.S. retail diesel ($/gal). + """Real input cost with the change — retail diesel ($/gal) or fertilizer ($/ton). - Latest real price + week-over-week and year-over-year moves + seasonal - percentile/range. Fills the input-cost side for the advisor (fuel). For - current fertilizer $/ton use current_input_price.""" + Latest real price + period-over-period and year-over-year moves + seasonal + percentile/range. Diesel is U.S. weekly (back to 1994); fertilizer is monthly + regional (USDA AgTransport, Cornbelt default, back to 2023). This is the + input-cost side for the advisor. For a one-off current fertilizer quote from a + local dealer feed, current_input_price (DTN) still applies.""" it = item.strip().lower() - with track("input_cost_trend", item=it, years=years): + g = geo.strip() if geo else None + with track("input_cost_trend", item=it, geo=g or "", years=years): if it not in VALID_INPUTS: return f"`item` must be one of: {sorted(VALID_INPUTS)}" - return fmt.fmt_price_trend(client.input_cost_trend(item=it, years=years)) + return fmt.fmt_price_trend(client.input_cost_trend(item=it, years=years, geo=g)) @mcp.tool() def input_cost_series( - item: Annotated[str, Field(description="Input: 'diesel'.")], + item: Annotated[ + str, Field(description="Input: 'diesel' or a fertilizer ('urea', 'uan', " + "'anhydrous', 'dap', 'map', 'potash').")], + geo: Annotated[ + str | None, + Field(description="Fertilizer region (default 'Cornbelt'). Ignored for diesel."), + ] = None, ) -> str: - """Raw historical series for a tracked input cost (diesel, $/gal).""" + """Raw historical series for a tracked input cost (diesel $/gal or fertilizer $/ton).""" it = item.strip().lower() - with track("input_cost_series", item=it): + g = geo.strip() if geo else None + with track("input_cost_series", item=it, geo=g or ""): if it not in VALID_INPUTS: return f"`item` must be one of: {sorted(VALID_INPUTS)}" - return fmt.fmt_price_series(client.input_cost_series(item=it)) + return fmt.fmt_price_series(client.input_cost_series(item=it, geo=g)) @mcp.tool() diff --git a/tests/test_format.py b/tests/test_format.py index 804d85a..0a8b570 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -131,6 +131,21 @@ def test_fmt_input_cost_payload_shape(): assert "+58.2%" in out # YoY surfaced +def test_fmt_input_cost_fertilizer_ton_and_region(): + # Fertilizer payload carries item + geo (region) + $/ton. + payload = {"item": "urea", "geo": "Cornbelt", "label": "urea", "unit": "$/ton", + "source": "USDA AgTransport", + "trend": {"period": "2026-03-01", "value_cents": 69812, "prev_cents": 51812, + "change_cents": 18000, "change_pct": 34.7, "yoy_cents": 45500, + "yoy_change_cents": 24312, "yoy_pct": 53.4, "seasonal": None, + "recent_direction": "up", "baseline_years": 10, "points": 39}} + out = fmt.fmt_price_trend(payload) + assert "urea — Cornbelt — urea" in out # item, region, label all present + assert "$698.12/ton" in out + assert "USDA AgTransport" in out + assert "Mar 2026" in out # monthly period rendered as month + + def test_fmt_inputs_lime_table(): payload = { "product": "lime", "count": 2,