From d5e2fd59909a3f951633b3cc284d0240b127eee1 Mon Sep 17 00:00:00 2001 From: Philip Murphy Date: Mon, 28 Sep 2026 22:58:58 +0100 Subject: [PATCH] fix(map-server): handle undefined coordinates and patch Cesium for sandboxed iframes --- examples/map-server/server.ts | 56 +++++++++---------- examples/map-server/src/mcp-app.ts | 87 ++++++++++++++++++++++++------ 2 files changed, 97 insertions(+), 46 deletions(-) diff --git a/examples/map-server/server.ts b/examples/map-server/server.ts index fe31e1e6f..346c21e3b 100644 --- a/examples/map-server/server.ts +++ b/examples/map-server/server.ts @@ -153,26 +153,10 @@ export function createServer(): McpServer { description: "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location. The widget is interactive and exposes tools for navigation (fly to locations) and querying the current view.", inputSchema: z.object({ - west: z - .number() - .optional() - .default(-0.5) - .describe("Western longitude (-180 to 180)"), - south: z - .number() - .optional() - .default(51.3) - .describe("Southern latitude (-90 to 90)"), - east: z - .number() - .optional() - .default(0.3) - .describe("Eastern longitude (-180 to 180)"), - north: z - .number() - .optional() - .default(51.7) - .describe("Northern latitude (-90 to 90)"), + west: z.number().optional().describe("Western longitude (-180 to 180)"), + south: z.number().optional().describe("Southern latitude (-90 to 90)"), + east: z.number().optional().describe("Eastern longitude (-180 to 180)"), + north: z.number().optional().describe("Northern latitude (-90 to 90)"), label: z .string() .optional() @@ -180,17 +164,29 @@ export function createServer(): McpServer { }), _meta: { ui: { resourceUri } }, }, - async ({ west, south, east, north, label }): Promise => ({ - content: [ - { - type: "text", - text: `Displaying globe at: W:${west.toFixed(4)}, S:${south.toFixed(4)}, E:${east.toFixed(4)}, N:${north.toFixed(4)}${label ? ` (${label})` : ""}`, + async ({ west, south, east, north, label }): Promise => { + // Determine what to log based on whether coordinates were provided + const hasCoords = + west !== undefined && + south !== undefined && + east !== undefined && + north !== undefined; + const text = hasCoords + ? `Displaying globe at: W:${west.toFixed(4)}, S:${south.toFixed(4)}, E:${east.toFixed(4)}, N:${north.toFixed(4)}${label ? ` (${label})` : ""}` + : `Displaying globe at default location${label ? ` (${label})` : ""}`; + + return { + content: [ + { + type: "text", + text, + }, + ], + _meta: { + viewUUID: randomUUID(), }, - ], - _meta: { - viewUUID: randomUUID(), - }, - }), + }; + }, ); // geocode tool - searches for places using Nominatim (no UI) diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts index d3090c7a4..02142e345 100644 --- a/examples/map-server/src/mcp-app.ts +++ b/examples/map-server/src/mcp-app.ts @@ -41,6 +41,38 @@ async function loadCesium(): Promise { script.onload = () => { // Set CESIUM_BASE_URL for asset loading (window as any).CESIUM_BASE_URL = CESIUM_BASE_URL; + + const CGlobal = (window as any).Cesium; + if (CGlobal?.TaskProcessor) { + // Run terrain geometry tasks synchronously on the main thread for sandboxed iframes + const origSchedule = CGlobal.TaskProcessor.prototype.scheduleTask; + CGlobal.TaskProcessor.prototype.scheduleTask = function (params: any) { + if (this._workerPath?.includes("transferTypedArrayTest")) { + return Promise.resolve(new ArrayBuffer(1)); + } + if (this._workerPath?.includes("createVerticesFromHeightmap")) { + try { + const D = { ...params }; + if (D.ellipsoid) + D.ellipsoid = CGlobal.Ellipsoid.clone(D.ellipsoid); + if (D.rectangle) + D.rectangle = CGlobal.Rectangle.clone(D.rectangle); + const r = CGlobal.HeightmapTessellator.computeVertices(D); + return Promise.resolve({ + ...r, + vertices: r.vertices.buffer, + numberOfAttributes: r.encoding.stride, + gridWidth: D.width, + gridHeight: D.height, + }); + } catch (err) { + log.error("Sync computeVertices error:", err); + } + } + return origSchedule.call(this, params); + }; + } + resolve(); }; script.onerror = () => @@ -373,6 +405,9 @@ async function initCesium(): Promise { sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false, + // Disable Knockout.js UI widgets to avoid EvalError under strict CSP without 'unsafe-eval' + infoBox: false, + selectionIndicator: false, // Disable terrain (requires Ion) terrainProvider: undefined, // WebGL context options for sandboxed iframe rendering @@ -831,11 +866,18 @@ app.ontoolinput = async (params) => { | undefined; if (args && viewer) { - // Handle both nested boundingBox and flat format + const DEFAULT_BBOX: BoundingBox = { + west: -0.5, + south: 51.3, + east: 0.3, + north: 51.7, + }; // London let bbox: BoundingBox | null = null; - + let isExplicitInput = false; + // 1. Check if model provided explicit coordinates if (args.boundingBox) { bbox = args.boundingBox; + isExplicitInput = true; } else if ( args.west !== undefined && args.south !== undefined && @@ -848,27 +890,40 @@ app.ontoolinput = async (params) => { east: args.east, north: args.north, }; + isExplicitInput = true; } - if (bbox) { - // Mark that we received explicit tool input (overrides persisted state) + if (isExplicitInput && bbox) { + // Model told us exactly where to look hasReceivedToolInput = true; - log.info("Positioning camera to bbox:", bbox); - - // Position camera instantly (no animation) + log.info("Positioning camera to explicit bbox:", bbox); setViewToBoundingBox(viewer, bbox); + } else { + // Model didn't provide coordinates (just requested to open the map). + // The View determines its own default state. + + // First try to restore persisted user state + if (restorePersistedView(viewer)) { + hasReceivedToolInput = true; // Count restoration as having an initial position + log.info("Restored persisted view on startup"); + } else { + // Fallback to the app's default home location (London) + hasReceivedToolInput = true; // We've positioned the camera + log.info("Using default app home location (London)"); + setViewToBoundingBox(viewer, DEFAULT_BBOX); + } + } - // Wait for tiles to load at this location - await waitForTilesLoaded(viewer); + // Wait for tiles to load at the new location + await waitForTilesLoaded(viewer); - // Now hide loading indicator - hideLoading(); + // Now hide loading indicator + hideLoading(); - log.info( - "Camera positioned, tiles loaded. Height:", - viewer.camera.positionCartographic.height, - ); - } + log.info( + "Camera positioned, tiles loaded. Height:", + viewer.camera.positionCartographic.height, + ); } };