diff --git a/manifest.json b/manifest.json index 2b6c6d4..4187694 100644 --- a/manifest.json +++ b/manifest.json @@ -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." diff --git a/package-lock.json b/package-lock.json index b26410f..85dc55a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mapbox/mcp-devkit-server", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mapbox/mcp-devkit-server", - "version": "0.3.0", + "version": "0.3.1", "license": "BSD-3-Clause", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/package.json b/package.json index 1ea9560..33d26b2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/tools/__snapshots__/tool-naming-convention.test.ts.snap b/src/tools/__snapshots__/tool-naming-convention.test.ts.snap index a1edcc3..97e36e9 100644 --- a/src/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/src/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -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", }, { diff --git a/src/tools/list-styles-tool/ListStylesTool.schema.ts b/src/tools/list-styles-tool/ListStylesTool.schema.ts index 0b682c7..e2715ba 100644 --- a/src/tools/list-styles-tool/ListStylesTool.schema.ts +++ b/src/tools/list-styles-tool/ListStylesTool.schema.ts @@ -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; diff --git a/src/tools/list-styles-tool/ListStylesTool.test.ts b/src/tools/list-styles-tool/ListStylesTool.test.ts index c8dca67..b4148ac 100644 --- a/src/tools/list-styles-tool/ListStylesTool.test.ts +++ b/src/tools/list-styles-tool/ListStylesTool.test.ts @@ -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, @@ -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', () => { diff --git a/src/tools/list-styles-tool/ListStylesTool.ts b/src/tools/list-styles-tool/ListStylesTool.ts index 0c62ba9..faca9a4 100644 --- a/src/tools/list-styles-tool/ListStylesTool.ts +++ b/src/tools/list-styles-tool/ListStylesTool.ts @@ -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 }); diff --git a/src/tools/preview-style-tool/PreviewStyleTool.schema.ts b/src/tools/preview-style-tool/PreviewStyleTool.schema.ts index 5254fda..eec52c3 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.schema.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.schema.ts @@ -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() diff --git a/src/tools/preview-style-tool/PreviewStyleTool.test.ts b/src/tools/preview-style-tool/PreviewStyleTool.test.ts index 7867dae..50f6eca 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.test.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.test.ts @@ -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', () => { @@ -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({ @@ -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({ @@ -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 }); @@ -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 }); @@ -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({ @@ -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); diff --git a/src/tools/preview-style-tool/PreviewStyleTool.ts b/src/tools/preview-style-tool/PreviewStyleTool.ts index 6cd27b0..32783cd 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.ts @@ -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 { readonly name = 'preview_style_tool'; readonly description = 'Generate preview URL for a Mapbox style using an existing public token'; @@ -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(); diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts b/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts index 6f51be4..aa62aec 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts @@ -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.' ), diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.ts b/src/tools/style-comparison-tool/StyleComparisonTool.ts index 334a858..1aec199 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.ts @@ -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 */ @@ -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);