File size: 11,905 Bytes
3464008 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | // MCP Apps (extension `io.modelcontextprotocol/ui`, spec 2026-01-26) β the
// `ui://` resource registry. These are the interactive in-conversation app
// shells an MCP-Apps host renders inline; a tool links to one via
// `_meta.ui.resourceUri` (emitted by buildPublicTool from the tool's
// internal `_uiResourceUri`).
//
// How this differs from the DATA resources in ../resources/index.ts:
// - DATA resources (worldmonitor://β¦) return live JSON and consume the Pro
// daily quota symmetrically with the equivalent tools/call.
// - UI resources (ui://β¦) return a STATIC, data-free HTML template. They
// carry no data and spend no quota, so resources/read of a ui:// URI is
// served on the anonymous discovery path (an MCP-Apps host β or an
// agent-readiness scanner β must be able to fetch the shell to render
// it). Live data reaches the shell later, via host postMessage after a
// normal gated tools/call. See the handler's resources/read gate.
//
// The MCP-Apps UI resource mimeType is EXACTLY `text/html;profile=mcp-app`
// (the extension's content profile) β NOT `text/html+skybridge` (that is the
// OpenAI Apps SDK's marker).
import { rpcError, rpcOk } from '../rpc';
import { CHOKEPOINT_MONITOR_APP_HTML } from './chokepoint-monitor-app';
import { COUNTRY_BRIEF_APP_HTML } from './country-brief-app';
import { COUNTRY_RISK_APP_HTML } from './country-risk-app';
import { MARKET_RADAR_APP_HTML } from './market-radar-app';
import { buildUiMeta, UI_RESOURCE_MIME_TYPE as SHELL_UI_MIME_TYPE, type UiResourceMeta } from './shell';
import { WORLD_BRIEF_APP_HTML } from './world-brief-app';
import { NEWS_INTELLIGENCE_APP_HTML } from './news-intelligence-app';
import { CONFLICT_EVENTS_APP_HTML } from './conflict-events-app';
import { NATURAL_DISASTERS_APP_HTML } from './natural-disasters-app';
import { PREDICTION_MARKETS_APP_HTML } from './prediction-markets-app';
import { FORECASTS_APP_HTML } from './forecasts-app';
// Re-exported from the shared shell so the mimeType has a single source of
// truth across the fleet (the first widget defined it here in v1.11.0).
export const UI_RESOURCE_MIME_TYPE = SHELL_UI_MIME_TYPE;
// Canonical ui:// URIs for each app shell. Each is imported by its backing tool
// def as the single-source-of-truth `_uiResourceUri`, so the tool linkage and
// the registered resource can never drift.
export const COUNTRY_RISK_UI_URI = 'ui://worldmonitor/country-risk.html';
export const WORLD_BRIEF_UI_URI = 'ui://worldmonitor/world-brief.html';
export const COUNTRY_BRIEF_UI_URI = 'ui://worldmonitor/country-brief.html';
export const MARKET_RADAR_UI_URI = 'ui://worldmonitor/market-radar.html';
export const CHOKEPOINT_MONITOR_UI_URI = 'ui://worldmonitor/chokepoint-monitor.html';
export const NEWS_INTELLIGENCE_UI_URI = 'ui://worldmonitor/news-intelligence.html';
export const CONFLICT_EVENTS_UI_URI = 'ui://worldmonitor/conflict-events.html';
export const NATURAL_DISASTERS_UI_URI = 'ui://worldmonitor/natural-disasters.html';
export const PREDICTION_MARKETS_UI_URI = 'ui://worldmonitor/prediction-markets.html';
export const FORECASTS_UI_URI = 'ui://worldmonitor/forecasts.html';
// Per-resource `_meta.ui` (ext-apps `UIResourceMeta`) is built by the shared
// `buildUiMeta()` in ./shell β SINGLE source of truth for the fleet's CSP /
// render policy. The `csp` block is the spec-native complement to the HTML
// `<meta http-equiv>` CSP: `connectDomains` mirrors the meta's `connect-src`
// (the MCP server origin); `resourceDomains` / `frameDomains` / `baseUriDomains`
// stay empty (the secure default) because the apps load no external assets,
// embed no frames, and need no external base URI (postMessage + inline CSS/JS
// only). `prefersBorder` asks the host to frame the card. A fresh object is
// minted per entry so a mutating consumer can't poison siblings. Surfaced on
// BOTH resources/list and the resources/read response.
export type { UiResourceMeta };
interface UiResourceDef {
uri: string;
name: string;
description: string;
mimeType: string;
_meta: UiResourceMeta;
// The verbatim self-contained HTML served on resources/read.
html: string;
}
export const UI_RESOURCE_REGISTRY: UiResourceDef[] = [
{
uri: COUNTRY_RISK_UI_URI,
name: 'Country Risk (interactive)',
description:
'Interactive in-conversation app shell for get_country_risk: renders the Composite Instability Index (CII 0-100), the unrest/conflict/security/news component breakdown, travel-advisory level, and sanctions exposure. Linked from the get_country_risk tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: COUNTRY_RISK_APP_HTML,
},
{
uri: WORLD_BRIEF_UI_URI,
name: 'World Brief (interactive)',
description:
'Interactive in-conversation app shell for get_world_brief: renders the AI-summarised global intelligence brief as readable paragraphs, the grounding headlines, and the source feed articles. Linked from the get_world_brief tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: WORLD_BRIEF_APP_HTML,
},
{
uri: COUNTRY_BRIEF_UI_URI,
name: 'Country Brief (interactive)',
description:
'Interactive in-conversation app shell for get_country_brief: renders the AI-synthesised per-country intelligence brief as paragraphs, the analytical framework lens, and the grounding sources. Linked from the get_country_brief tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: COUNTRY_BRIEF_APP_HTML,
},
{
uri: MARKET_RADAR_UI_URI,
name: 'Market Radar (interactive)',
description:
'Interactive in-conversation app shell for get_market_data: renders the Fear & Greed composite plus per-asset-class quote tables (equities, commodities, crypto, Gulf, sectors) with signed, colour-coded change. Linked from the get_market_data tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: MARKET_RADAR_APP_HTML,
},
{
uri: CHOKEPOINT_MONITOR_UI_URI,
name: 'Chokepoint Monitor (interactive)',
description:
'Interactive in-conversation app shell for get_chokepoint_status: renders per-chokepoint rolling transit summaries (today\'s transit count, week-over-week change, tanker split) with a risk-level badge. Linked from the get_chokepoint_status tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: CHOKEPOINT_MONITOR_APP_HTML,
},
{
uri: NEWS_INTELLIGENCE_UI_URI,
name: 'News Intelligence (interactive)',
description:
'Interactive in-conversation app shell for get_news_intelligence: renders AI-classified top stories (title, category, alert flag, country, source) from WorldMonitor\'s intelligence layer. Linked from the get_news_intelligence tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: NEWS_INTELLIGENCE_APP_HTML,
},
{
uri: CONFLICT_EVENTS_UI_URI,
name: 'Conflict Events (interactive)',
description:
'Interactive in-conversation app shell for get_conflict_events: renders active armed-conflict events (belligerents, violence type, country, fatalities, date) from the UCDP feed. Linked from the get_conflict_events tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: CONFLICT_EVENTS_APP_HTML,
},
{
uri: NATURAL_DISASTERS_UI_URI,
name: 'Natural Disasters (interactive)',
description:
'Interactive in-conversation app shell for get_natural_disasters: groups recent earthquakes (USGS magnitude, place, time) and active wildfires (NASA FIRMS). Linked from the get_natural_disasters tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: NATURAL_DISASTERS_APP_HTML,
},
{
uri: PREDICTION_MARKETS_UI_URI,
name: 'Prediction Markets (interactive)',
description:
'Interactive in-conversation app shell for get_prediction_markets: renders active event-contract odds grouped by category (geopolitical, tech, finance) with a probability bar per market. Linked from the get_prediction_markets tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: PREDICTION_MARKETS_APP_HTML,
},
{
uri: FORECASTS_UI_URI,
name: 'Forecasts (interactive)',
description:
'Interactive in-conversation app shell for get_forecast_predictions: renders WorldMonitor\'s AI-generated geopolitical and economic forecasts as probability cards (title, domain, region). Linked from the get_forecast_predictions tool via _meta.ui.resourceUri; an MCP-Apps host renders it inline and streams the tool result in via postMessage. Static, data-free template β public and quota-exempt.',
mimeType: UI_RESOURCE_MIME_TYPE,
_meta: buildUiMeta(),
html: FORECASTS_APP_HTML,
},
];
// Fast membership set for the handler's gate promotion + parsing.
const UI_RESOURCE_BY_URI = new Map(UI_RESOURCE_REGISTRY.map((r) => [r.uri, r]));
export function isUiResourceUri(uri: string): boolean {
return UI_RESOURCE_BY_URI.has(uri);
}
// resources/list public shape β {uri, name, description, mimeType} plus the
// spec `_meta.ui` (CSP + render prefs) so a host learns the view policy at
// discovery time. The internal `html` field never leaks.
export interface PublicUiResourceShape {
uri: string;
name: string;
description: string;
mimeType: string;
_meta: UiResourceMeta;
}
export const UI_RESOURCE_LIST_RESPONSE: PublicUiResourceShape[] = UI_RESOURCE_REGISTRY.map((r) => ({
uri: r.uri,
name: r.name,
description: r.description,
mimeType: r.mimeType,
_meta: r._meta,
}));
// resources/read responder for a ui:// URI. Returns the static HTML verbatim
// as a spec-shaped resources/read result. No auth context, no dispatch, no
// quota β the caller (handler) has already resolved that this URI is a public
// UI resource via isUiResourceUri().
export function buildUiResourceRead(
id: unknown,
uri: string,
corsHeaders: Record<string, string>,
): Response {
const def = UI_RESOURCE_BY_URI.get(uri);
if (!def) {
// Unreachable in practice β the handler only routes here after
// isUiResourceUri(uri) is true β but fail closed with a spec -32602.
return rpcError(id, -32602, `Unknown ui:// resource "${uri}".`, corsHeaders);
}
return rpcOk(
id,
{ contents: [{ uri: def.uri, mimeType: def.mimeType, text: def.html, _meta: def._meta }] },
corsHeaders,
);
}
|