Skip to content

Commit c938b8d

Browse files
authored
Improve style preview (#11)
1 parent 05e3cfd commit c938b8d

12 files changed

Lines changed: 72 additions & 130 deletions

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/__snapshots__/tool-naming-convention.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ exports[`Tool Naming Convention should maintain consistent tool list (snapshot t
3939
},
4040
{
4141
"className": "ListStylesTool",
42-
"description": "List all styles for a Mapbox account",
42+
"description": "List styles for a Mapbox account. Use limit parameter to avoid large responses (recommended: limit=5-10). Use start parameter for pagination.",
4343
"toolName": "list_styles_tool",
4444
},
4545
{

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: 5-10 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.test.ts

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

55
import {
66
setupFetch,
@@ -17,7 +17,9 @@ describe('ListStylesTool', () => {
1717
it('should have correct name and description', () => {
1818
const tool = new ListStylesTool();
1919
expect(tool.name).toBe('list_styles_tool');
20-
expect(tool.description).toBe('List all styles for a Mapbox account');
20+
expect(tool.description).toBe(
21+
'List styles for a Mapbox account. Use limit parameter to avoid large responses (recommended: limit=5-10). Use start parameter for pagination.'
22+
);
2123
});
2224

2325
it('should have correct input schema', () => {

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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ 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+
.startsWith(
8+
'pk.',
9+
'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.'
10+
)
11+
.describe(
12+
'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.'
13+
),
514
title: z
615
.boolean()
716
.optional()

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

Lines changed: 33 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,12 @@
11
// Use a token with valid JWT format for tests
22
process.env.MAPBOX_ACCESS_TOKEN =
3-
'eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';
3+
'sk.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-
});
37-
38-
afterEach(() => {
39-
jest.restoreAllMocks();
40-
});
8+
const TEST_ACCESS_TOKEN =
9+
'pk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';
4110

4211
describe('tool metadata', () => {
4312
it('should have correct name and description', () => {
@@ -54,8 +23,11 @@ describe('PreviewStyleTool', () => {
5423
});
5524
});
5625

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

6032
expect(result.isError).toBe(false);
6133
expect(result.content[0]).toMatchObject({
@@ -64,16 +36,12 @@ describe('PreviewStyleTool', () => {
6436
'/styles/v1/test-user/test-style.html?access_token=pk.'
6537
)
6638
});
67-
68-
// Verify that ListTokensTool was called with correct parameters
69-
expect(mockListTokensTool).toHaveBeenCalledWith({
70-
usage: 'pk'
71-
});
7239
});
7340

7441
it('includes styleId in URL', async () => {
7542
const result = await new PreviewStyleTool().run({
76-
styleId: 'my-custom-style'
43+
styleId: 'my-custom-style',
44+
accessToken: TEST_ACCESS_TOKEN
7745
});
7846

7947
expect(result.content[0]).toMatchObject({
@@ -85,6 +53,7 @@ describe('PreviewStyleTool', () => {
8553
it('includes title parameter when provided', async () => {
8654
const result = await new PreviewStyleTool().run({
8755
styleId: 'test-style',
56+
accessToken: TEST_ACCESS_TOKEN,
8857
title: true
8958
});
9059

@@ -97,6 +66,7 @@ describe('PreviewStyleTool', () => {
9766
it('includes zoomwheel parameter when provided', async () => {
9867
const result = await new PreviewStyleTool().run({
9968
styleId: 'test-style',
69+
accessToken: TEST_ACCESS_TOKEN,
10070
zoomwheel: false
10171
});
10272

@@ -108,7 +78,8 @@ describe('PreviewStyleTool', () => {
10878

10979
it('includes fresh parameter for secure access', async () => {
11080
const result = await new PreviewStyleTool().run({
111-
styleId: 'test-style'
81+
styleId: 'test-style',
82+
accessToken: TEST_ACCESS_TOKEN
11283
});
11384

11485
expect(result.content[0]).toMatchObject({
@@ -117,51 +88,42 @@ describe('PreviewStyleTool', () => {
11788
});
11889
});
11990

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-
]
91+
it('rejects secret tokens', async () => {
92+
const result = await new PreviewStyleTool().run({
93+
styleId: 'test-style',
94+
accessToken:
95+
'sk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.secret_token'
12996
});
13097

131-
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
132-
13398
expect(result.isError).toBe(true);
13499
expect(result.content[0]).toMatchObject({
135100
type: 'text',
136-
text: expect.stringContaining('Failed to retrieve public tokens')
101+
text: expect.stringContaining(
102+
'Invalid access token. Only public tokens (starting with pk.*) are allowed'
103+
)
137104
});
138105
});
139106

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-
]
107+
it('rejects temporary tokens', async () => {
108+
const result = await new PreviewStyleTool().run({
109+
styleId: 'test-style',
110+
accessToken: 'tk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.temp_token'
152111
});
153112

154-
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
155-
156113
expect(result.isError).toBe(true);
157114
expect(result.content[0]).toMatchObject({
158115
type: 'text',
159-
text: expect.stringContaining('No public tokens found')
116+
text: expect.stringContaining(
117+
'Invalid access token. Only public tokens (starting with pk.*) are allowed'
118+
)
160119
});
161120
});
162121

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

166128
expect(result.isError).toBe(false);
167129
expect(result.content).toHaveLength(1);

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

Lines changed: 6 additions & 33 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,12 @@ 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);
20+
const username = MapboxApiBasedTool.getUserNameFromToken(input.accessToken);
2421

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) {
44-
throw new Error(
45-
'No public tokens found. Please create a public token first.'
46-
);
47-
}
48-
49-
// Use the first available public token
50-
const publicToken = publicTokens[0].token;
22+
// Use the user-provided public token
23+
const publicToken = input.accessToken;
5124

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

0 commit comments

Comments
 (0)