diff --git a/TOOL_CONFIGURATION.md b/TOOL_CONFIGURATION.md new file mode 100644 index 0000000..f94dffd --- /dev/null +++ b/TOOL_CONFIGURATION.md @@ -0,0 +1,120 @@ +# Tool Configuration Guide + +The Mapbox MCP Devkit Server supports command-line configuration to enable or disable specific tools at startup. + +## Command-Line Options + +### --enable-tools + +Enable only specific tools (exclusive mode). When this option is used, only the listed tools will be available. + +```bash + --enable-tools list_styles_tool,create_style_tool +``` + +### --disable-tools + +Disable specific tools. All other tools will remain enabled. + +```bash + --disable-tools delete_style_tool,update_style_tool +``` + +## Available Tools + +The following tools are available in the Mapbox MCP Devkit Server: + +### Style Management Tools + +- `list_styles_tool` - List all Mapbox styles +- `create_style_tool` - Create a new Mapbox style +- `retrieve_style_tool` - Retrieve details of a specific style +- `update_style_tool` - Update an existing Mapbox style +- `delete_style_tool` - Delete a Mapbox style +- `preview_style_tool` - Generate a preview image of a Mapbox style + +### Visualization Tools + +- `geojson_preview_tool` - Generate a preview map with GeoJSON data overlay + +### Token Management Tools + +- `create_token_tool` - Create a new Mapbox access token +- `list_tokens_tool` - List all Mapbox access tokens + +### Geographic Tools + +- `bounding_box_tool` - Calculate bounding box for given coordinates +- `country_bounding_box_tool` - Get bounding box for a specific country +- `coordinate_conversion_tool` - Convert between different coordinate formats + +## Usage Examples + +### Node.js + +```bash +node dist/index.js --enable-tools list_styles_tool,create_style_tool,preview_style_tool +``` + +### NPX + +```bash +npx @mapbox/mcp-devkit-server --disable-tools delete_style_tool,update_style_tool +``` + +### Docker + +```bash +docker run mapbox/mcp-devkit-server --enable-tools geojson_preview_tool,preview_style_tool,coordinate_conversion_tool +``` + +### Claude Desktop App Configuration + +In your Claude Desktop configuration file: + +```json +{ + "mcpServers": { + "mapbox-devkit": { + "command": "node", + "args": [ + "/path/to/mcp-devkit-server/dist/index.js", + "--enable-tools", + "list_styles_tool,create_style_tool,preview_style_tool" + ], + "env": { + "MAPBOX_ACCESS_TOKEN": "your-mapbox-token-here" + } + } + } +} +``` + +## Example Configurations + +### Enable only read-only tools (safe mode) + +```bash +node dist/index.js --enable-tools list_styles_tool,retrieve_style_tool,list_tokens_tool,preview_style_tool +``` + +### Enable only style management tools + +```bash +node dist/index.js --enable-tools list_styles_tool,create_style_tool,retrieve_style_tool,update_style_tool,delete_style_tool,preview_style_tool +``` + +### Disable dangerous operations + +```bash +node dist/index.js --disable-tools delete_style_tool,create_token_tool +``` + +## Notes + +- If both `--enable-tools` and `--disable-tools` are provided, `--enable-tools` takes precedence +- Tool names must match exactly (case-sensitive) +- Multiple tools can be specified using comma separation +- Invalid tool names are silently ignored +- Arguments are passed after the main command, regardless of how the server is invoked +- All tools require a valid Mapbox access token set in the `MAPBOX_ACCESS_TOKEN` environment variable diff --git a/manifest.json b/manifest.json index 8d4ff0e..2b6c6d4 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.2.3", + "version": "0.3.0", "description": "Mapbox MCP devkit server", "author": { "name": "Mapbox, Inc." diff --git a/src/config/toolConfig.test.ts b/src/config/toolConfig.test.ts new file mode 100644 index 0000000..8ff7aa4 --- /dev/null +++ b/src/config/toolConfig.test.ts @@ -0,0 +1,233 @@ +import { + describe, + it, + expect, + beforeEach, + afterAll, + jest +} from '@jest/globals'; +import { + parseToolConfigFromArgs, + filterTools, + ToolConfig +} from './toolConfig.js'; + +// Mock getVersionInfo to avoid import.meta.url issues in Jest +jest.mock('../utils/versionUtils.js', () => ({ + getVersionInfo: jest.fn(() => ({ + name: 'Mapbox MCP devkit server', + version: '1.0.0', + sha: 'mock-sha', + tag: 'mock-tag', + branch: 'mock-branch' + })) +})); + +describe('Tool Configuration', () => { + // Save original argv + const originalArgv = process.argv; + + beforeEach(() => { + // Reset argv before each test + process.argv = [...originalArgv]; + }); + + afterAll(() => { + // Restore original argv + process.argv = originalArgv; + }); + + describe('parseToolConfigFromArgs', () => { + it('should return empty config when no arguments provided', () => { + process.argv = ['node', 'index.js']; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({}); + }); + + it('should parse --enable-tools with single tool', () => { + process.argv = ['node', 'index.js', '--enable-tools', 'list_styles_tool']; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + enabledTools: ['list_styles_tool'] + }); + }); + + it('should parse --enable-tools with multiple tools', () => { + process.argv = [ + 'node', + 'index.js', + '--enable-tools', + 'list_styles_tool,create_style_tool,preview_style_tool' + ]; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + enabledTools: [ + 'list_styles_tool', + 'create_style_tool', + 'preview_style_tool' + ] + }); + }); + + it('should trim whitespace from tool names', () => { + process.argv = [ + 'node', + 'index.js', + '--enable-tools', + 'list_styles_tool , create_style_tool , preview_style_tool' + ]; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + enabledTools: [ + 'list_styles_tool', + 'create_style_tool', + 'preview_style_tool' + ] + }); + }); + + it('should parse --disable-tools with single tool', () => { + process.argv = [ + 'node', + 'index.js', + '--disable-tools', + 'delete_style_tool' + ]; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + disabledTools: ['delete_style_tool'] + }); + }); + + it('should parse --disable-tools with multiple tools', () => { + process.argv = [ + 'node', + 'index.js', + '--disable-tools', + 'delete_style_tool,update_style_tool' + ]; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + disabledTools: ['delete_style_tool', 'update_style_tool'] + }); + }); + + it('should parse both --enable-tools and --disable-tools', () => { + process.argv = [ + 'node', + 'index.js', + '--enable-tools', + 'list_styles_tool', + '--disable-tools', + 'delete_style_tool' + ]; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + enabledTools: ['list_styles_tool'], + disabledTools: ['delete_style_tool'] + }); + }); + + it('should handle missing value for --enable-tools', () => { + process.argv = ['node', 'index.js', '--enable-tools']; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({}); + }); + + it('should handle missing value for --disable-tools', () => { + process.argv = ['node', 'index.js', '--disable-tools']; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({}); + }); + + it('should ignore unknown arguments', () => { + process.argv = [ + 'node', + 'index.js', + '--unknown-arg', + 'value', + '--enable-tools', + 'list_styles_tool' + ]; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + enabledTools: ['list_styles_tool'] + }); + }); + }); + + describe('filterTools', () => { + // Mock tools for testing + const mockTools = [ + { name: 'list_styles_tool', description: 'List styles' }, + { name: 'create_style_tool', description: 'Create style' }, + { name: 'delete_style_tool', description: 'Delete style' }, + { name: 'preview_style_tool', description: 'Preview style' } + ] as any; + + it('should return all tools when no config provided', () => { + const config: ToolConfig = {}; + const filtered = filterTools(mockTools, config); + expect(filtered).toEqual(mockTools); + }); + + it('should filter tools based on enabledTools', () => { + const config: ToolConfig = { + enabledTools: ['list_styles_tool', 'create_style_tool'] + }; + const filtered = filterTools(mockTools, config); + expect(filtered).toHaveLength(2); + expect(filtered.map((t) => t.name)).toEqual([ + 'list_styles_tool', + 'create_style_tool' + ]); + }); + + it('should filter tools based on disabledTools', () => { + const config: ToolConfig = { + disabledTools: ['delete_style_tool', 'preview_style_tool'] + }; + const filtered = filterTools(mockTools, config); + expect(filtered).toHaveLength(2); + expect(filtered.map((t) => t.name)).toEqual([ + 'list_styles_tool', + 'create_style_tool' + ]); + }); + + it('should prioritize enabledTools over disabledTools', () => { + const config: ToolConfig = { + enabledTools: ['list_styles_tool'], + disabledTools: ['list_styles_tool', 'create_style_tool'] + }; + const filtered = filterTools(mockTools, config); + expect(filtered).toHaveLength(1); + expect(filtered.map((t) => t.name)).toEqual(['list_styles_tool']); + }); + + it('should handle non-existent tool names gracefully', () => { + const config: ToolConfig = { + enabledTools: ['list_styles_tool', 'non_existent_tool'] + }; + const filtered = filterTools(mockTools, config); + expect(filtered).toHaveLength(1); + expect(filtered.map((t) => t.name)).toEqual(['list_styles_tool']); + }); + + it('should return empty array when enabledTools is empty', () => { + const config: ToolConfig = { + enabledTools: [] + }; + const filtered = filterTools(mockTools, config); + expect(filtered).toHaveLength(0); + }); + + it('should return all tools when disabledTools is empty', () => { + const config: ToolConfig = { + disabledTools: [] + }; + const filtered = filterTools(mockTools, config); + expect(filtered).toEqual(mockTools); + }); + }); +}); diff --git a/src/config/toolConfig.ts b/src/config/toolConfig.ts new file mode 100644 index 0000000..d2b323c --- /dev/null +++ b/src/config/toolConfig.ts @@ -0,0 +1,55 @@ +import { ToolInstance } from '../tools/toolRegistry.js'; + +export interface ToolConfig { + enabledTools?: string[]; + disabledTools?: string[]; +} + +export function parseToolConfigFromArgs(): ToolConfig { + const args = process.argv.slice(2); + const config: ToolConfig = {}; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '--enable-tools') { + const value = args[++i]; + if (value) { + config.enabledTools = value.split(',').map((t) => t.trim()); + } + } else if (arg === '--disable-tools') { + const value = args[++i]; + if (value) { + config.disabledTools = value.split(',').map((t) => t.trim()); + } + } + } + + return config; +} + +export function filterTools( + tools: readonly ToolInstance[], + config: ToolConfig +): ToolInstance[] { + let filteredTools = [...tools]; + + // If enabledTools is specified, only those tools should be enabled + // This takes precedence over disabledTools + if (config.enabledTools !== undefined) { + filteredTools = filteredTools.filter((tool) => + config.enabledTools!.includes(tool.name) + ); + // Return early since enabledTools takes precedence + return filteredTools; + } + + // Apply disabledTools filter only if enabledTools is not specified + if (config.disabledTools && config.disabledTools.length > 0) { + filteredTools = filteredTools.filter( + (tool) => !config.disabledTools!.includes(tool.name) + ); + } + + return filteredTools; +} diff --git a/src/index.ts b/src/index.ts index 75ba865..234f2c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { BoundingBoxTool } from './tools/bounding-box-tool/BoundingBoxTool.js'; -import { CountryBoundingBoxTool } from './tools/bounding-box-tool/CountryBoundingBoxTool.js'; -import { CoordinateConversionTool } from './tools/coordinate-conversion-tool/CoordinateConversionTool.js'; -import { CreateStyleTool } from './tools/create-style-tool/CreateStyleTool.js'; -import { CreateTokenTool } from './tools/create-token-tool/CreateTokenTool.js'; -import { DeleteStyleTool } from './tools/delete-style-tool/DeleteStyleTool.js'; -import { GeojsonPreviewTool } from './tools/geojson-preview-tool/GeojsonPreviewTool.js'; -import { ListStylesTool } from './tools/list-styles-tool/ListStylesTool.js'; -import { ListTokensTool } from './tools/list-tokens-tool/ListTokensTool.js'; -import { PreviewStyleTool } from './tools/preview-style-tool/PreviewStyleTool.js'; -import { RetrieveStyleTool } from './tools/retrieve-style-tool/RetrieveStyleTool.js'; -import { UpdateStyleTool } from './tools/update-style-tool/UpdateStyleTool.js'; +import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; +import { getAllTools } from './tools/toolRegistry.js'; import { patchGlobalFetch } from './utils/requestUtils.js'; import { getVersionInfo } from './utils/versionUtils.js'; @@ -19,6 +9,13 @@ import { getVersionInfo } from './utils/versionUtils.js'; const versionInfo = getVersionInfo(); patchGlobalFetch(versionInfo); +// Parse configuration from command-line arguments +const config = parseToolConfigFromArgs(); + +// Get and filter tools based on configuration +const allTools = getAllTools(); +const enabledTools = filterTools(allTools, config); + // Create an MCP server const server = new McpServer( { @@ -33,22 +30,10 @@ const server = new McpServer( } ); -// INSERT NEW TOOL IMPORT HERE - -// Register tools -// INSERT NEW TOOL REGISTRATION HERE -new ListStylesTool().installTo(server); -new CreateStyleTool().installTo(server); -new RetrieveStyleTool().installTo(server); -new UpdateStyleTool().installTo(server); -new DeleteStyleTool().installTo(server); -new PreviewStyleTool().installTo(server); -new GeojsonPreviewTool().installTo(server); -new CreateTokenTool().installTo(server); -new ListTokensTool().installTo(server); -new BoundingBoxTool().installTo(server); -new CountryBoundingBoxTool().installTo(server); -new CoordinateConversionTool().installTo(server); +// Register enabled tools to the server +enabledTools.forEach((tool) => { + tool.installTo(server); +}); // Start the server const transport = new StdioServerTransport(); diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts new file mode 100644 index 0000000..c8dc93c --- /dev/null +++ b/src/tools/toolRegistry.ts @@ -0,0 +1,38 @@ +import { BoundingBoxTool } from './bounding-box-tool/BoundingBoxTool.js'; +import { CountryBoundingBoxTool } from './bounding-box-tool/CountryBoundingBoxTool.js'; +import { CoordinateConversionTool } from './coordinate-conversion-tool/CoordinateConversionTool.js'; +import { CreateStyleTool } from './create-style-tool/CreateStyleTool.js'; +import { CreateTokenTool } from './create-token-tool/CreateTokenTool.js'; +import { DeleteStyleTool } from './delete-style-tool/DeleteStyleTool.js'; +import { GeojsonPreviewTool } from './geojson-preview-tool/GeojsonPreviewTool.js'; +import { ListStylesTool } from './list-styles-tool/ListStylesTool.js'; +import { ListTokensTool } from './list-tokens-tool/ListTokensTool.js'; +import { PreviewStyleTool } from './preview-style-tool/PreviewStyleTool.js'; +import { RetrieveStyleTool } from './retrieve-style-tool/RetrieveStyleTool.js'; +import { UpdateStyleTool } from './update-style-tool/UpdateStyleTool.js'; + +// Central registry of all tools +export const ALL_TOOLS = [ + new ListStylesTool(), + new CreateStyleTool(), + new RetrieveStyleTool(), + new UpdateStyleTool(), + new DeleteStyleTool(), + new PreviewStyleTool(), + new GeojsonPreviewTool(), + new CreateTokenTool(), + new ListTokensTool(), + new BoundingBoxTool(), + new CountryBoundingBoxTool(), + new CoordinateConversionTool() +] as const; + +export type ToolInstance = (typeof ALL_TOOLS)[number]; + +export function getAllTools(): readonly ToolInstance[] { + return ALL_TOOLS; +} + +export function getToolByName(name: string): ToolInstance | undefined { + return ALL_TOOLS.find((tool) => tool.name === name); +}