Skip to content

Commit 00daa6a

Browse files
committed
improve style preview
1 parent 05e3cfd commit 00daa6a

8 files changed

Lines changed: 64 additions & 107 deletions

File tree

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"dxt_version": "0.1",
33
"name": "@mapbox/mcp-devkit-server",
44
"display_name": "Mapbox MCP DevKit Server",
5-
"version": "0.3.0",
5+
"version": "0.3.1",
66
"description": "Mapbox MCP devkit server",
77
"author": {
88
"name": "Mapbox, Inc."

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mapbox/mcp-devkit-server",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "Mapbox MCP devkit server",
55
"main": "dist/index.js",
66
"module": "dist/index-esm.js",

src/tools/list-styles-tool/ListStylesTool.schema.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@ export const ListStylesSchema = z.object({
44
limit: z
55
.number()
66
.optional()
7-
.describe('Maximum number of styles to return (default: no limit)'),
8-
start: z.string().optional().describe('Start token for pagination')
7+
.describe(
8+
'Maximum number of styles to return (recommended: 10-50 to avoid token limits, default: no limit)'
9+
),
10+
start: z
11+
.string()
12+
.optional()
13+
.describe(
14+
'Start token for pagination (use the "start" value from previous response)'
15+
)
916
});
1017

1118
export type ListStylesInput = z.infer<typeof ListStylesSchema>;

src/tools/list-styles-tool/ListStylesTool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export class ListStylesTool extends MapboxApiBasedTool<
55
typeof ListStylesSchema
66
> {
77
name = 'list_styles_tool';
8-
description = 'List all styles for a Mapbox account';
8+
description =
9+
'List styles for a Mapbox account. Use limit parameter to avoid large responses (recommended: limit=5-10). Use start parameter for pagination.';
910

1011
constructor() {
1112
super({ inputSchema: ListStylesSchema });

src/tools/preview-style-tool/PreviewStyleTool.schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { z } from 'zod';
22

33
export const PreviewStyleSchema = z.object({
44
styleId: z.string().describe('Style ID to preview'),
5+
accessToken: z
6+
.string()
7+
.describe(
8+
'Mapbox public access token (required, must start with pk.* and have styles:read permission). Secret tokens (sk.*) cannot be used as they cannot be exposed in browser URLs. Please use an existing public token or get one from list_tokens_tool or create one with create_token_tool with styles:read permission.'
9+
),
510
title: z
611
.boolean()
712
.optional()

src/tools/preview-style-tool/PreviewStyleTool.test.ts

Lines changed: 34 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,14 @@
11
// Use a token with valid JWT format for tests
22
process.env.MAPBOX_ACCESS_TOKEN =
3-
'eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';
3+
'pk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';
44

5-
import { ListTokensTool } from '../list-tokens-tool/ListTokensTool.js';
65
import { PreviewStyleTool } from './PreviewStyleTool.js';
76

87
describe('PreviewStyleTool', () => {
9-
let mockListTokensTool: jest.SpyInstance;
10-
11-
beforeEach(() => {
12-
// Mock the ListTokensTool.run method
13-
mockListTokensTool = jest
14-
.spyOn(ListTokensTool.prototype, 'run')
15-
.mockResolvedValue({
16-
isError: false,
17-
content: [
18-
{
19-
type: 'text',
20-
text: JSON.stringify({
21-
tokens: [
22-
{
23-
id: 'cktest123',
24-
note: 'Public token for testing',
25-
usage: 'pk',
26-
token:
27-
'pk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.public_token',
28-
scopes: ['styles:read', 'fonts:read']
29-
}
30-
],
31-
count: 1
32-
})
33-
}
34-
]
35-
});
36-
});
8+
const TEST_ACCESS_TOKEN =
9+
'pk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';
3710

38-
afterEach(() => {
39-
jest.restoreAllMocks();
40-
});
11+
// No setup needed since we use user-provided tokens directly
4112

4213
describe('tool metadata', () => {
4314
it('should have correct name and description', () => {
@@ -54,8 +25,11 @@ describe('PreviewStyleTool', () => {
5425
});
5526
});
5627

57-
it('fetches public token and returns preview URL', async () => {
58-
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
28+
it('uses user-provided public token and returns preview URL', async () => {
29+
const result = await new PreviewStyleTool().run({
30+
styleId: 'test-style',
31+
accessToken: TEST_ACCESS_TOKEN
32+
});
5933

6034
expect(result.isError).toBe(false);
6135
expect(result.content[0]).toMatchObject({
@@ -64,16 +38,12 @@ describe('PreviewStyleTool', () => {
6438
'/styles/v1/test-user/test-style.html?access_token=pk.'
6539
)
6640
});
67-
68-
// Verify that ListTokensTool was called with correct parameters
69-
expect(mockListTokensTool).toHaveBeenCalledWith({
70-
usage: 'pk'
71-
});
7241
});
7342

7443
it('includes styleId in URL', async () => {
7544
const result = await new PreviewStyleTool().run({
76-
styleId: 'my-custom-style'
45+
styleId: 'my-custom-style',
46+
accessToken: TEST_ACCESS_TOKEN
7747
});
7848

7949
expect(result.content[0]).toMatchObject({
@@ -85,6 +55,7 @@ describe('PreviewStyleTool', () => {
8555
it('includes title parameter when provided', async () => {
8656
const result = await new PreviewStyleTool().run({
8757
styleId: 'test-style',
58+
accessToken: TEST_ACCESS_TOKEN,
8859
title: true
8960
});
9061

@@ -97,6 +68,7 @@ describe('PreviewStyleTool', () => {
9768
it('includes zoomwheel parameter when provided', async () => {
9869
const result = await new PreviewStyleTool().run({
9970
styleId: 'test-style',
71+
accessToken: TEST_ACCESS_TOKEN,
10072
zoomwheel: false
10173
});
10274

@@ -108,7 +80,8 @@ describe('PreviewStyleTool', () => {
10880

10981
it('includes fresh parameter for secure access', async () => {
11082
const result = await new PreviewStyleTool().run({
111-
styleId: 'test-style'
83+
styleId: 'test-style',
84+
accessToken: TEST_ACCESS_TOKEN
11285
});
11386

11487
expect(result.content[0]).toMatchObject({
@@ -117,51 +90,42 @@ describe('PreviewStyleTool', () => {
11790
});
11891
});
11992

120-
it('handles token listing failure', async () => {
121-
mockListTokensTool.mockResolvedValueOnce({
122-
isError: true,
123-
content: [
124-
{
125-
type: 'text',
126-
text: 'Token listing failed'
127-
}
128-
]
93+
it('rejects secret tokens', async () => {
94+
const result = await new PreviewStyleTool().run({
95+
styleId: 'test-style',
96+
accessToken:
97+
'sk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.secret_token'
12998
});
13099

131-
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
132-
133100
expect(result.isError).toBe(true);
134101
expect(result.content[0]).toMatchObject({
135102
type: 'text',
136-
text: expect.stringContaining('Failed to retrieve public tokens')
103+
text: expect.stringContaining(
104+
'Invalid access token. Only public tokens (starting with pk.*) are allowed'
105+
)
137106
});
138107
});
139108

140-
it('handles no public tokens found', async () => {
141-
mockListTokensTool.mockResolvedValueOnce({
142-
isError: false,
143-
content: [
144-
{
145-
type: 'text',
146-
text: JSON.stringify({
147-
tokens: [],
148-
count: 0
149-
})
150-
}
151-
]
109+
it('rejects temporary tokens', async () => {
110+
const result = await new PreviewStyleTool().run({
111+
styleId: 'test-style',
112+
accessToken: 'tk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.temp_token'
152113
});
153114

154-
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
155-
156115
expect(result.isError).toBe(true);
157116
expect(result.content[0]).toMatchObject({
158117
type: 'text',
159-
text: expect.stringContaining('No public tokens found')
118+
text: expect.stringContaining(
119+
'Invalid access token. Only public tokens (starting with pk.*) are allowed'
120+
)
160121
});
161122
});
162123

163124
it('returns URL on success', async () => {
164-
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
125+
const result = await new PreviewStyleTool().run({
126+
styleId: 'test-style',
127+
accessToken: TEST_ACCESS_TOKEN
128+
});
165129

166130
expect(result.isError).toBe(false);
167131
expect(result.content).toHaveLength(1);

src/tools/preview-style-tool/PreviewStyleTool.ts

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1+
import { BaseTool } from '../BaseTool.js';
12
import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js';
2-
import { ListTokensTool } from '../list-tokens-tool/ListTokensTool.js';
33
import {
44
PreviewStyleSchema,
55
PreviewStyleInput
66
} from './PreviewStyleTool.schema.js';
77

8-
export class PreviewStyleTool extends MapboxApiBasedTool<
9-
typeof PreviewStyleSchema
10-
> {
8+
export class PreviewStyleTool extends BaseTool<typeof PreviewStyleSchema> {
119
readonly name = 'preview_style_tool';
1210
readonly description =
1311
'Generate preview URL for a Mapbox style using an existing public token';
@@ -17,37 +15,19 @@ export class PreviewStyleTool extends MapboxApiBasedTool<
1715
}
1816

1917
protected async execute(
20-
input: PreviewStyleInput,
21-
accessToken?: string
18+
input: PreviewStyleInput
2219
): Promise<{ type: 'text'; text: string }> {
23-
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);
24-
25-
// Get list of tokens to find a public token
26-
const listTokensTool = new ListTokensTool();
27-
const tokensResult = await listTokensTool.run({
28-
usage: 'pk' // Filter for public tokens only
29-
});
30-
31-
if (tokensResult.isError) {
32-
throw new Error('Failed to retrieve public tokens');
33-
}
34-
35-
// Extract tokens from the response
36-
const firstContent = tokensResult.content[0];
37-
if (firstContent.type !== 'text') {
38-
throw new Error('Unexpected response format from list tokens');
39-
}
40-
const tokensData = JSON.parse(firstContent.text);
41-
const publicTokens = tokensData.tokens;
42-
43-
if (!publicTokens || publicTokens.length === 0) {
20+
// Validate that the provided token is a public token
21+
if (!input.accessToken.startsWith('pk.')) {
4422
throw new Error(
45-
'No public tokens found. Please create a public token first.'
23+
'Invalid access token. Only public tokens (starting with pk.*) are allowed for preview URLs. Secret tokens (sk.*) cannot be used as they cannot be exposed in browser URLs.'
4624
);
4725
}
4826

49-
// Use the first available public token
50-
const publicToken = publicTokens[0].token;
27+
const username = MapboxApiBasedTool.getUserNameFromToken(input.accessToken);
28+
29+
// Use the user-provided public token
30+
const publicToken = input.accessToken;
5131

5232
// Build URL for the embeddable HTML endpoint
5333
const params = new URLSearchParams();

0 commit comments

Comments
 (0)