map-server: interactive annotations (markers, routes, areas, circles)#505
Open
map-server: interactive annotations (markers, routes, areas, circles)#505
Conversation
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-preact
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-solid
@modelcontextprotocol/server-basic-svelte
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-basic-vue
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-debug
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
Add semi-transparent dark background behind labels, use bold font, thicker outline, and disable depth test so labels are always visible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change add_marker to add_markers accepting an array of markers - Assign UUIDs to each marker and return them in the tool result - Add update_markers and remove_markers actions to the interact tool - Track markers by ID with Cesium entity references for edit/removal - Add copy button (clipboard icon) that appears when markers exist - Multi-mime clipboard: text/plain gets Markdown table + GeoJSON code block, text/html gets an HTML table with GeoJSON in a details block Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Geocode tool now accepts `queries` (string array) and performs batched lookups (respecting Nominatim rate limit between each) - Show-map accepts initial `markers` array — IDs are assigned server-side and returned in structuredContent - Show-map supports center+radius mode: `latitude`/`longitude` with optional `radiusKm` (default 50) as alternative to bounding box - App handles initial markers from both ontoolinput and ontoolresult Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
structuredContent is only seen by the model when content is empty (per MCP spec). Move initial marker data to _meta for the app, and include marker IDs in the content text for the model. Also remove structuredContent from add_markers (IDs already in text). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Marker id is now required and chosen by the caller on show-map (initial markers), add_markers, and update_markers. Removes sequential counter and server-side ID allocation — simpler and produces shorter, more meaningful IDs in the conversation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Cesium's built-in label with canvas-rendered billboard images:
- Proper vertical text alignment (textBaseline: middle)
- Rounded rectangle background (quadraticCurveTo corners)
- System font stack, DPR-aware sizing
- Dark semi-transparent background (rgba 30,30,30,0.78)
Persist markers to localStorage keyed by `${viewUUID}:markers`.
Restored on reconnect before adding any new initial markers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The model already knows the IDs it picked — no need to repeat them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Split marker into two Cesium entities (point + billboard label) to avoid point/billboard conflict that suppressed labels entirely - Remove heightReference: CLAMP_TO_GROUND (no terrain loaded, caused 3D offset on tilted views) - Use canvas roundRect() for cleaner rounded corners - Use actualBoundingBoxAscent/Descent for precise text centering - Add hint in show-map description: skip markers for single location - Update/remove now correctly handles both entities Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Markers from ontoolinput are added before viewUUID is available, so persistMarkers() was a no-op. Now explicitly persist after ontoolresult finishes setting up viewUUID and all markers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move persistMarkers() out of addMarker/updateMarker/removeMarker into processCommands (once after the entire batch). The ontoolresult handler already has its own explicit persistMarkers() call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The app runs in a sandboxed iframe — Clipboard API requires the host to set allow="clipboard-write". Declare it via permissions metadata. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace marker-specific types with a discriminated union (oneOf in JSON Schema) supporting four annotation types: marker, route, area, circle. - z.discriminatedUnion for proper oneOf in tool schemas - Separate full schemas (show-map/add) and update schemas (partial fields) - Unified add/update/remove actions replacing add_markers/update_markers/remove_markers - Cesium rendering: point (marker), polyline (route), polygon (area), ellipse (circle) - Canvas label billboards for all types at appropriate anchor points - ontoolinputpartial streaming: render all-but-last annotation progressively - Persistence and clipboard export updated for all annotation types - GeoJSON export: Point/LineString/Polygon with type-specific properties Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Annotations were added after `await waitForTilesLoaded()` which blocks up to 10 seconds. If the host doesn't forward `_meta.initialAnnotations` to ontoolresult, annotations wouldn't appear until tiles finish loading. Now annotations are added immediately after camera positioning, before the tile-loading await. Also adds field validation in ontoolinputpartial to prevent creating broken entities from truncated streaming data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The camera only moved after the full tool input was received. Now ontoolinputpartial positions the camera as soon as bbox/center fields are available (once), so markers appear in the right location during progressive streaming. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If the camera was positioned during streaming (ontoolinputpartial), skip re-positioning in ontoolinput. This preserves the user's view if they panned or zoomed while annotations were streaming in. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ullscreen Use a CustomDataSource with CesiumJS EntityCluster to automatically cluster nearby markers/labels instead of overlapping them. Clusters show a blue circle badge with the count. Also change the fullscreen keyboard shortcut from Ctrl/Cmd+Enter to Alt+Enter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CesiumJS may not cluster newly-added entities until the camera moves. Work around this by toggling clustering.enabled off/on after each batch of annotation additions (debounced to a single microtask). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each marker was creating separate point and label entities in the clustered data source, causing double-counting in clusters. Now markers use a single entity with both point and billboard properties, so each marker counts as one for clustering. Routes, areas, and circles are moved back to viewer.entities since their geometry can't be clustered. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clustering fix: switch marker labels from custom billboard images to Cesium's native LabelGraphics. EntityCluster hides point+label together when clustered; it did NOT hide the billboard, causing all labels to stack on top of the cluster badge. New floating annotation panel (inspired by pdf-server's panel): - Toggle button with count badge in the toolbar - Lists all annotations (markers, routes, areas, circles) with color swatch, type, and label - Per-row actions: eye icon (hide/show) and trash icon (delete) - Click a card to select + fly to the annotation; camera target is shifted horizontally to avoid the annotation landing behind the panel - Prev/next arrows in the footer for sequential navigation - Details expand to show coordinates + optional markdown description (supports **bold**, *italic*, `code`, [links](url), - bullets) - Drag panel header to snap to any corner - Clicking an entity on the map selects it in the panel - Visibility is persisted to localStorage alongside annotation defs Server schema: added optional `description` (markdown) field to all annotation types and their update variants. Also: disabled Cesium's built-in selectionIndicator + infoBox since selection is now handled by the panel.
Addressing UX feedback: Panel layout: - Eye icon moved before the color swatch (leading action) - Removed type labels (MARKER/ROUTE/etc); just show the name - Removed coordinates from details (kept in MD/GeoJSON export) - Master eye in header: hide all / show all Interactions: - ↑/↓ keyboard arrows navigate selection (wraps at edges) - Multi-select: cmd/ctrl-click to toggle, shift-click for range - Fly-to now fits the combined bbox of the whole selection - Markdown links call app.openLink (iframe sandbox blocks navigation) - Escape closes the panel Styling: - Light/dark theming via applyDocumentTheme + light-dark() CSS vars - Disabled Cesium's built-in selection indicator + info box - Less aggressive clustering: pixelRange 25 (was 60), minClusterSize 3 Map: - Markers now render as pin-shaped billboards (teardrop with hole) instead of plain dots; pin tip anchors to the coordinate - Labels sit above the pin (-36px offset) Server: - Added sample Paris annotations as zod .default() so the basic-host pre-fills a useful demo payload
…g hysteresis
Visibility:
- Global eye is now a true override (globalVisible flag): toggling
it off→on restores each item's individual eye state exactly.
Effective visibility = globalVisible && tracked.visible.
- Space key cycles through: hide all selected → show all selected →
revert each to its pre-cycle state. Snapshot cleared when selection
changes.
- Backspace/Delete removes all selected items.
Panel:
- Default anchor changed to bottom-right.
- Auto-fit: initial view flies to frame all annotations (unless a
persisted camera position exists).
Persistence (diff-based):
- Store only the diff from the tool's baseline annotations:
{removed, hidden, added, selected}. Much smaller than full list
for large initial sets, and the stored data is human-readable.
- Baseline = args.annotations from ontoolinput + _meta.initialAnnotations
from ontoolresult. Diff applied AFTER baseline is loaded on restore.
- Selection is persisted too (restored without flying).
Model context:
- Include annotation diff summary: 'Annotations: N total (X added,
Y removed, Z hidden)' so the model knows what the user has changed.
Clustering:
- Force recluster on camera moveEnd to fix hysteresis (clusters
forming at pixelRange on zoom-out but needing much more zoom-in
to break apart).
…ace cycle Export: - New download button (saves .md file with date-stamped name) - Markdown export now includes per-annotation description sections after the summary table (multi-line markdown renders properly) - GeoJSON export includes description in feature.properties - Refactored copy/download to share exportText() builder Space key: - When all selected items share the same visibility, Space is a simple 2-state toggle (no snapshot needed since revert == original) - Only enters the 3-state cycle (snapshot + hide → show → revert) when the selection is mixed
Selecting a hidden annotation should still navigate to where it would appear, so the user can find and re-enable it. Dropped the visibility filter from flyToSelection(). Also added a comment documenting the route-endpoint clustering quirk: routes always draw to actual coordinates, so when an endpoint marker is clustered the line visually detaches from the cluster badge. This is geographically correct; a future refinement could exempt route waypoint markers from clustering.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds full interactivity and rich annotation support to the CesiumJS map server:
oneOfin JSON Schema) supporting marker, route (polyline), area (polygon), and circle (ellipse) annotation typesnavigate,add,update,removeactions via command queue with 200ms batchingontoolinputpartialprogressively renders annotations as the model streams them (skipping last potentially-truncated item)Annotation types
markerrouteareacircleJSON Schema
Uses
z.discriminatedUnion("type", [...])which produces properoneOfwithconstdiscriminator values. Separate full schemas (foradd) and update schemas (partial fields, forupdate).Test plan
show-mapwith no annotations → globe loads at default/bbox locationshow-mapwith mixed annotations → markers, routes, areas, circles render correctlyinteractadd/update/remove → annotations appear/change/disappearontoolinputpartial) → annotations render progressively🤖 Generated with Claude Code