// MCP Apps (extension `io.modelcontextprotocol/ui`, spec 2026-01-26) — the // interactive app shell for the `get_forecast_predictions` tool: WorldMonitor's // AI-generated geopolitical/economic forecasts as probability cards (title, // domain + region, probability bar). Built on the shared shell foundation. // // Tool result shape (cache tool — content[0].text JSON, freshness envelope). // The tool serves the raw `forecast:predictions:v2` cache value under label // `predictions`; items carry `probability` as a 0-1 fraction (see // src/components/ForecastPanel.ts): // { cached_at, stale, data: { // predictions: { predictions: [{ title, domain, region, probability }] } } } // probability is nullable (render "—" and hide the bar when absent). This widget // renders forecast titles and probabilities ONLY — no calibration / Brier / // scorecard claims (that is the get_forecast_scorecard tool's domain, guarded // elsewhere). // // textContent-only rendering; renderBody stays backtick/`${`/regex-free. import { buildAppHtml } from './shell'; const STYLES = ` .fc { padding: 8px 0; border-bottom: 1px solid var(--border); } .fc:last-child { border-bottom: none; } .fc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; } .fc-title { font-size: 13px; color: var(--fg); min-width: 0; } .fc-prob { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 13px; white-space: nowrap; } .fc-meta { margin-top: 3px; display: flex; gap: 6px; flex-wrap: wrap; } .chip { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 1px 7px; } `; const BODY = `
Forecasts
WorldMonitor Forecasts
Waiting for forecasts…
`; const RENDER = ` if (!data || typeof data !== "object") return; var d = data.data && typeof data.data === "object" ? data.data : data; q("empty").style.display = "none"; q("card").style.display = "block"; var node = d.predictions && typeof d.predictions === "object" ? d.predictions : null; var preds = listState(node && node.predictions).items; var host = q("list"); host.textContent = ""; for (var i = 0; i < preds.length && i < 12; i++) { var p = preds[i]; if (!p || typeof p !== "object") continue; var fc = el("div", "fc"); var head = el("div", "fc-head"); head.appendChild(el("span", "fc-title", collapseWs(p.title) || "Forecast")); var pr = num(p.probability); var pct = pr == null ? null : (pr <= 1 ? pr * 100 : pr); head.appendChild(el("span", "fc-prob", pct == null ? "—" : Math.round(pct) + "%")); fc.appendChild(head); var meta = el("div", "fc-meta"); var dom = collapseWs(p.domain); if (dom) meta.appendChild(el("span", "chip", dom)); var reg = collapseWs(p.region); if (reg) meta.appendChild(el("span", "chip", reg)); if (meta.childNodes.length) fc.appendChild(meta); var bar = probabilityBar(pct); if (bar) fc.appendChild(bar); host.appendChild(fc); } if (!host.childNodes.length) host.appendChild(el("div", "empty", "No forecasts available.")); q("foot").textContent = data.cached_at ? "Snapshot: " + collapseWs(data.cached_at) + (data.stale ? " (stale)" : "") : ""; `; export const FORECASTS_APP_HTML = buildAppHtml({ title: 'Forecasts — WorldMonitor', appName: 'worldmonitor-forecasts', styles: STYLES, body: BODY, renderBody: RENDER, });