Skip to content

feat: preview_style_tool and style_comparison_tool no longer require a token - #136

Merged
mattpodwysocki merged 2 commits into
feat/consolidate-preview-ui-resourcesfrom
feat/preview-style-inline-no-elicitation
Aug 24, 2026
Merged

feat: preview_style_tool and style_comparison_tool no longer require a token#136
mattpodwysocki merged 2 commits into
feat/consolidate-preview-ui-resourcesfrom
feat/preview-style-inline-no-elicitation

Conversation

@mattpodwysocki

Copy link
Copy Markdown
Contributor

What changed

Stacked on #135 (the preview-resource consolidation). preview_style_tool and style_comparison_tool no longer require an existing accessToken — both previously made it 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 look.

accessToken is now optional on both. Omit it and the tool auto-generates a preview token from the server's own access token; pass share: true alongside your own accessToken when you actually want a link built from a token you manage yourself (for something durable/shareable). share: true without accessToken is a clear validation error, not a silent fallback.

Why

This came out of a Slack thread about the still-open elicitation PR (#57 — asks the user which token to use/create/paste, with a whole SEP-1036-compliant local-HTTP-server flow for securely collecting a pasted token). The question was whether we could avoid needing that whole flow for the common case. The answer: yes, for inline previews — elicitation is only genuinely needed when the goal is a durable, shareable link (which is a real, separate feature these tools already promise elsewhere — see README/docs/STYLE_BUILDER.md/two prompts — not something this PR removes). #57 stays exactly as valuable as before for that share: true case; this PR just means the common "show me this now" case doesn't have to pay that cost.

A real bug caught before shipping

The two tools' downstream consumers turned out to need different token shapes, confirmed live against the real API rather than assumed:

  • preview_style_tool's embeddable Styles API HTML preview page and GL JS both accept a short-lived tk.* token fine (the same kind geojson_preview_tool's resource already auto-mints).
  • style_comparison_tool's comparison link points at agent.mapbox.com/tools/style-compare, which validates the token prefix client-side and hard-rejects anything but pk.* — a tk.* token there 400s with a Configuration Error: Invalid token type page. Caught this by actually loading the real returned URL in a browser, not just checking isError: false from the tool call.

So style_comparison_tool's auto-mint path requests a genuine, non-expiring pk.* token instead (confirmed: omitting the Tokens API's expires field is what makes it return "usage":"pk" rather than a temporary token) — still narrowly scoped to styles:tiles/styles:read/fonts:read, just not self-expiring. Extracted the minting logic into a shared mintScopedPreviewToken utility (src/utils/mintScopedPreviewToken.ts) — used by both tools now, and MapPreviewUIResource refactored onto it too rather than keeping its own third copy of the same POST-to-tokens/v2 logic.

Verification

  • Live end-to-end in a real browser for both tools: loaded preview_style_tool's auto-minted URL and saw the real custom style render; loaded style_comparison_tool's and saw the real before/after swipe comparison render.
  • New unit tests for the auto-mint path on both tools (success, share: true without a token, an explicit token overriding auto-mint even with share: true, no server token to mint from, mint failure) and for the shared mintScopedPreviewToken utility directly (scopes/note/expiry in the request body, the expiresInMs: null no-expiry path, the AGI-905-style cross-account check, error propagation).
  • Both tools now take httpRequest as a constructor dependency, matching every other network-calling tool in this repo (toolRegistry.ts/tools/index.ts updated accordingly).
  • npx tsc -p tsconfig.src.json, npx eslint: clean (0 errors; pre-existing warnings elsewhere untouched).
  • Full suite passes (630/630); updated the tool-description snapshot test for the two changed descriptions.

🤖 Generated with Claude Code

…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>
@mattpodwysocki
mattpodwysocki requested a review from a team as a code owner August 21, 2026 15:43
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>
@mattpodwysocki
mattpodwysocki merged commit edc614d into feat/consolidate-preview-ui-resources Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants