Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
19cf40f
Add MCP elicitation for secure preview token handling
mattpodwysocki Jan 13, 2026
2b81c67
Fix: Ensure preview tokens are created as public tokens (pk.*)
mattpodwysocki Jan 13, 2026
0e68743
Fix: Use only public scopes to create public tokens (pk.*)
mattpodwysocki Jan 13, 2026
f51feff
Fix: Check client elicitation capability before using elicitInput()
mattpodwysocki Jan 13, 2026
ec35e38
Docs: Clarify varying MCP elicitation support across clients
mattpodwysocki Jan 13, 2026
b9a2246
Docs: Update elicitation support status for Cursor and VS Code
mattpodwysocki Jan 13, 2026
c974bcc
Docs: Add Goose elicitation bug report and documentation
mattpodwysocki Jan 13, 2026
ffdc83a
Docs: Link to filed Goose elicitation bug issue
mattpodwysocki Jan 13, 2026
08b2460
Remove redundant Goose bug report file
mattpodwysocki Jan 13, 2026
3c6f8a6
Tests: Add unit tests for elicitation and token storage
mattpodwysocki Jan 13, 2026
90cbd1b
Docs: Confirm Claude Code does not support elicitation yet
mattpodwysocki Jan 14, 2026
1e3e9e7
Add elicitation support to style_comparison_tool
mattpodwysocki Jan 15, 2026
14e54b3
Merge remote-tracking branch 'origin/main' into add-preview-token-eli…
mattpodwysocki Jul 31, 2026
19d3637
Add tk.* guard, HttpPipeline DI, and hosted-endpoint docs for token e…
mattpodwysocki Jul 31, 2026
8f9fcf2
Correct tk.* guard claims and improve the actual create-token failure…
mattpodwysocki Jul 31, 2026
9eaf79f
Add HTTP integration test for elicitation over the real MCP wire prot…
mattpodwysocki Jul 31, 2026
ae725b0
Fix race in HTTP integration test harness that caused CI flake
mattpodwysocki Jul 31, 2026
2b3fddc
Fix backwards client list in the no-elicitation-support error message
mattpodwysocki Jul 31, 2026
848963c
Address PR #57 review feedback: useCustomToken fallback and cache-key…
mattpodwysocki Aug 4, 2026
a92ebac
Fix cross-session elicitation hijack: stop relying on this.server
mattpodwysocki Aug 5, 2026
dbfcea2
Add CHANGELOG entry for cross-session elicitation hijack fix
mattpodwysocki Aug 5, 2026
8d64169
Merge remote-tracking branch 'origin/main' into add-preview-token-eli…
mattpodwysocki Aug 10, 2026
c2f49b5
Address more PR #57 review feedback: CHANGELOG framing, jwtUtils docs
mattpodwysocki Aug 12, 2026
25fc3ef
Fix resource-exhaustion gaps in elicitation flow found in offline review
mattpodwysocki Aug 12, 2026
de3a2ee
Fix two security regressions and several correctness bugs in elicitat…
mattpodwysocki Aug 14, 2026
95a77d7
Redesign preview-token "provide" flow to use MCP URL-mode elicitation
mattpodwysocki Aug 14, 2026
1ee7462
Flip ENABLE_LOCAL_URL_ELICITATION to opt-in, default disabled
mattpodwysocki Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
## Unreleased

### New Features

