Skip to content

Conversation

@mattpodwysocki
Copy link
Contributor

@mattpodwysocki mattpodwysocki commented Jan 13, 2026

Summary

Implements MCP elicitation for secure token management in `preview_style_tool` and `style_comparison_tool`, 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.

The Security Problem

Previously, `preview_style_tool` and `style_comparison_tool` required users to provide an `accessToken` directly in the tool input, which created security and UX risks:

  • ⚠️ Risk of exposing powerful server token - Users might accidentally pass through their server token (sk.*) with write permissions
  • ⚠️ Over-privileged tokens - Users might manually provide tokens with more permissions than needed
  • ❌ No guided workflow for token selection
  • ❌ Users had to manually create and manage tokens
  • ❌ Token management friction

The Solution: Elicitation with Least Privilege

This PR implements MCP elicitation to enforce the principle of least privilege:

  • Server token stays secure - Your powerful secret token (sk.*) with write permissions NEVER appears in chat history or URLs
  • Minimal-scope public tokens - Preview URLs only contain read-only public tokens (pk.*) with scopes: styles:read, styles:tiles, fonts:read
  • Acceptable security trade-off - Even if preview URLs are shared, they only expose read-only access to styles, not your admin token

How It Works

When a user calls `preview_style_tool` or `style_comparison_tool` without providing an `accessToken`:

  1. Check for cached token - If a preview token was already created this session, use it
  2. Elicit token from user - Present a guided dialog with three options:
    • Provide existing token - User pastes a public token they already have
    • Create new preview token - System creates a new token with optional URL restrictions
    • Auto-create basic token - System auto-creates a simple preview token (recommended)
  3. Cache for session - Store token in memory to avoid re-prompting
  4. Generate preview/comparison URL - Use the minimal-scope public token in the URL

Key Security Properties:

  • Server token (sk.*) is only used server-side to create limited tokens via Mapbox API
  • Created tokens are always public (pk.*) with minimal read-only scopes
  • No risk of exposing write permissions (styles:write, tokens:write, etc.)

Testing

Test Case 1: First Time Preview (Elicitation Shown)

Steps:

  1. Start MCP server with valid Mapbox token
  2. Call `preview_style_tool` with only `styleId` (no `accessToken`)
  3. Observe elicitation dialog with three options
  4. Select "Auto-create a basic preview token for me"
  5. Verify preview URL is generated

Expected:

  • Elicitation dialog appears with token options
  • Dialog shows existing public tokens (if any)
  • After selection, preview URL is returned
  • Server token NEVER appears anywhere - only the minimal-scope public token in URL

Screenshot locations:

  • Elicitation dialog with options
Screenshot 2026-01-13 at 13 29 05
  • Preview URL result (only public token in URL)
Screenshot 2026-01-13 at 13 29 29
  • Token not in tool call log
Screenshot 2026-01-13 at 13 36 57

Test Case 2: Subsequent Preview (Cached Token)

Steps:

  1. After Test Case 1, call `preview_style_tool` again with different styleId
  2. No `accessToken` provided

Expected:

  • No elicitation dialog (token is cached)
Screenshot 2026-01-13 at 13 29 29
  • Preview URL generated immediately
Screenshot 2026-01-13 at 13 31 56
  • Same minimal-scope token used from cache

Test Case 3: Force New Token Selection

Steps:

  1. Call `preview_style_tool` with `useCustomToken: true`
  2. This time select "I have a token to provide"
  3. Paste a valid public token

Expected:

  • Elicitation dialog appears despite cache
Screenshot 2026-01-13 at 13 39 44
  • Token field accepts input
  • New token replaces cached token
Screenshot 2026-01-13 at 13 39 53

Test Case 4: Create Token with URL Restrictions

Steps:

  1. Call `preview_style_tool` with new styleId, `useCustomToken: true`
  2. Select "Create a new preview token with custom settings"
  3. Enter token name: "Test Preview Token"
  4. Enter URL restrictions: "https://example.com/*,https://test.com/*"

Expected:

  • New token created via Mapbox API with minimal scopes
Screenshot 2026-01-13 at 13 43 07
  • Token includes URL restrictions for additional security
Screenshot 2026-01-13 at 13 43 07
  • Preview URL generated with new token
Screenshot 2026-01-13 at 13 42 38

Test Case 5: Backward Compatibility

Steps:

  1. Call `preview_style_tool` with both `styleId` AND `accessToken`

Expected:

  • No elicitation dialog (token provided directly)
Screenshot 2026-01-13 at 13 48 46
  • Works exactly like before this PR
  • Preview URL generated with provided token
Screenshot 2026-01-13 at 13 48 53

Test Case 6: Elicitation Cancellation

