/** * TomTom MCP App Host – Debug UI * * Three-column layout: * Left sidebar – scrollable tool list with search * Center panel – JSON input editor + call controls * Right panel – tabbed map widget / JSON result */ import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { StrictMode, Suspense, use, useCallback, useEffect, useRef, useState, useMemo } from "react"; import { createRoot } from "react-dom/client"; import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, newAppBridge, type ServerInfo, type ToolCallInfo, } from "./implementation"; import { toggleTheme, getTheme, onThemeChange, type Theme } from "./theme"; import tomtomLogoUrl from "../../images/TomTom-logo.svg"; // ─── Example Inputs ────────────────────────────────────────────────────── // Orbis examples (locations as [lon, lat] tuples, routeType "fast"/"short", traffic as enum) const ORBIS_EXAMPLE_INPUTS: Record> = { "tomtom-geocode": { query: "Amsterdam Central Station", limit: 3, language: "en-US", show_ui: true, response_detail: "compact", }, "tomtom-reverse-geocode": { position: [4.8897, 52.374], language: "en-US", show_ui: true, response_detail: "compact", }, "tomtom-fuzzy-search": { query: "restaurants in Amsterdam", position: [4.8897, 52.374], limit: 5, show_ui: true, response_detail: "compact", }, "tomtom-poi-search": { query: "coffee shop", position: [4.8897, 52.374], limit: 5, show_ui: true, response_detail: "compact", }, "tomtom-nearby": { position: [4.8897, 52.374], poiCategories: ["RESTAURANT"], radius: 2000, limit: 5, show_ui: true, response_detail: "compact", }, "tomtom-routing": { locations: [[4.8897, 52.374], [13.405, 52.52]], travelMode: "car", routeType: "fast", traffic: "live", show_ui: true, response_detail: "compact", }, "tomtom-reachable-range": { origin: [4.8897, 52.374], timeBudgetInSec: 1800, travelMode: "car", routeType: "fast", show_ui: true, response_detail: "compact", }, "tomtom-traffic": { bbox: [4.8, 52.3, 4.95, 52.4], language: "en-US", show_ui: true, response_detail: "compact", }, "tomtom-dynamic-map": { markers: [{ lat: 52.374, lon: 4.8897, label: "Amsterdam" }], width: 600, height: 400, show_ui: true, }, "tomtom-ev-search": { position: [4.9041, 52.3676], radius: 5000, limit: 5, show_ui: true, response_detail: "compact", }, "tomtom-area-search": { query: "restaurant", center: [4.9041, 52.3676], radius: 2000, limit: 5, show_ui: true, response_detail: "compact", }, "tomtom-search-along-route": { origin: [4.9041, 52.3676], destination: [5.4697, 51.4416], query: "gas station", limit: 3, show_ui: true, response_detail: "compact", }, "tomtom-ev-routing": { origin: [4.9041, 52.3676], destination: [5.4697, 51.4416], currentChargePercent: 80, maxChargeKWH: 60, show_ui: true, response_detail: "compact", }, "tomtom-data-viz": { data_url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson", layers: [ { type: "clusters", label_property: "title", popup_fields: ["title", "mag", "place", "type", "time"], }, ], title: "USGS Earthquakes — Past 7 Days", show_ui: true, }, }; // Genesis examples (origin/destination as {lat, lon} objects, routeType "fastest"/"shortest", traffic as boolean) const GENESIS_EXAMPLE_INPUTS: Record> = { "tomtom-geocode": { query: "Amsterdam Central Station", limit: 3, language: "en-US", response_detail: "compact", }, "tomtom-reverse-geocode": { lat: 52.374, lon: 4.8897, language: "en-US", response_detail: "compact", }, "tomtom-fuzzy-search": { query: "restaurants in Amsterdam", lat: 52.374, lon: 4.8897, limit: 5, response_detail: "compact", }, "tomtom-poi-search": { query: "coffee shop", lat: 52.374, lon: 4.8897, limit: 5, response_detail: "compact", }, "tomtom-nearby": { lat: 52.374, lon: 4.8897, categorySet: "7315", radius: 2000, limit: 5, response_detail: "compact", }, "tomtom-routing": { origin: { lat: 52.374, lon: 4.8897 }, destination: { lat: 52.52, lon: 13.405 }, travelMode: "car", routeType: "fastest", traffic: true, response_detail: "compact", }, "tomtom-waypoint-routing": { waypoints: [ { lat: 52.374, lon: 4.8897 }, { lat: 51.2217, lon: 4.4051 }, { lat: 50.8503, lon: 4.3517 }, ], travelMode: "car", routeType: "fastest", traffic: true, response_detail: "compact", }, "tomtom-reachable-range": { origin: { lat: 52.374, lon: 4.8897 }, timeBudgetInSec: 1800, travelMode: "car", routeType: "fastest", response_detail: "compact", }, "tomtom-traffic": { bbox: "4.8,52.3,4.95,52.4", language: "en-US", response_detail: "compact", }, "tomtom-static-map": { center: { lat: 52.374, lon: 4.8897 }, zoom: 12, width: 512, height: 512, }, "tomtom-dynamic-map": { markers: [{ lat: 52.374, lon: 4.8897, label: "Amsterdam" }], width: 600, height: 400, }, }; // ─── Data Viz Presets (one per layer type) ─────────────────────────────── const DATA_VIZ_PRESETS: { key: string; label: string; input: Record }[] = [ { key: "tomtom-ev", label: "TomTom EV Stations", input: { geojson: JSON.stringify({ type: "FeatureCollection", features: [ { type: "Feature", properties: { name: "Fastned Amsterdam Arena", power_kw: 300, connectors: 8, operator: "Fastned", status: "Available" }, geometry: { type: "Point", coordinates: [4.9415, 52.3137] } }, { type: "Feature", properties: { name: "Shell Recharge Centrum", power_kw: 50, connectors: 4, operator: "Shell Recharge", status: "Available" }, geometry: { type: "Point", coordinates: [4.8936, 52.3702] } }, { type: "Feature", properties: { name: "Allego Zuidas", power_kw: 150, connectors: 6, operator: "Allego", status: "In Use" }, geometry: { type: "Point", coordinates: [4.8757, 52.3387] } }, { type: "Feature", properties: { name: "EVBox Museumplein", power_kw: 22, connectors: 2, operator: "EVBox", status: "Available" }, geometry: { type: "Point", coordinates: [4.8789, 52.3579] } }, { type: "Feature", properties: { name: "Fastned A10 West", power_kw: 300, connectors: 10, operator: "Fastned", status: "Available" }, geometry: { type: "Point", coordinates: [4.8351, 52.3536] } }, { type: "Feature", properties: { name: "Shell Recharge Sloterdijk", power_kw: 175, connectors: 6, operator: "Shell Recharge", status: "Available" }, geometry: { type: "Point", coordinates: [4.8358, 52.3893] } }, { type: "Feature", properties: { name: "Allego Centraal Station", power_kw: 50, connectors: 3, operator: "Allego", status: "Occupied" }, geometry: { type: "Point", coordinates: [4.8997, 52.3791] } }, { type: "Feature", properties: { name: "EVBox Vondelpark", power_kw: 22, connectors: 2, operator: "EVBox", status: "Available" }, geometry: { type: "Point", coordinates: [4.8679, 52.3607] } }, { type: "Feature", properties: { name: "Fastned Amstel", power_kw: 300, connectors: 12, operator: "Fastned", status: "Available" }, geometry: { type: "Point", coordinates: [4.9268, 52.3442] } }, { type: "Feature", properties: { name: "Allego NDSM Wharf", power_kw: 150, connectors: 5, operator: "Allego", status: "Available" }, geometry: { type: "Point", coordinates: [4.8936, 52.4012] } }, { type: "Feature", properties: { name: "Shell Recharge Oost", power_kw: 50, connectors: 4, operator: "Shell Recharge", status: "In Use" }, geometry: { type: "Point", coordinates: [4.9395, 52.3611] } }, { type: "Feature", properties: { name: "EVBox Jordaan", power_kw: 11, connectors: 1, operator: "EVBox", status: "Available" }, geometry: { type: "Point", coordinates: [4.8814, 52.3759] } }, ], }), layers: [ { type: "markers", color_property: "power_kw", size_property: "connectors", color_scale: ["#22c55e", "#ef4444"], label_property: "name", popup_fields: ["name", "power_kw", "connectors", "operator", "status"], }, ], title: "EV Charging Stations \u2014 Amsterdam (TomTom Data)", show_ui: true, }, }, { key: "reverse-geocode", label: "Auto Address", input: { geojson: JSON.stringify({ type: "FeatureCollection", features: [ { type: "Feature", properties: { id: 1, label: "Eiffel Tower area" }, geometry: { type: "Point", coordinates: [2.2945, 48.8584] } }, { type: "Feature", properties: { id: 2, label: "Colosseum area" }, geometry: { type: "Point", coordinates: [12.4924, 41.8902] } }, { type: "Feature", properties: { id: 3, label: "Big Ben area" }, geometry: { type: "Point", coordinates: [-0.1246, 51.5007] } }, { type: "Feature", properties: { id: 4, label: "Brandenburg Gate area" }, geometry: { type: "Point", coordinates: [13.3777, 52.5163] } }, { type: "Feature", properties: { id: 5, label: "Sagrada Familia area" }, geometry: { type: "Point", coordinates: [2.1744, 41.4036] } }, { type: "Feature", properties: { id: 6, label: "Dam Square area" }, geometry: { type: "Point", coordinates: [4.8952, 52.3730] } }, ], }), layers: [ { type: "markers", label_property: "label", popup_fields: ["label", "id"], }, ], title: "European Landmarks \u2014 Click for TomTom Address Enrichment", show_ui: true, }, }, { key: "clusters", label: "Clusters", input: { data_url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson", layers: [ { type: "clusters", label_property: "title", popup_fields: ["title", "mag", "place", "type", "time"], }, ], title: "USGS Earthquakes \u2014 Past 7 Days", show_ui: true, }, }, { key: "heatmap", label: "Heatmap", input: { data_url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson", layers: [ { type: "heatmap", heatmap_weight: "mag", heatmap_intensity: 0.8, }, ], title: "Earthquake Density \u2014 Past 30 Days", show_ui: true, }, }, { key: "markers", label: "Markers", input: { data_url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/significant_month.geojson", layers: [ { type: "markers", color_property: "mag", size_property: "mag", color_scale: ["#22c55e", "#ef4444"], label_property: "title", popup_fields: ["title", "mag", "place", "tsunami", "time"], }, ], title: "Significant Earthquakes \u2014 Past 30 Days", show_ui: true, }, }, { key: "lines", label: "Lines", input: { geojson: JSON.stringify({ type: "FeatureCollection", features: [ { type: "Feature", properties: { name: "Amsterdam \u2192 Berlin", mode: "driving", distance_km: 660 }, geometry: { type: "LineString", coordinates: [[4.89, 52.37], [5.47, 52.23], [6.58, 52.44], [7.47, 52.28], [8.68, 52.38], [9.99, 52.37], [11.63, 52.13], [13.41, 52.52]], }, }, { type: "Feature", properties: { name: "Paris \u2192 Lyon", mode: "driving", distance_km: 465 }, geometry: { type: "LineString", coordinates: [[2.35, 48.86], [2.76, 48.58], [3.08, 47.98], [3.85, 46.78], [4.35, 45.94], [4.83, 45.76]], }, }, ], }), layers: [ { type: "line", line_width: 3, label_property: "name", popup_fields: ["name", "mode", "distance_km"], }, ], title: "European Driving Routes", show_ui: true, }, }, { key: "choropleth", label: "Choropleth", input: { data_url: "https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json", layers: [ { type: "choropleth", color_property: "density", color_scale: ["#ffffcc", "#800026"], label_property: "name", popup_fields: ["name", "density"], }, ], title: "US Population Density by State", show_ui: true, }, }, { key: "multi-layer", label: "Multi-Layer", input: { data_url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson", layers: [ { type: "heatmap", heatmap_weight: "mag", heatmap_intensity: 0.5, }, { type: "markers", color_property: "mag", color_scale: ["#3b82f6", "#ef4444"], size_property: "mag", label_property: "title", popup_fields: ["title", "mag", "place", "time"], }, ], title: "Earthquakes \u2014 Heatmap + Markers Overlay", show_ui: true, }, }, ]; function getExampleInput(toolName: string, isOrbis: boolean): string { if (toolName === "tomtom-data-viz" && DATA_VIZ_PRESETS.length > 0) { return JSON.stringify(DATA_VIZ_PRESETS[0].input, null, 2); } const examples = isOrbis ? ORBIS_EXAMPLE_INPUTS : GENESIS_EXAMPLE_INPUTS; const example = examples[toolName]; if (example) return JSON.stringify(example, null, 2); return "{}"; } // ─── Helpers ───────────────────────────────────────────────────────────── function isToolVisibleToModel(tool: { _meta?: Record }): boolean { const result = McpUiToolMetaSchema.safeParse(tool._meta?.ui); if (!result.success) return true; const visibility = result.data.visibility; if (!visibility) return true; return visibility.includes("model"); } function compareTools(a: Tool, b: Tool): number { const aHasUi = !!getToolUiResourceUri(a); const bHasUi = !!getToolUiResourceUri(b); if (aHasUi && !bHasUi) return -1; if (!aHasUi && bHasUi) return 1; return a.name.localeCompare(b.name); } function stripPrefix(name: string): string { return name.replace(/^tomtom-/, ""); } // ─── SVG Icons (inline, no deps) ───────────────────────────────────────── const Icons = { map: ( ), tool: ( ), search: ( ), play: ( ), sun: ( ), moon: ( ), clock: ( ), check: ( ), alert: ( ), server: ( ), }; // ─── App Iframe ────────────────────────────────────────────────────────── function AppIFrame({ toolCallInfo }: { toolCallInfo: Required }) { const iframeRef = useRef(null); useEffect(() => { const iframe = iframeRef.current!; toolCallInfo.appResourcePromise.then(({ csp, permissions }) => { loadSandboxProxy(iframe, csp, permissions).then((firstTime) => { if (firstTime) { const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe); initializeApp(iframe, appBridge, toolCallInfo); } }); }); }, [toolCallInfo]); return (