- **Secure token elicitation for `preview_style_tool` / `style_comparison_tool`** (#57): `accessToken` is now optional on both tools. When omitted, the tool asks how you'd like to provide a public token — pasting an existing `pk.*` token, creating a new one with optional URL restrictions, or auto-creating a minimally-scoped one (`styles:read`, `styles:tiles`, `fonts:read`). This keeps your server's `sk.*`/`pk.*` access token out of chat history and preview URLs. Falls back to requiring `accessToken` directly on clients without elicitation support (Claude Desktop, Claude Code). A chosen/created token is cached in memory per token; pass `useCustomToken: true` to force re-selection. Elicitation requests are routed through the per-call session context rather than a shared server reference, so a tool call's prompt is always delivered back to the session that made it, even when the same tool instance is reused across multiple concurrent sessions.
- If the server's own access token is a literal Mapbox temporary token (`tk.*`), the "create a new token" and "auto-create" options are omitted from the dialog up front, since that token shape is guaranteed to lack `tokens:write`. Other callers lacking `tokens:write` (e.g. the hosted MCP endpoint, which authenticates with its own access token rather than a Mapbox `pk.*`/`sk.*`/`tk.*` token) aren't detectable ahead of time; for those, choosing "create"/"auto-create" fails against the Tokens API with a scope/permission hint appended to the error, steering back to "I have a token to provide".
- The initial choice dialog has an explicit 60s timeout rather than relying on the SDK's default; a client-returned token name/URL-restriction list that exceeds a sane size is rejected server-side regardless of what the dialog's schema hints suggest a client's form should enforce; and the in-memory token cache is bounded to 1000 entries (LRU eviction), so none of this can grow memory usage without limit.
- **"I have a token to provide" now uses MCP URL-mode elicitation, not a form field.** The MCP spec (SEP-1036) prohibits collecting credentials via form-mode elicitation — a `pk.*` token is still a credential, so the original design (a `token` text field in the same form-mode dialog as the choice picker) didn't comply, and separately meant a pasted `sk.*` secret token skipped the `pk.*` prefix check every other path enforces and got embedded straight into the returned preview URL (found by an adversarial review pass). Choosing "provide" now opens a short-lived HTTP server on `127.0.0.1` (the same pattern `gh auth login`/`gcloud auth login` use) and sends a URL-mode elicitation request pointing at it; the token is submitted directly to that local page, never through the MCP client or chat history, then validated with the same `pk.*`/length checks as every other path. This is opt-in via `ENABLE_LOCAL_URL_ELICITATION` (default `false`) — only this package's own local stdio entry point (`dist/esm/index.js`) enables it automatically, since that's the one context confirmed to run on the same machine as the user's browser. Any other embedding (e.g. a hosted/cloud deployment, where a `127.0.0.1` URL wouldn't resolve to anything the user's browser could reach) stays safe with no action needed; with it disabled, "provide" falls back to requiring `accessToken` directly, same as a client without elicitation support at all.
- Every field the client returns from the choice dialog (choice, token name, URL restrictions) is now validated against its expected runtime type and, for `choice`, against the set of options actually offered — a client returning an unrecognized or wrong-typed value previously either fell through to auto-creating a real token or threw an unclassified `TypeError`. A request timeout or cancellation at either step (the choice dialog or the URL-mode consent request) is now also distinguished from "client doesn't support elicitation", so a caller passing `useCustomToken: true` gets a real error instead of silently getting back the stale cached token it was trying to replace.
- Listing a user's existing public tokens (to populate the dialog) only requires `tokens:read`, a separate scope from the `tokens:write` needed to create one — it's no longer skipped for `tk.*`-authenticated servers, which lack the latter but not necessarily the former.
- Error messages from a failed token-creation call are now redacted the same way every other Mapbox API tool's errors are, since these two tools don't extend the base class that applies that redaction automatically. A misconfigured endpoint could previously surface the caller's own access token verbatim in the returned error text.

### Changed

- **`preview_style_tool` / `style_comparison_tool`**: token-listing and token-creation HTTP calls now go through the shared `HttpPipeline` (constructor-injected `httpRequest`) instead of a bare `fetch`, consistent with the rest of the API-calling tools.

### Dependencies

- Bumped `@modelcontextprotocol/sdk` to `1.30.0`. Not adopting the `2026-07-28` spec revision this release covers (stateless request/response model, elicitation replaced by Multi Round-Trip Requests, Sampling deprecated) — that's a separate migration, tracked in #130, given this repo's own elicitation-based features depend on the mechanism being replaced. Regenerated `patches/@modelcontextprotocol+sdk+1.30.0.patch` (previously pinned to `1.29.0`) — same patch content, applies cleanly to the new version, verified live against the built server.
Expand Down
77 changes: 73 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ Get started by integrating with your preferred AI development environment:
- [Cursor Integration](./docs/cursor-integration.md) - Cursor IDE integration
- [VS Code Integration](./docs/vscode-integration.md) - Visual Studio Code with GitHub Copilot

**Note on MCP Elicitation Support**: Some tools (like `preview_style_tool` and `style_comparison_tool`) use [MCP elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) to provide secure token management following the principle of least privilege. Elicitation ensures that only minimal-scope public tokens (pk._) appear in preview URLs, while your powerful server token (sk._) stays secure. This guided workflow also improves UX for token selection and creation. Elicitation support varies by client:

- **MCP Inspector**: ✅ Full support
- **Cursor**: ✅ Full support
- **VS Code (with Copilot)**: ✅ Full support
- **Goose**: ⚠️ Known bug - Form displays after timeout ([goose#6471](https://github.com/block/goose/issues/6471))
- **Claude Desktop**: ⚠️ Not yet supported (Claude will fall back to creating tokens via chat)
- **Claude Code**: ⚠️ Not yet supported (provide `accessToken` parameter directly)

Choosing **"I have a token to provide"** doesn't paste the token into a form — the MCP spec requires credentials to go through [URL-mode elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#url-mode-elicitation-requests) instead, so this opens a page served locally on your own machine (`http://127.0.0.1:<port>`) to submit it. This requires a client that supports URL-mode elicitation specifically, and only works when the server process is running on the same machine as your browser — running via `dist/esm/index.js` (Claude Desktop, Claude Code, Cursor, VS Code, or any other local stdio client) enables it automatically. See [`ENABLE_LOCAL_URL_ELICITATION`](#enable_local_url_elicitation) if you're embedding this package's tools yourself rather than running it as a local server.

**Note on the hosted MCP endpoint**: even on a client with full elicitation support, "create a new token" and "auto-create" will fail on the [hosted endpoint](#hosted-mcp-endpoint) — see below for why. "I have a token to provide" is also unavailable there; use the `accessToken` parameter directly instead.

### DXT Package Distribution

This MCP server can be packaged as a DXT (Desktop Extension) file for easy distribution and installation. DXT is a standardized format for distributing local MCP servers, similar to browser extensions.
Expand Down Expand Up @@ -96,6 +109,13 @@ For quick access, you can use our hosted MCP endpoint:

For detailed setup instructions for different clients and API usage, see the [Hosted MCP Server Guide](https://github.com/mapbox/mcp-server/blob/main/docs/hosted-mcp-guide.md). Note: This guide references the standard MCP endpoint - you'll need to update the endpoint URL to use the devkit endpoint above.

**Token creation is unavailable on the hosted endpoint**: the hosted deployment authenticates each request with its own access token rather than your personal Mapbox account token, and that token is not granted `tokens:write`. As a result, on the hosted endpoint:

- `preview_style_tool` / `style_comparison_tool`'s elicitation dialog still offers all three options, but choosing "create a new token" or "auto-create" fails against the Mapbox Tokens API with a scope/permission error (the dialog can't know ahead of time that this particular deployment's token lacks `tokens:write` — see the `isTemporaryServerToken` caveat in `src/utils/tokenElicitation.ts` for tokens where it can tell).
- `create_token_tool` is not exposed on the hosted endpoint at all.

**"I have a token to provide" is also unavailable on the hosted endpoint by default**: that option works via URL-mode elicitation to a page served on `127.0.0.1`, which only makes sense when the server process runs on your own machine. This is opt-in (see [`ENABLE_LOCAL_URL_ELICITATION`](#enable_local_url_elicitation)) and only this package's own local stdio entry point (`dist/esm/index.js`) turns it on automatically, so a hosted/cloud deployment stays safe with no action needed — pass `accessToken` directly there instead. Create a token ahead of time from your [Mapbox Account page](https://account.mapbox.com/) if you don't have one. Running this server **locally** with your own `pk.*`/`sk.*` access token (which can carry `tokens:write`) also enables create and auto-create.

### Getting Your Mapbox Access Token

**A Mapbox access token is required to use this MCP server.**
Expand Down Expand Up @@ -169,11 +189,49 @@ Complete set of tools for managing Mapbox styles via the Styles API:
- Input: `styleId`
- Returns: Success confirmation

**PreviewStyleTool** - Generate preview URL for a Mapbox style using an existing public token
**PreviewStyleTool** - Generate preview URL for a Mapbox style with secure token handling

- Input: `styleId`, `title` (optional), `zoomwheel` (optional), `zoom` (optional), `center` (optional), `bearing` (optional), `pitch` (optional)
- Input:
- `styleId` (required): Style ID to preview
- `accessToken` (optional): Provide a specific public token (for backward compatibility)
- `useCustomToken` (optional): Force token selection dialog even if a token is cached
- `title` (optional): Show title in preview
- `zoomwheel` (optional): Enable zoom wheel control
- Returns: URL to open the style preview in browser
- **Note**: This tool automatically fetches the first available public token from your account for the preview URL. Requires at least one public token with `styles:read` scope.
- **🔐 Secure Token Management**: If `accessToken` is not provided, this tool uses MCP **elicitation** to create minimal-scope public tokens (pk._) instead of exposing your powerful server token. This follows the **principle of least privilege** - preview/comparison URLs only contain read-only tokens (styles:read, styles:tiles, fonts:read), keeping your server token (sk._) with write permissions secure. **Elicitation support varies by client**:
- **MCP Inspector, Cursor, VS Code**: ✅ Full support - Shows guided form dialog with three options:
1. **Provide an existing token** - Paste a token you already have
2. **Create a new preview token** - Create a new token with optional URL restrictions for enhanced security
3. **Auto-create a basic token** - Let the tool create a simple preview token for you
- **Goose**: ⚠️ Known bug - Form displays after timeout ([goose#6471](https://github.com/block/goose/issues/6471))
- **Claude Desktop, Claude Code**: ⚠️ Not yet supported - Provide `accessToken` parameter directly, or Claude will intelligently offer to create a token for you using `create_token_tool`
- **Hosted MCP endpoint**: ⚠️ "Create" and "auto-create" will fail regardless of client — see [Token creation is unavailable on the hosted endpoint](#hosted-mcp-endpoint)
- **Alternative**: Provide `accessToken` parameter directly for backward compatibility with any client
- **Session Storage**: Your token choice is cached for the session, so you only need to provide it once (when elicitation is supported)
- **Best Practice**: Use URL-restricted tokens to further limit token usage to specific domains. While public tokens in URLs are read-only, URL restrictions add an extra layer of security by ensuring tokens only work on your specified domains

**StyleComparisonTool** - Generate side-by-side comparison URL for two Mapbox styles

- Input:
- `before` (required): Mapbox style for the "before" side (accepts full style URL, username/styleId format, or just styleId)
- `after` (required): Mapbox style for the "after" side (accepts full style URL, username/styleId format, or just styleId)
- `accessToken` (optional): Provide a specific public token (for backward compatibility)
- `useCustomToken` (optional): Force token selection dialog even if a token is cached
- `zoom` (optional): Initial zoom level (0-22)
- `latitude` (optional): Latitude coordinate for initial map center (-90 to 90)
- `longitude` (optional): Longitude coordinate for initial map center (-180 to 180)
- Returns: URL to open the side-by-side style comparison in browser
- **🔐 Secure Token Management**: If `accessToken` is not provided, this tool uses MCP **elicitation** to create minimal-scope public tokens (pk._) instead of exposing your powerful server token. This follows the **principle of least privilege** - preview/comparison URLs only contain read-only tokens (styles:read, styles:tiles, fonts:read), keeping your server token (sk._) with write permissions secure. **Elicitation support varies by client**:
- **MCP Inspector, Cursor, VS Code**: ✅ Full support - Shows guided form dialog with three options:
1. **Provide an existing token** - Paste a token you already have
2. **Create a new preview token** - Create a new token with optional URL restrictions for enhanced security
3. **Auto-create a basic token** - Let the tool create a simple preview token for you
- **Goose**: ⚠️ Known bug - Form displays after timeout ([goose#6471](https://github.com/block/goose/issues/6471))
- **Claude Desktop, Claude Code**: ⚠️ Not yet supported - Provide `accessToken` parameter directly, or Claude will intelligently offer to create a token for you using `create_token_tool`
- **Hosted MCP endpoint**: ⚠️ "Create" and "auto-create" will fail regardless of client — see [Token creation is unavailable on the hosted endpoint](#hosted-mcp-endpoint)
- **Alternative**: Provide `accessToken` parameter directly for backward compatibility with any client
- **Session Storage**: Your token choice is cached for the session, so you only need to provide it once (when elicitation is supported)
- **Best Practice**: Use URL-restricted tokens to further limit token usage to specific domains. While public tokens in URLs are read-only, URL restrictions add an extra layer of security by ensuring tokens only work on your specified domains

**ValidateStyleTool** - Validate Mapbox style JSON against the Mapbox Style Specification

Expand All @@ -195,7 +253,8 @@ Complete set of tools for managing Mapbox styles via the Styles API:
- **RetrieveStyleTool**: Requires `styles:download` scope
- **UpdateStyleTool**: Requires `styles:write` scope
- **DeleteStyleTool**: Requires `styles:write` scope
- **PreviewStyleTool**: Requires `tokens:read` scope (to list tokens) and at least one public token with `styles:read` scope
- **PreviewStyleTool**: Can work without token scopes via elicitation, or optionally accepts a direct public token. If using automatic token listing, requires `tokens:read` scope
- **StyleComparisonTool**: Can work without token scopes via elicitation, or optionally accepts a direct public token. If using automatic token listing, requires `tokens:read` scope

**Note:** The username is automatically extracted from the JWT token payload.

Expand Down Expand Up @@ -1247,6 +1306,16 @@ Set `VERBOSE_ERRORS=true` to get detailed error messages from the MCP server. Th

By default, the server returns generic error messages. With verbose errors enabled, you'll receive the actual error details, which can help diagnose API connection issues, invalid parameters, or other problems.

#### ENABLE_LOCAL_URL_ELICITATION

Controls whether `preview_style_tool` / `style_comparison_tool`'s "I have a token to provide" option is offered. Per the MCP spec, servers must not collect credentials via form-mode elicitation, so providing a token instead opens a short-lived HTTP server on `127.0.0.1` and sends a URL-mode elicitation request pointing at it — the same pattern CLI tools like `gh auth login` use. This only works when the server process and your browser are on the same machine.

**Opt-in, not opt-out: defaults to `false`.** Set to `true` only after confirming the server process and the user's browser really are on the same machine — a `127.0.0.1` URL otherwise resolves to the browser's own loopback interface, where nothing is listening, rather than the server. This package's own local stdio entry point (`dist/esm/index.js`, used by Claude Desktop, Claude Code, Cursor, VS Code, and similar clients) sets this automatically; you only need to set it yourself if you're embedding these tools in your own server rather than running `dist/esm/index.js` directly. With it disabled (the default for any such embedding), choosing "provide" falls back to the same message shown to clients without elicitation support at all: pass `accessToken` directly instead.

```bash
export ENABLE_LOCAL_URL_ELICITATION=true
```

#### ENABLE_MCP_UI

**Interactive Previews: MCP Apps (primary) & MCP-UI (compatibility)**
Expand Down
1 change: 1 addition & 0 deletions docs/engineering_standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Keep secrets out of repositories. Use environment variables for sensitive data:
- `MAPBOX_ACCESS_TOKEN` - Required for all Mapbox API operations
- `VERBOSE_ERRORS` - Set to `true` for detailed error messages
- `ENABLE_MCP_UI` - Controls MCP-UI support (default: `true`)
- `ENABLE_LOCAL_URL_ELICITATION` - Controls whether `preview_style_tool`/`style_comparison_tool` offer URL-mode token collection via a local `127.0.0.1` server (opt-in, default: `false` — see `src/utils/tokenCollectionServer.ts`). Only `src/index.ts` (this package's stdio entry point) sets it to `true` automatically, since that's the one context confirmed to run on the same machine as the user's browser; any other embedder (e.g. a hosted/cloud deployment) stays safe by default and must not enable this unless it's confirmed the same machine/browser relationship holds.
- `OTEL_EXPORTER_OTLP_ENDPOINT` - OpenTelemetry endpoint (optional)
- `OTEL_SERVICE_NAME` - Override service name for tracing (optional)

Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@
}
}

// preview_style_tool / style_comparison_tool's "provide a token" option collects the
// token via a local http://127.0.0.1 server (see src/utils/tokenCollectionServer.ts),
// which only makes sense when the MCP server process and the user's browser are on the
// same machine — true for this stdio entry point, but not for every way this package's
// tools can be embedded (e.g. a cloud deployment). Opt in here, since this is the one
// context confirmed safe, rather than defaulting it on everywhere and relying on every
// other embedder to remember to opt out. Left untouched if already set (by the
// environment or the .env file below), so an explicit override always wins.
if (process.env.ENABLE_LOCAL_URL_ELICITATION === undefined) {
process.env.ENABLE_LOCAL_URL_ELICITATION = 'true';
}

const versionInfo = getVersionInfo();

// Parse configuration from command-line arguments
Expand Down Expand Up @@ -100,7 +112,7 @@
// This tells clients (like Claude Desktop) that this is an MCP App
uiResources.forEach((resource) => {
registerAppResource(
server as any,

Check warning on line 115 in src/index.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
resource.name,
resource.uri,
{ mimeType: RESOURCE_MIME_TYPE, description: resource.description },
Expand Down
Loading
Loading