Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"dxt_version": "0.1",
"name": "@mapbox/mcp-devkit-server",
"display_name": "Mapbox MCP DevKit Server",
"version": "0.3.0",
"version": "0.3.1",
"description": "Mapbox MCP devkit server",
"author": {
"name": "Mapbox, Inc."
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mapbox/mcp-devkit-server",
"version": "0.3.0",
"version": "0.3.1",
"description": "Mapbox MCP devkit server",
"main": "dist/index.js",
"module": "dist/index-esm.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ exports[`Tool Naming Convention should maintain consistent tool list (snapshot t
},
{
"className": "ListStylesTool",
"description": "List all styles for a Mapbox account",
"description": "List styles for a Mapbox account. Use limit parameter to avoid large responses (recommended: limit=5-10). Use start parameter for pagination.",
"toolName": "list_styles_tool",
},
{
Expand Down
11 changes: 9 additions & 2 deletions src/tools/list-styles-tool/ListStylesTool.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ export const ListStylesSchema = z.object({
limit: z
.number()
.optional()
.describe('Maximum number of styles to return (default: no limit)'),
start: z.string().optional().describe('Start token for pagination')
.describe(
'Maximum number of styles to return (recommended: 5-10 to avoid token limits, default: no limit)'
),
start: z
.string()
.optional()
.describe(
'Start token for pagination (use the "start" value from previous response)'
)
});

export type ListStylesInput = z.infer<typeof ListStylesSchema>;
6 changes: 4 additions & 2 deletions src/tools/list-styles-tool/ListStylesTool.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Use a token with valid JWT format for tests
process.env.MAPBOX_ACCESS_TOKEN =
'eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';
'sk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';

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

it('should have correct input schema', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/tools/list-styles-tool/ListStylesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export class ListStylesTool extends MapboxApiBasedTool<
typeof ListStylesSchema
> {
name = 'list_styles_tool';
description = 'List all styles for a Mapbox account';
description =
'List styles for a Mapbox account. Use limit parameter to avoid large responses (recommended: limit=5-10). Use start parameter for pagination.';

constructor() {
super({ inputSchema: ListStylesSchema });
Expand Down
9 changes: 9 additions & 0 deletions src/tools/preview-style-tool/PreviewStyleTool.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import { z } from 'zod';

export const PreviewStyleSchema = z.object({
styleId: z.string().describe('Style ID to preview'),
accessToken: z
.string()
.startsWith(
'pk.',
'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.'
)
.describe(
'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.'
),
title: z
.boolean()
.optional()
Expand Down
104 changes: 33 additions & 71 deletions src/tools/preview-style-tool/PreviewStyleTool.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,12 @@
// Use a token with valid JWT format for tests
process.env.MAPBOX_ACCESS_TOKEN =
'eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';
'sk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';

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

describe('PreviewStyleTool', () => {
let mockListTokensTool: jest.SpyInstance;

beforeEach(() => {
// Mock the ListTokensTool.run method
mockListTokensTool = jest
.spyOn(ListTokensTool.prototype, 'run')
.mockResolvedValue({
isError: false,
content: [
{
type: 'text',
text: JSON.stringify({
tokens: [
{
id: 'cktest123',
note: 'Public token for testing',
usage: 'pk',
token:
'pk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.public_token',
scopes: ['styles:read', 'fonts:read']
}
],
count: 1
})
}
]
});
});

afterEach(() => {
jest.restoreAllMocks();
});
const TEST_ACCESS_TOKEN =
'pk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature';

describe('tool metadata', () => {
it('should have correct name and description', () => {
Expand All @@ -54,8 +23,11 @@ describe('PreviewStyleTool', () => {
});
});

it('fetches public token and returns preview URL', async () => {
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
it('uses user-provided public token and returns preview URL', async () => {
const result = await new PreviewStyleTool().run({
styleId: 'test-style',
accessToken: TEST_ACCESS_TOKEN
});

expect(result.isError).toBe(false);
expect(result.content[0]).toMatchObject({
Expand All @@ -64,16 +36,12 @@ describe('PreviewStyleTool', () => {
'/styles/v1/test-user/test-style.html?access_token=pk.'
)
});

// Verify that ListTokensTool was called with correct parameters
expect(mockListTokensTool).toHaveBeenCalledWith({
usage: 'pk'
});
});

it('includes styleId in URL', async () => {
const result = await new PreviewStyleTool().run({
styleId: 'my-custom-style'
styleId: 'my-custom-style',
accessToken: TEST_ACCESS_TOKEN
});

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

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

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

it('includes fresh parameter for secure access', async () => {
const result = await new PreviewStyleTool().run({
styleId: 'test-style'
styleId: 'test-style',
accessToken: TEST_ACCESS_TOKEN
});

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

it('handles token listing failure', async () => {
mockListTokensTool.mockResolvedValueOnce({
isError: true,
content: [
{
type: 'text',
text: 'Token listing failed'
}
]
it('rejects secret tokens', async () => {
const result = await new PreviewStyleTool().run({
styleId: 'test-style',
accessToken:
'sk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.secret_token'
});

const result = await new PreviewStyleTool().run({ styleId: 'test-style' });

expect(result.isError).toBe(true);
expect(result.content[0]).toMatchObject({
type: 'text',
text: expect.stringContaining('Failed to retrieve public tokens')
text: expect.stringContaining(
'Invalid access token. Only public tokens (starting with pk.*) are allowed'
)
});
});

it('handles no public tokens found', async () => {
mockListTokensTool.mockResolvedValueOnce({
isError: false,
content: [
{
type: 'text',
text: JSON.stringify({
tokens: [],
count: 0
})
}
]
it('rejects temporary tokens', async () => {
const result = await new PreviewStyleTool().run({
styleId: 'test-style',
accessToken: 'tk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.temp_token'
});

const result = await new PreviewStyleTool().run({ styleId: 'test-style' });

expect(result.isError).toBe(true);
expect(result.content[0]).toMatchObject({
type: 'text',
text: expect.stringContaining('No public tokens found')
text: expect.stringContaining(
'Invalid access token. Only public tokens (starting with pk.*) are allowed'
)
});
});

it('returns URL on success', async () => {
const result = await new PreviewStyleTool().run({ styleId: 'test-style' });
const result = await new PreviewStyleTool().run({
styleId: 'test-style',
accessToken: TEST_ACCESS_TOKEN
});

expect(result.isError).toBe(false);
expect(result.content).toHaveLength(1);
Expand Down
39 changes: 6 additions & 33 deletions src/tools/preview-style-tool/PreviewStyleTool.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { BaseTool } from '../BaseTool.js';
import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js';
import { ListTokensTool } from '../list-tokens-tool/ListTokensTool.js';
import {
PreviewStyleSchema,
PreviewStyleInput
} from './PreviewStyleTool.schema.js';

export class PreviewStyleTool extends MapboxApiBasedTool<
typeof PreviewStyleSchema
> {
export class PreviewStyleTool extends BaseTool<typeof PreviewStyleSchema> {
readonly name = 'preview_style_tool';
readonly description =
'Generate preview URL for a Mapbox style using an existing public token';
Expand All @@ -17,37 +15,12 @@ export class PreviewStyleTool extends MapboxApiBasedTool<
}

protected async execute(
input: PreviewStyleInput,
accessToken?: string
input: PreviewStyleInput
): Promise<{ type: 'text'; text: string }> {
const username = MapboxApiBasedTool.getUserNameFromToken(accessToken);
const username = MapboxApiBasedTool.getUserNameFromToken(input.accessToken);

// Get list of tokens to find a public token
const listTokensTool = new ListTokensTool();
const tokensResult = await listTokensTool.run({
usage: 'pk' // Filter for public tokens only
});

if (tokensResult.isError) {
throw new Error('Failed to retrieve public tokens');
}

// Extract tokens from the response
const firstContent = tokensResult.content[0];
if (firstContent.type !== 'text') {
throw new Error('Unexpected response format from list tokens');
}
const tokensData = JSON.parse(firstContent.text);
const publicTokens = tokensData.tokens;

if (!publicTokens || publicTokens.length === 0) {
throw new Error(
'No public tokens found. Please create a public token first.'
);
}

// Use the first available public token
const publicToken = publicTokens[0].token;
// Use the user-provided public token
const publicToken = input.accessToken;

// Build URL for the embeddable HTML endpoint
const params = new URLSearchParams();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export const StyleComparisonSchema = z.object({
),
accessToken: z
.string()
.startsWith(
'pk.',
'Invalid token type. Style comparison requires a public token (pk.*) that can be used in browser URLs. Secret tokens (sk.*) cannot be exposed in client-side applications. Please provide a public token with styles:read permission.'
)
.describe(
'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 a public token or create one with styles:read permission.'
),
Expand Down
16 changes: 0 additions & 16 deletions src/tools/style-comparison-tool/StyleComparisonTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,6 @@ export class StyleComparisonTool extends BaseTool<
super({ inputSchema: StyleComparisonSchema });
}

/**
* Validates that the token is a public token
*/
private validatePublicToken(token: string): void {
if (!token.startsWith('pk.')) {
throw new Error(
`Invalid token type. Style comparison requires a public token (pk.*) that can be used in browser URLs. ` +
`Secret tokens (sk.*) cannot be exposed in client-side applications. ` +
`Please provide a public token with styles:read permission.`
);
}
}

/**
* Processes style input to extract username/styleId format
*/
Expand Down Expand Up @@ -61,9 +48,6 @@ export class StyleComparisonTool extends BaseTool<
protected async execute(
input: StyleComparisonInput
): Promise<{ type: 'text'; text: string }> {
// Validate that we have a public token
this.validatePublicToken(input.accessToken);

// Process style IDs to get username/styleId format
const beforeStyleId = this.processStyleId(input.before, input.accessToken);
const afterStyleId = this.processStyleId(input.after, input.accessToken);
Expand Down
Loading