feat: consolidate GeoJSON preview and style preview into one MCP App - #135
Merged
Conversation
GeojsonPreviewUIResource and PreviewStyleUIResource were two nearly identical hand-written MCP Apps templates — the same postMessage handshake, fullscreen/open-link controls, and resize handling copy-pasted across ~650 lines, differing only in what they drew on the map once a tool result arrived. Merged into MapPreviewUIResource, which dispatches on the shape of the incoming tool-result URL (a geojson.io data URL vs. a Styles API .html preview URL) instead of each resource assuming a fixed mode. geojson_preview_tool and preview_style_tool now both point at 'ui://mapbox/map-preview/index.html'. Neither tool's own input/output contract changed — this only touches which resource renders their result. The GeoJSON path keeps its existing eager map bootstrap (drawing Standard immediately so an overlay has something to render onto right away); the style-preview path reuses that map via setStyle() when it exists, falling back to constructing its own when it doesn't (e.g. no server-mintable token yet). style_comparison_tool's StyleComparisonUIResource stays separate — it renders two synced map instances under a swipe/compare slider, a genuinely different UI shape from "one map, different content." Breaking change to the public resources export: previewStyleUI/ geojsonPreviewUI are replaced by mapPreviewUI. Migrated the existing AGI-905 cross-account token-leak regression suite onto the merged resource unchanged, and added new coverage for the merged dispatch logic (geojson vs. style-preview routing, the setStyle()-reuse vs. fresh-map paths, and the unrecognized-URL error case) via a node:vm sandbox running the actual embedded script, mirroring the pattern already used for this in the sibling mcp-server repo. Verified live against the real Mapbox API in a real browser: both the GeoJSON overlay path and the style-swap-in-place path (loading a real custom style by name) work end-to-end on the merged resource. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…a token Both previously made accessToken a required pk.* input, so a caller had to already have (or separately go create) a public token before either tool would do anything, even for a one-off inline preview. accessToken is now optional on both: when omitted, the tool auto-generates a preview token from the server's own access token via a new shared mintScopedPreviewToken utility (the same tokens/v2 minting pattern MapPreviewUIResource already used for geojson_preview_tool, extracted so it's not duplicated a third time), so a first call needs no setup at all. Pass share: true alongside your own accessToken to get a link built from a token you manage yourself, for when something durable/shareable is actually wanted. share: true without accessToken is a clear validation error, not a silent fallback. The two tools' auto-mint tokens differ in shape, confirmed live against the real API rather than assumed: preview_style_tool mints a short-lived (~1h) tk.* token, since the Styles API's embeddable HTML preview page and GL JS both accept it. style_comparison_tool mints a non-expiring pk.* token instead, because agent.mapbox.com/tools/style-compare (which its comparison link embeds) validates the token prefix client-side and hard-rejects anything but pk.* — a tk.* token 400s there with "Configuration Error: Invalid token type", caught by loading the real returned URL in a browser before shipping this. The pk.* token stays narrowly scoped but doesn't self-expire; it persists on the account until manually revoked. Both tools now take httpRequest as a constructor dependency, matching every other network-calling tool in this repo. Verified live end-to-end in a real browser for both tools: a real custom style loaded via preview_style_tool's auto-minted token, and a real before/after comparison rendered via style_comparison_tool's. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per README.md, the hosted MCP endpoint authenticates each request with its
own access token rather than a personal Mapbox account token, and that
token isn't granted tokens:write — so the auto-mint path added in the
previous commit can never succeed there. Without this, a hosted caller
omitting accessToken would hit a raw, misleading jwtUtils error
("MAPBOX_ACCESS_TOKEN is not in valid JWT format", naming an env var the
hosted deployment doesn't use) or a bare "Token API 403" with no next step.
Added describeAutoMintFailure() (src/utils/mintScopedPreviewToken.ts),
used by both preview_style_tool and style_comparison_tool's auto-mint
catch blocks, which recognizes both failure shapes (a server token that
doesn't parse as a Mapbox token at all, or one that parses fine but lacks
tokens:write) and rewrites them into a message that actually tells the
caller what to do: pass accessToken directly via list_tokens_tool/
create_token_tool. Anything else (e.g. a transient 5xx) passes through
unchanged rather than being guessed at.
Verified live via a real MCP client with a non-Mapbox-shaped bearer token
(simulating the hosted endpoint's auth shape) that the new message is
what actually gets returned end-to-end, not just in unit tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Valiunia
previously approved these changes
Aug 24, 2026
…itation feat: preview_style_tool and style_comparison_tool no longer require a token
Valiunia
approved these changes
Aug 24, 2026
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.
What changed
Consolidates
GeojsonPreviewUIResourceandPreviewStyleUIResourceinto oneMapPreviewUIResource.geojson_preview_toolandpreview_style_toolnow both point their_meta.ui.resourceUriatui://mapbox/map-preview/index.html. Neither tool's own input/output contract changed — this only touches which resource renders their result.Why
Raised in Slack: three of DevKit's tools each render a live GL JS map (GeoJSON preview, style preview, style comparison), each with its own hand-written MCP Apps template. On inspection, two of the three (
GeojsonPreviewUIResource397 lines,PreviewStyleUIResource268 lines) were genuinely the same thing done twice — the MCP-Apps postMessage handshake, fullscreen/open-link controls, and resize handling were copy-pasted byte-for-byte, and they differed only in what got drawn on the map once a tool result arrived (a GeoJSON overlay on the default Standard style, vs. swapping to an arbitrary preview style).style_comparison_tool'sStyleComparisonUIResourcestays separate on purpose — it renders two syncedmapboxgl.Mapinstances under a swipe/compare slider (via themapboxgl.Compareplugin), a genuinely different UI shape from "one map, different content," not just a third mode to fold in.How the merged resource works
The shared template dispatches on the shape of the tool-result URL it receives, rather than each resource assuming a fixed mode:
geojson.io/?data=data:application/json,...URL (fromgeojson_preview_tool) → draws a GeoJSON overlay on the default Standard style..../styles/v1/{user}/{style}.html?access_token=...URL (frompreview_style_tool) → swaps to that style.The GeoJSON path keeps its existing eager bootstrap: it draws the default Standard style immediately (using a token minted server-side from the caller's
sk.*, same as before) so an overlay has something to render onto as soon as data arrives, rather than waiting. The style-preview path now reuses that same map instance viasetStyle()when it exists, swappingmapboxgl.accessTokento the token embedded in its own URL first — falling back to constructing a freshmapboxgl.Map(asPreviewStyleUIResourcealways did) only when no eager map exists (e.g. nosk.*/pk.*was available to mint from).Breaking change
@mapbox/mcp-devkit-server/resources:previewStyleUI/geojsonPreviewUInamed exports and theGeojsonPreviewUIResource/PreviewStyleUIResourceclasses are replaced bymapPreviewUI/MapPreviewUIResource.Verification
GeojsonPreviewUIResource.test.tsAGI-905 cross-account token-leak regression suite onto the merged resource unchanged (same assertions, same scenarios) — this is a security-sensitive test suite and I didn't want to weaken it while moving it.setStyle()-reuse path vs. the fresh-map fallback, and the unrecognized-URL error case — via anode:vmsandbox running the actual embedded<script>, mirroring the pattern the siblingmcp-serverrepo already uses for this kind of test.postMessagehandler (bootstrapped map → overlay added,fitBounds'd to SF) and, on a fresh load of the same resource, a real custom style from this account by name (bootstrapped map →setStyle()swap → style-name badge showed "SF Coffee Finder", the real style's name) — confirming both paths work end-to-end, not just against mocks.npx tsc --noEmit,npx eslint: clean (0 errors; pre-existingno-explicit-anywarnings elsewhere, untouched).🤖 Generated with Claude Code