Steps:

  1. Call `preview_style_tool` without `accessToken`
  2. When elicitation dialog appears, click "Cancel" or "Decline"

Expected:

  • Tool returns error: "Token elicitation was cancelled or declined by user"
Screenshot 2026-01-13 at 13 52 25 - No preview URL generated - Graceful error handling

Test Case 7: Works in Cursor

  • Run Elicitation
Screenshot 2026-01-13 at 11 14 05 - Get result Screenshot 2026-01-13 at 11 09 45

Test Case 8: Works in VS Code

  • Run Elicitation
Screenshot 2026-01-13 at 13 56 12
  • Get result
Screenshot 2026-01-13 at 13 56 31

Test Case 9: Works in Claude Desktop without support for Elicitation

  • Run tool
Screenshot 2026-01-13 at 13 57 42
  • Ask to create a token for you
Screenshot 2026-01-13 at 13 58 34

MCP Inspector Testing

To test in MCP Inspector:

```bash
npm run inspect:build
```

Then:

  1. Connect to the server in MCP Inspector
  2. Find `preview_style_tool` or `style_comparison_tool` in tools list
  3. Call with: `{"styleId": "cmi189f9600lj01sc7evj2vhs"}` or `{"before": "mapbox/streets-v12", "after": "mapbox/outdoors-v12"}`
  4. Observe elicitation dialog in Inspector UI
  5. Test all three token options

Key Features

🔐 Security Improvements

  • Principle of least privilege - Only minimal-scope read-only tokens (pk.*) appear in URLs
  • Server token protection - Powerful server token (sk.*) with write permissions never exposed
  • Reduced blast radius - Even if preview URL is shared, only read-only access to styles is exposed
  • URL-restricted tokens - Users can create tokens that only work on specific domains
  • Minimal scopes - Auto-created tokens only get `styles:read`, `styles:tiles`, `fonts:read`

🎯 User Experience

  • Guided workflow - Three clear options for token management (provide, create, or auto-create)
  • Session caching - Token only requested once per session
  • Smart defaults - Auto-create option requires zero configuration
  • No repeated prompts - Session storage remembers choice
  • Backward compatible - Can still provide token directly via `accessToken` parameter
  • Force re-selection - `useCustomToken: true` forces new token dialog
  • Client capability checks - Gracefully falls back for clients without elicitation support

🛠️ Implementation Details

New Files:

  • `src/utils/tokenElicitation.ts` - Elicitation utility with session storage
  • `test/utils/tokenElicitation.test.ts` - Unit tests for token storage (10 tests)

Modified Files:

  • `src/tools/preview-style-tool/PreviewStyleTool.ts` - Integrated elicitation flow with capability checks
  • `src/tools/preview-style-tool/PreviewStyleTool.input.schema.ts` - Made `accessToken` optional, added `useCustomToken`
  • `src/tools/style-comparison-tool/StyleComparisonTool.ts` - Integrated elicitation flow with capability checks
  • `src/tools/style-comparison-tool/StyleComparisonTool.schema.ts` - Made `accessToken` optional, added `useCustomToken`
  • `test/tools/preview-style-tool/PreviewStyleTool.test.ts` - Added elicitation behavior tests (2 tests)
  • `test/tools/style-comparison-tool/StyleComparisonTool.test.ts` - Added elicitation behavior tests (2 tests)
  • `README.md` - Updated documentation emphasizing security model

Test Results

  • ✅ All 529 tests pass (added 14 new tests)
  • ✅ Build succeeds with no type errors
  • ✅ Backward compatibility maintained
  • ✅ Manual testing in MCP Inspector - All test cases pass
  • ✅ Manual testing in Cursor - Works perfectly
  • ✅ Manual testing in VS Code - Works perfectly
  • ✅ Manual testing in Claude Desktop - Graceful fallback works

Breaking Changes

None - This is fully backward compatible:

  • Existing code providing `accessToken` works exactly as before
  • New behavior only activates when `accessToken` is omitted
  • Clients without elicitation support work via capability checks

Security Model

What We Protect:

  • ✅ Server token (sk.*) with write permissions never appears in chat history or URLs
  • ✅ Only minimal-scope public tokens (pk.*) with read-only access can appear in URLs
  • ✅ Enforces principle of least privilege automatically

Acceptable Security Trade-off:

  • Public tokens (pk.*) with read-only scopes appear in preview URLs
  • This is acceptable because:
    • They only grant read access to styles/tiles (no write permissions)
    • URL restrictions can further limit where tokens work
    • Much safer than exposing the server token with admin permissions

