875a190983
Exposes live + historical ag-bids commodity data (from the ag-monitor service at agbids.paul.farm) as MCP tools, sitting behind MetaMCP at https://mcp.jpaul.io/metamcp/ag-bids/mcp. Pattern mirrors zerto-docs-rag with one addition: HTTP Basic auth in front of the streamable-HTTP transport so namespace guessers can't reach the tools. Stdio transport is unaffected (used by local Claude Desktop dev). Tools (markdown returns, ~15 LOC each): best_local_bid(commodity) — where to sell corn/soy/wheat today, for the current calendar month only current_lime_price() — latest lime quotes ($/ton) current_input_price(product?) — MAP / Potash / Lime latest_prices(...) — filtered snapshot price_history(...) — per-(source,delivery) trend list_sources / list_commodities / list_deliveries source_health() — healthy / stale / down buckets todays_summary() — same shape as morning brief snapshot Data path: ag-bids-mcp -> X-API-Key -> /api/data/* on ag-monitor (reuses BRIEF_API_KEY). Tests: 24 covering the httpx client, markdown formatters, HTTP Basic middleware (401/200), and JSONL usage logging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
250 lines
8.3 KiB
Python
250 lines
8.3 KiB
Python
"""JSON-from-ag-monitor → markdown helpers.
|
|
|
|
One function per @mcp.tool. Markdown is what FastMCP tools return; Claude /
|
|
OpenWebUI render it nicely. Cents → dollars formatting is centralized here so
|
|
all tools use the same precision (4 decimals for $/bu, 2 decimals for $/ton).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
|
|
def _bu(cents: Optional[int]) -> str:
|
|
if cents is None:
|
|
return "—"
|
|
return f"${cents / 100:.4f}"
|
|
|
|
|
|
def _ton(cents: Optional[int]) -> str:
|
|
if cents is None:
|
|
return "—"
|
|
return f"${cents / 100:.2f}"
|
|
|
|
|
|
def _basis(cents: Optional[int]) -> str:
|
|
if cents is None:
|
|
return "—"
|
|
sign = "+" if cents >= 0 else ""
|
|
return f"{sign}{cents / 100:.2f}"
|
|
|
|
|
|
def _delta_arrow(cents: Optional[int]) -> str:
|
|
if cents is None or cents == 0:
|
|
return "—"
|
|
return "▲" if cents > 0 else "▼"
|
|
|
|
|
|
# ---------- best_local_bid ----------
|
|
|
|
|
|
def fmt_best(commodity: str, payload: dict) -> str:
|
|
best = payload.get("best")
|
|
today = payload.get("today")
|
|
if not best:
|
|
return (
|
|
f"### Best place to sell {commodity} today ({today})\n\n"
|
|
f"No current-month {commodity} bids posted across the tracked sources.\n"
|
|
)
|
|
return (
|
|
f"### Best place to sell {commodity} today ({today})\n\n"
|
|
f"**{best['source_name']}** — delivery **{best['delivery']}** — "
|
|
f"bid **{_bu(best['bid_cents'])}/bu** (basis {_basis(best.get('basis_cents'))}, "
|
|
f"futures {best.get('futures_contract') or '—'})\n\n"
|
|
f"_Fetched {best.get('fetched_at') or '?'}_\n"
|
|
)
|
|
|
|
|
|
# ---------- inputs / fertilizer ----------
|
|
|
|
|
|
def fmt_inputs(payload: dict) -> str:
|
|
rows = payload.get("rows") or []
|
|
product = payload.get("product")
|
|
title = f"### {product.upper()} prices" if product else "### Fertilizer + lime prices"
|
|
if not rows:
|
|
scope = product or "any tracked input"
|
|
return f"{title}\n\nNo {scope} prices on file.\n"
|
|
lines = [
|
|
title, "",
|
|
"| Source | Product | Delivery | Price ($/ton) | Fetched |",
|
|
"|---|---|---|---:|---|",
|
|
]
|
|
for r in rows:
|
|
lines.append(
|
|
f"| {r['source_name']} | {r.get('display_name') or r['commodity']} | "
|
|
f"{r['delivery']} | {_ton(r.get('bid_cents'))} | {r.get('fetched_at') or '?'} |"
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
# ---------- latest snapshot ----------
|
|
|
|
|
|
def fmt_latest(payload: dict) -> str:
|
|
rows = payload.get("rows") or []
|
|
if not rows:
|
|
return "### Latest prices\n\nNo rows match those filters.\n"
|
|
lines = [
|
|
"### Latest prices", "",
|
|
"| Source | Commodity | Delivery | Bid | Basis | Futures | Fetched |",
|
|
"|---|---|---|---:|---:|---|---|",
|
|
]
|
|
for r in rows:
|
|
unit_fmt = _ton if r.get("commodity_kind") == "fertilizer" else _bu
|
|
lines.append(
|
|
f"| {r['source_name']} | {r.get('display_name') or r['commodity']} | "
|
|
f"{r['delivery']} | {unit_fmt(r.get('bid_cents'))} | "
|
|
f"{_basis(r.get('basis_cents'))} | {r.get('futures_contract') or '—'} | "
|
|
f"{r.get('fetched_at') or '?'} |"
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
# ---------- price history ----------
|
|
|
|
|
|
def fmt_history(payload: dict, max_rows: int = 60) -> str:
|
|
rows = payload.get("rows") or []
|
|
commodity = payload.get("commodity")
|
|
days = payload.get("days")
|
|
if not rows:
|
|
return f"### {commodity} history ({days}d)\n\nNo samples in the window.\n"
|
|
|
|
# Per (source, delivery) trend annotation: first vs last sample.
|
|
series: dict[tuple, list[dict]] = {}
|
|
for r in rows:
|
|
series.setdefault((r["source_name"], r["delivery"]), []).append(r)
|
|
|
|
lines = [f"### {commodity} price history — last {days} days", ""]
|
|
for (src, dlv), pts in sorted(series.items()):
|
|
if not pts:
|
|
continue
|
|
first = pts[0].get("bid_cents")
|
|
last = pts[-1].get("bid_cents")
|
|
delta = (last - first) if (first is not None and last is not None) else None
|
|
arrow = _delta_arrow(delta)
|
|
lines.append(
|
|
f"- **{src}** / {dlv}: {len(pts)} samples · "
|
|
f"{_bu(first)} → {_bu(last)} {arrow} {_basis(delta) if delta is not None else ''}".rstrip()
|
|
)
|
|
|
|
# If the history is shallow include the raw rows too (helpful for charts).
|
|
if sum(len(p) for p in series.values()) <= max_rows:
|
|
lines.extend([
|
|
"",
|
|
"| Time | Source | Delivery | Bid | Basis |",
|
|
"|---|---|---|---:|---:|",
|
|
])
|
|
for r in rows[-max_rows:]:
|
|
lines.append(
|
|
f"| {r['fetched_at']} | {r['source_name']} | {r['delivery']} | "
|
|
f"{_bu(r.get('bid_cents'))} | {_basis(r.get('basis_cents'))} |"
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
# ---------- sources / health ----------
|
|
|
|
|
|
def fmt_sources(payload: dict) -> str:
|
|
src = payload.get("sources") or []
|
|
if not src:
|
|
return "### Sources\n\nNo active sources.\n"
|
|
lines = [
|
|
"### Tracked sources", "",
|
|
"| Source | Kind | Last success | Consecutive failures | Last error |",
|
|
"|---|---|---|---:|---|",
|
|
]
|
|
for s in src:
|
|
lines.append(
|
|
f"| {s['name']} | {s['kind']} | {s.get('last_success_at') or '—'} | "
|
|
f"{s.get('consecutive_failures') or 0} | {s.get('last_error') or ''} |"
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def fmt_health(payload: dict) -> str:
|
|
src = payload.get("sources") or []
|
|
healthy, stale, down = [], [], []
|
|
for s in src:
|
|
n = s.get("consecutive_failures") or 0
|
|
if n >= 3:
|
|
down.append(s)
|
|
elif not s.get("last_success_at"):
|
|
stale.append(s)
|
|
else:
|
|
healthy.append(s)
|
|
lines = ["### Source health", ""]
|
|
lines.append(f"- ✅ Healthy: **{len(healthy)}**")
|
|
lines.append(f"- ⚠ Stale (never succeeded): **{len(stale)}**")
|
|
lines.append(f"- ❌ Down (3+ consecutive failures): **{len(down)}**")
|
|
if stale or down:
|
|
lines.append("")
|
|
lines.append("| Source | State | Last success | Failures | Error |")
|
|
lines.append("|---|---|---|---:|---|")
|
|
for s in stale + down:
|
|
state = "stale" if s in stale else "down"
|
|
lines.append(
|
|
f"| {s['name']} | {state} | {s.get('last_success_at') or '—'} | "
|
|
f"{s.get('consecutive_failures') or 0} | "
|
|
f"{(s.get('last_error') or '')[:80]} |"
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
# ---------- list helpers ----------
|
|
|
|
|
|
def fmt_deliveries(payload: dict) -> str:
|
|
commodity = payload.get("commodity")
|
|
labels = payload.get("deliveries") or []
|
|
if not labels:
|
|
return f"### {commodity} deliveries\n\nNo posted deliveries.\n"
|
|
return (
|
|
f"### Posted delivery labels for {commodity}\n\n"
|
|
+ "\n".join(f"- {x}" for x in labels)
|
|
+ "\n"
|
|
)
|
|
|
|
|
|
def fmt_commodities() -> str:
|
|
return (
|
|
"### Tracked commodities\n\n"
|
|
"- **corn** ($/bu)\n"
|
|
"- **soy** — soybeans ($/bu)\n"
|
|
"- **wheat** ($/bu) — tracked but excluded from daily brief emails\n"
|
|
"- **map** — MAP 11-52-0 ($/ton)\n"
|
|
"- **potash** — Potash 0-0-60 ($/ton)\n"
|
|
"- **lime** ($/ton)\n"
|
|
)
|
|
|
|
|
|
# ---------- today's summary ----------
|
|
|
|
|
|
def fmt_summary(payload: dict) -> str:
|
|
today = payload.get("today")
|
|
prev = payload.get("prev_trading_day")
|
|
lines = [f"### Market summary — today {today} vs prev trading day {prev}", ""]
|
|
for c in payload.get("commodities", []):
|
|
f = c.get("futures") or {}
|
|
chg = f.get("change_cents")
|
|
arrow = _delta_arrow(chg)
|
|
lines.append(
|
|
f"**{c['display_name']}** — CBOT {f.get('contract','?')} "
|
|
f"last {_bu(f.get('today_last_cents'))}, prev close "
|
|
f"{_bu(f.get('prev_close_cents'))} {arrow} {_basis(chg) if chg is not None else ''}"
|
|
)
|
|
bft = c.get("best_for_today")
|
|
if bft:
|
|
lines.append(
|
|
f" · Best for today's delivery ({bft['delivery']}): "
|
|
f"**{bft['source_name']}** @ {_bu(bft['bid_cents'])} "
|
|
f"(basis {_basis(bft.get('basis_cents'))})"
|
|
)
|
|
else:
|
|
lines.append(" · No current-month local bid posted.")
|
|
lines.append("")
|
|
return "\n".join(lines).rstrip() + "\n"
|