fix(examples): improve updateModelContext with structured YAML frontmatter (#271)

* map-server: fix async/await in ontoolresult, remove unused getViewStorageKey

* fix(map-server): improve updateModelContext and fix initialization

- Use structured YAML frontmatter format for model context (like pdf-server)
- Add tool name, altitude, and cleaner data format
- Fix initialization deadlock: app.connect() was inside ontoolresult handler
  which could never fire until connected
- Fix view state persistence: widgetUUID now properly extracted from
  ontoolresult and used to restore persisted camera position
- Server now includes widgetUUID in tool result _meta for widget identification

* fix(pdf-server): use widgetUUID for page persistence

- Add widgetUUID to tool result _meta for widget identification
- Use widgetUUID instead of generated key (pdfUrl+toolId) for localStorage
- Enables reliable page restoration when revisiting PDF widgets

* fix(transcript-server): use structured YAML frontmatter for model context

- Add tool name, listening status, and unsent entry count in frontmatter
- Consistent format with pdf-server and map-server

* update names

* drop bogus altitude

* Update mcp-app.ts
This commit is contained in:
Olivier Chafik 2026-01-14 22:08:09 +00:00 • committed by GitHub
commit 75ecd62384
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -71,6 +71,8 @@ let persistViewTimer: ReturnType<typeof setTimeout> | null = null;
// Track whether tool input has been received (to know if we should restore persisted state)
let hasReceivedToolInput = false;
let widgetUUID: string | undefined = undefined;
/**
* Persisted camera state for localStorage
*/
@ -83,18 +85,6 @@ interface PersistedCameraState {
roll: number; // radians
}
/**
* Get localStorage key for persisting view state
* Uses toolInfo.id (tool invocation ID) - localStorage is scoped per conversation per server,
* so each tool call remembers its own view state within the conversation.
*/
function getViewStorageKey(): string | null {
const context = app.getHostContext();
const toolId = context?.toolInfo?.id;
if (!toolId) return null;
return `cesium-view:${toolId}`;
}
/**
* Get current camera state for persistence
*/
@ -132,8 +122,7 @@ function schedulePersistViewState(cesiumViewer: any): void {
* Persist current view state to localStorage
*/
function persistViewState(cesiumViewer: any): void {
const key = getViewStorageKey();
if (!key) {
if (!widgetUUID) {
log.info("No storage key available, skipping view persistence");
return;
}
@ -142,13 +131,9 @@ function persistViewState(cesiumViewer: any): void {
if (!state) return;
try {
localStorage.setItem(key, JSON.stringify(state));
log.info(
"Persisted view state:",
key,
state.latitude.toFixed(2),
state.longitude.toFixed(2),
);
const value = JSON.stringify(state);
localStorage.setItem(widgetUUID, value);
log.info("Persisted view state:", widgetUUID, value);
} catch (e) {
log.warn("Failed to persist view state:", e);
}
@ -158,12 +143,14 @@ function persistViewState(cesiumViewer: any): void {
* Load persisted view state from localStorage
*/
function loadPersistedViewState(): PersistedCameraState | null {
const key = getViewStorageKey();
if (!key) return null;
if (!widgetUUID) return null;
try {
const stored = localStorage.getItem(key);
if (!stored) return null;
const stored = localStorage.getItem(widgetUUID);
if (!stored) {
console.info("No persisted view state found");
return null;
}
const state = JSON.parse(stored) as PersistedCameraState;
// Basic validation
@ -175,6 +162,7 @@ function loadPersistedViewState(): PersistedCameraState | null {
log.warn("Invalid persisted view state, ignoring");
return null;
}
log.info("Loaded persisted view state:", state);
return state;
} catch (e) {
log.warn("Failed to load persisted view state:", e);
@ -402,8 +390,10 @@ async function getVisiblePlaces(extent: BoundingBox): Promise<string[]> {
}
/**
* Debounced location update using multi-point reverse geocoding
* Samples multiple points in the visible extent to discover places
* Debounced location update using multi-point reverse geocoding.
* Samples multiple points in the visible extent to discover places.
*
* Updates model context with structured YAML frontmatter (similar to pdf-server).
*/
function scheduleLocationUpdate(cesiumViewer: any): void {
if (reverseGeocodeTimer) {
@ -420,34 +410,35 @@ function scheduleLocationUpdate(cesiumViewer: any): void {
}
const { widthKm, heightKm } = getScaleDimensions(extent);
const extentInfo =
`Extent: [${extent.west.toFixed(4)}, ${extent.south.toFixed(4)}, ` +
`${extent.east.toFixed(4)}, ${extent.north.toFixed(4)}] ` +
`(${widthKm.toFixed(1)}km × ${heightKm.toFixed(1)}km)`;
log.info(extentInfo);
log.info(`Extent: ${widthKm.toFixed(1)}km × ${heightKm.toFixed(1)}km`);
// Get places visible in the extent (samples multiple points for large areas)
const places = await getVisiblePlaces(extent);
const placesText =
places.length > 0 ? `Visible places: ${places.join(", ")}` : "";
if (places.length > 0 || center) {
const centerText = center
? `Center: ${center.lat.toFixed(4)}, ${center.lon.toFixed(4)}`
: "";
// Build structured markdown with YAML frontmatter (like pdf-server)
// Note: tool name isn't in the notification protocol, so we hardcode it
const frontmatter = [
"---",
`tool: show-map`,
center
? `center: [${center.lat.toFixed(4)}, ${center.lon.toFixed(4)}]`
: null,
`extent: [${extent.west.toFixed(4)}, ${extent.south.toFixed(4)}, ${extent.east.toFixed(4)}, ${extent.north.toFixed(4)}]`,
`extent-size: ${widthKm.toFixed(1)}km × ${heightKm.toFixed(1)}km`,
places.length > 0 ? `visible-places: [${places.join(", ")}]` : null,
"---",
]
.filter(Boolean)
.join("\n");
const contextText = [placesText, centerText, extentInfo]
.filter(Boolean)
.join("\n");
log.info("Updating model context:", frontmatter);
log.info("Updating model context:", contextText);
// Update the model's context with the current map location.
// If the host doesn't support this, the request will silently fail.
app.updateModelContext({
content: [{ type: "text", text: contextText }],
});
}
// Update the model's context with the current map location.
// If the host doesn't support this, the request will silently fail.
app.updateModelContext({
content: [{ type: "text", text: frontmatter }],
});
}, 1500);
}
@ -947,40 +938,36 @@ app.ontoolinput = async (params) => {
// },
// );
// Handle tool result - extract widgetUUID and restore persisted view if available
app.ontoolresult = async (result) => {
widgetUUID = result._meta?.widgetUUID
? String(result._meta.widgetUUID)
: undefined;
log.info("Tool result received, widgetUUID:", widgetUUID);
// Now that we have widgetUUID, try to restore persisted view
// This overrides the tool input position if a saved state exists
if (viewer && widgetUUID) {
const restored = restorePersistedView(viewer);
if (restored) {
log.info("Restored persisted view from tool result handler");
await waitForTilesLoaded(viewer);
hideLoading();
}
}
};
// Initialize Cesium and connect to host
async function init() {
async function initialize() {
try {
log.info("Loading CesiumJS from CDN...");
await loadCesium();
log.info("CesiumJS loaded successfully");
viewer = await initCesium();
// Don't hide loading here - we wait for tool input to position camera
// and for tiles to load before hiding the loading indicator
log.info("CesiumJS initialized, waiting for tool input...");
log.info("CesiumJS initialized");
// Fallback: if no tool input received within 2 seconds, try restoring
// persisted view or show default view
setTimeout(async () => {
const loadingEl = document.getElementById("loading");
if (
loadingEl &&
loadingEl.style.display !== "none" &&
!hasReceivedToolInput
) {
// No explicit tool input - try to restore persisted view
const restored = restorePersistedView(viewer!);
if (restored) {
log.info("Restored persisted view, waiting for tiles...");
} else {
log.info("No persisted view, using default view...");
}
await waitForTilesLoaded(viewer!);
hideLoading();
}
}, 2000);
// Connect to host (auto-creates PostMessageTransport)
// Connect to host (must happen before we can receive notifications)
await app.connect();
log.info("Connected to host");
@ -1009,6 +996,26 @@ async function init() {
// Set up keyboard shortcuts for fullscreen (Escape to exit, Ctrl/Cmd+Enter to toggle)
document.addEventListener("keydown", handleFullscreenKeyboard);
// Wait a bit for tool input, then try restoring persisted view or show default
setTimeout(async () => {
const loadingEl = document.getElementById("loading");
if (
loadingEl &&
loadingEl.style.display !== "none" &&
!hasReceivedToolInput
) {
// No explicit tool input - try to restore persisted view
const restored = restorePersistedView(viewer!);
if (restored) {
log.info("Restored persisted view, waiting for tiles...");
} else {
log.info("No persisted view, using default view...");
}
await waitForTilesLoaded(viewer!);
hideLoading();
}
}, 500);
} catch (error) {
log.error("Failed to initialize:", error);
const loadingEl = document.getElementById("loading");
@ -1019,4 +1026,5 @@ async function init() {
}
}
init();
// Start initialization
initialize();