- Implement token elicitation to keep tokens out of chat history
- Users can provide, create, or auto-create preview tokens
- Add session-level token storage to avoid repeated prompts
- Support URL-restricted tokens for enhanced security
- Maintain backward compatibility with direct token provision
- Update README with security best practices

Security improvements:
- Preview tokens no longer appear in chat history via elicitation
- Users can create URL-restricted tokens inline
- Token caching reduces friction while maintaining security

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@mattpodwysocki mattpodwysocki requested a review from a team as a code owner January 13, 2026 15:01
mattpodwysocki and others added 6 commits January 13, 2026 10:30
Critical security fix for PreviewStyleTool:
- Add `public: true` flag to token creation API request body
- Validate that created tokens start with 'pk.' prefix
- Prevent accidental creation of secret tokens (sk.*) which should
  never be exposed in browser URLs

This ensures preview URLs always use public tokens that can be safely
shared in preview URLs without security risk.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Root cause: The Mapbox Tokens API automatically determines token type
(public vs secret) based on the SCOPES requested, not an explicit parameter.

Problem:
- We were requesting 'styles:download' which is a SECRET scope
- This forced the API to create a secret token (sk.*) instead of public (pk.*)
- Secret tokens cannot be safely exposed in browser URLs

Solution:
- Changed scopes to only public scopes: ['styles:read', 'styles:tiles', 'fonts:read']
- These are sufficient for preview URLs and guarantee public token creation
- Removed the unsupported 'public: true' parameter
- Updated comments to explain the scope selection rationale

Testing: Verified in MCP Inspector that auto-create now produces pk.* tokens

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
According to the MCP specification, servers must verify that the client
supports elicitation capability before attempting to use elicitInput().

Changes:
- Added client capability check before calling elicitPreviewToken()
- Returns clear error message if client doesn't support elicitation
- Suggests providing accessToken parameter directly as fallback
- Prevents "Method not found" errors when client lacks capability

This fixes the issue where tools using elicitation would fail on clients
that don't advertise elicitation support in their capabilities.

Reference: https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Added documentation to clarify that MCP elicitation support varies by client:
- MCP Inspector has full support for secure token elicitation
- Claude Desktop does not support elicitation yet, but Claude intelligently
  falls back to offering token creation via create_token_tool
- Other clients should check their documentation for elicitation support

Changes:
- Added "Note on MCP Elicitation Support" in Quick Start section
- Updated PreviewStyleTool description with client-specific behavior
- Clarified that tokens appear in chat history when elicitation is unavailable
- Added visual indicators (✅/⚠️) for support status

This helps users understand expected behavior based on their MCP client.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Confirmed that Cursor and VS Code both have full MCP elicitation support.
Updated README to accurately reflect support status:

✅ Full support:
- MCP Inspector
- Cursor
- VS Code (with Copilot)

⚠️ Not yet supported:
- Claude Desktop (falls back to create_token_tool)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Created comprehensive bug report for Goose's MCP elicitation timing issue
where forms display after timeout instead of during tool execution.

Added:
- docs/goose-elicitation-bug-report.md - Detailed bug report for Goose team
  with reproduction steps, expected vs actual behavior, technical details,
  and suggested fix
- Updated README to document Goose's known elicitation bug with link to
  bug report in both Quick Start and PreviewStyleTool sections

Bug Summary: Goose advertises elicitation capability but displays forms
after tool execution completes/times out, preventing user input.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
mattpodwysocki and others added 4 commits January 13, 2026 11:47
Updated bug report and README to reference the filed GitHub issue:
block/goose#6471

This allows users and developers to track the bug status directly
with the Goose team.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Bug is now tracked on GitHub at block/goose#6471
No need to maintain a duplicate markdown file in the repo.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Added comprehensive test coverage for the new elicitation features:

Token Storage Tests (test/utils/tokenElicitation.test.ts):
- Store and retrieve tokens by username
- Return undefined for non-existent username
- Overwrite existing tokens
- Store tokens for multiple users independently
- Clear specific username token
- Clear all tokens
- Handle edge cases (empty string, special characters)

PreviewStyleTool Elicitation Tests:
- Error when no accessToken and no server token
- Backward compatibility when accessToken provided directly

Test Results: All 527 tests pass (12 new tests added)

These tests ensure the elicitation feature works correctly and
maintains backward compatibility with existing usage patterns.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Tested preview_style_tool directly via MCP and confirmed that Claude Code
does not advertise elicitation capability. The tool correctly returns the
error message we designed for clients without elicitation support.

Updated README to reflect:
- Claude Code: ⚠️ Not yet supported (provide accessToken directly)
- Grouped with Claude Desktop in the "not yet supported" category

This was confirmed by calling the tool through the registered MCP server
and observing the capability check work as expected.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
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.

1 participant