From eacd6afdbf4219279d6dcc13d27218d8b9247a85 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Thu, 12 Feb 2026 01:25:23 -0500 Subject: [PATCH] feat: add API endpoint explorer tool (#71) Add explore_mapbox_api_tool to provide structured, queryable information about Mapbox API endpoints, operations, parameters, and authentication. Features: - List all available Mapbox APIs (7 APIs with 24 operations total) - View operations for specific APIs - Get detailed endpoint information with parameters, scopes, rate limits - Optional detailed mode includes example requests/responses - Proper MCP structured content support with separate text and data Coverage: - Geocoding API (forward/reverse geocoding) - Styles API (CRUD operations) - Tokens API (token management) - Static Images API (map image generation) - Directions API (route calculation) - Tilequery API (vector tile queries) - Feedback API (user feedback events) Implementation: - Created mapboxApiEndpoints.ts with curated API definitions - Implemented BaseTool with input/output schemas - Returns both markdown content and structured data - 21 comprehensive test cases, all passing - Updated tool registry and documentation Complements get_latest_mapbox_docs_tool by providing structured API reference data instead of prose documentation. Co-Authored-By: Claude Sonnet 4.5 --- CHANGELOG.md | 10 + README.md | 17 + src/constants/mapboxApiEndpoints.ts | 1278 +++++++++++++++++ .../ExploreMapboxApiTool.input.schema.ts | 25 + .../ExploreMapboxApiTool.output.schema.ts | 59 + .../ExploreMapboxApiTool.ts | 292 ++++ src/tools/toolRegistry.ts | 2 + .../tool-naming-convention.test.ts.snap | 5 + .../ExploreMapboxApiTool.test.ts | 321 +++++ 9 files changed, 2009 insertions(+) create mode 100644 src/constants/mapboxApiEndpoints.ts create mode 100644 src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.input.schema.ts create mode 100644 src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.output.schema.ts create mode 100644 src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.ts create mode 100644 test/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8d818..88102b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## Unreleased +### Features Added + +- **API Endpoint Explorer Tool**: New `explore_mapbox_api_tool` provides structured, queryable information about Mapbox APIs (#71) + - List all available Mapbox APIs with descriptions and operation counts + - View detailed operations for specific APIs (geocoding, styles, tokens, static-images, directions, tilequery, feedback) + - Get complete endpoint details including HTTP methods, parameters, required scopes, and rate limits + - Optional detailed mode includes example requests and responses + - Complements `get_latest_mapbox_docs_tool` by providing structured API reference data + - No API access required - works with curated endpoint definitions + ### Documentation - **PR Guidelines**: Added CHANGELOG requirement to CLAUDE.md (#67) diff --git a/README.md b/README.md index 85143d6..6a0f917 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,23 @@ The `MAPBOX_ACCESS_TOKEN` environment variable is required. **Each tool requires 📖 **[See more examples and interactive demo →](./docs/mapbox-docs-tool-demo.md)** +**explore_mapbox_api_tool** - Explore Mapbox API endpoints with structured, queryable information. Get details about available APIs, their operations, required parameters, authentication scopes, and rate limits. This tool provides structured API reference data complementing the prose documentation from `get_latest_mapbox_docs_tool`. + +**Features:** + +- List all available Mapbox APIs (geocoding, styles, tokens, static-images, directions, tilequery, feedback) +- View operations for a specific API +- Get detailed endpoint information including HTTP methods, parameters, and examples +- See required token scopes and rate limits for each operation + +**Example prompts:** + +- "What Mapbox APIs are available?" +- "Show me the geocoding API operations" +- "What parameters does the forward geocoding endpoint accept?" +- "What scopes do I need for the Styles API?" +- "How do I use the directions API? Show me examples" + ### Reference Tools **get_reference_tool** - Access static Mapbox reference documentation and schemas. This tool provides essential reference information that helps AI assistants understand Mapbox concepts and build correct styles and tokens. diff --git a/src/constants/mapboxApiEndpoints.ts b/src/constants/mapboxApiEndpoints.ts new file mode 100644 index 0000000..9c2bbe2 --- /dev/null +++ b/src/constants/mapboxApiEndpoints.ts @@ -0,0 +1,1278 @@ +/** + * Mapbox API endpoint definitions for the explore_mapbox_api_tool. + * Provides structured, queryable information about Mapbox APIs. + */ + +export interface Parameter { + name: string; + type: string; + required: boolean; + description: string; + default?: any; + enum?: string[]; +} + +export interface ApiOperation { + name: string; + operationId: string; + description: string; + endpoint: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + pathParameters?: Parameter[]; + queryParameters?: Parameter[]; + bodyParameters?: Parameter[]; + requiredScopes: string[]; + rateLimit?: { + requests: number; + period: string; + notes?: string; + }; + exampleRequest?: string; + exampleResponse?: string; +} + +export interface MapboxApiEndpoint { + api: string; + category: string; + description: string; + docsUrl: string; + operations: ApiOperation[]; +} + +export const MAPBOX_API_ENDPOINTS: MapboxApiEndpoint[] = [ + { + api: 'geocoding', + category: 'Search', + description: + 'Convert location names to coordinates (forward) and coordinates to location names (reverse)', + docsUrl: 'https://docs.mapbox.com/api/search/geocoding/', + operations: [ + { + name: 'Forward Geocoding', + operationId: 'forward-geocode', + description: 'Search for places by name and get geographic coordinates', + endpoint: '/geocoding/v5/{mode}/{query}.json', + method: 'GET', + pathParameters: [ + { + name: 'mode', + type: 'string', + required: true, + description: 'Geocoding mode', + enum: ['mapbox.places', 'mapbox.places-permanent'] + }, + { + name: 'query', + type: 'string', + required: true, + description: + 'The location text to search for (e.g., "San Francisco" or "1600 Pennsylvania Ave")' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + }, + { + name: 'autocomplete', + type: 'boolean', + required: false, + description: 'Whether to return autocomplete results', + default: true + }, + { + name: 'bbox', + type: 'string', + required: false, + description: + 'Bounding box to limit results (minLon,minLat,maxLon,maxLat)' + }, + { + name: 'country', + type: 'string', + required: false, + description: + 'Comma-separated ISO 3166-1 alpha-2 country codes to limit results' + }, + { + name: 'fuzzyMatch', + type: 'boolean', + required: false, + description: 'Whether to use fuzzy matching', + default: true + }, + { + name: 'language', + type: 'string', + required: false, + description: 'Comma-separated ISO 639-1 language codes for results' + }, + { + name: 'limit', + type: 'number', + required: false, + description: 'Maximum number of results (1-10)', + default: 5 + }, + { + name: 'proximity', + type: 'string', + required: false, + description: 'Bias results toward a location (longitude,latitude)' + }, + { + name: 'types', + type: 'string', + required: false, + description: 'Comma-separated feature types to filter results', + enum: [ + 'country', + 'region', + 'postcode', + 'district', + 'place', + 'locality', + 'neighborhood', + 'address', + 'poi' + ] + } + ], + requiredScopes: ['styles:read', 'geocoding:read'], + rateLimit: { + requests: 600, + period: 'minute', + notes: 'Free tier: 100,000 requests/month' + }, + exampleRequest: + 'https://api.mapbox.com/geocoding/v5/mapbox.places/San%20Francisco.json?access_token=YOUR_TOKEN', + exampleResponse: JSON.stringify( + { + type: 'FeatureCollection', + query: ['san', 'francisco'], + features: [ + { + id: 'place.123', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: {}, + text: 'San Francisco', + place_name: 'San Francisco, California, United States', + center: [-122.4194, 37.7749], + geometry: { + type: 'Point', + coordinates: [-122.4194, 37.7749] + } + } + ] + }, + null, + 2 + ) + }, + { + name: 'Reverse Geocoding', + operationId: 'reverse-geocode', + description: 'Get place names from geographic coordinates', + endpoint: '/geocoding/v5/{mode}/{longitude},{latitude}.json', + method: 'GET', + pathParameters: [ + { + name: 'mode', + type: 'string', + required: true, + description: 'Geocoding mode', + enum: ['mapbox.places', 'mapbox.places-permanent'] + }, + { + name: 'longitude', + type: 'number', + required: true, + description: 'Longitude coordinate (-180 to 180)' + }, + { + name: 'latitude', + type: 'number', + required: true, + description: 'Latitude coordinate (-90 to 90)' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + }, + { + name: 'country', + type: 'string', + required: false, + description: + 'Comma-separated ISO 3166-1 alpha-2 country codes to limit results' + }, + { + name: 'language', + type: 'string', + required: false, + description: 'Comma-separated ISO 639-1 language codes for results' + }, + { + name: 'limit', + type: 'number', + required: false, + description: 'Maximum number of results (1-5)', + default: 1 + }, + { + name: 'types', + type: 'string', + required: false, + description: 'Comma-separated feature types to filter results' + } + ], + requiredScopes: ['styles:read', 'geocoding:read'], + rateLimit: { + requests: 600, + period: 'minute', + notes: 'Free tier: 100,000 requests/month' + }, + exampleRequest: + 'https://api.mapbox.com/geocoding/v5/mapbox.places/-122.4194,37.7749.json?access_token=YOUR_TOKEN', + exampleResponse: JSON.stringify( + { + type: 'FeatureCollection', + query: [-122.4194, 37.7749], + features: [ + { + id: 'place.123', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: {}, + text: 'San Francisco', + place_name: 'San Francisco, California, United States', + center: [-122.4194, 37.7749], + geometry: { + type: 'Point', + coordinates: [-122.4194, 37.7749] + } + } + ] + }, + null, + 2 + ) + } + ] + }, + { + api: 'styles', + category: 'Maps', + description: + 'Create, read, update, and delete map styles. See also: create_style_tool, update_style_tool, retrieve_style_tool, list_styles_tool', + docsUrl: 'https://docs.mapbox.com/api/maps/styles/', + operations: [ + { + name: 'Create Style', + operationId: 'create-style', + description: + 'Create a new map style (use create_style_tool for implementation)', + endpoint: '/styles/v1/{username}', + method: 'POST', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + } + ], + bodyParameters: [ + { + name: 'name', + type: 'string', + required: true, + description: 'Style name' + }, + { + name: 'version', + type: 'number', + required: true, + description: 'Style specification version', + default: 8 + }, + { + name: 'sources', + type: 'object', + required: true, + description: 'Data sources for the style' + }, + { + name: 'layers', + type: 'array', + required: true, + description: 'Style layers' + }, + { + name: 'glyphs', + type: 'string', + required: false, + description: 'Font glyphs URL template' + }, + { + name: 'sprite', + type: 'string', + required: false, + description: 'Sprite image URL' + } + ], + requiredScopes: ['styles:write'], + rateLimit: { + requests: 100, + period: 'minute' + }, + exampleRequest: + 'POST https://api.mapbox.com/styles/v1/your-username?access_token=YOUR_TOKEN', + exampleResponse: JSON.stringify( + { + version: 8, + name: 'My Style', + id: 'cjhtjdksl00009op8t7eee8k2', + owner: 'your-username', + created: '2023-01-01T00:00:00.000Z', + modified: '2023-01-01T00:00:00.000Z' + }, + null, + 2 + ) + }, + { + name: 'Retrieve Style', + operationId: 'retrieve-style', + description: + 'Get a map style by ID (use retrieve_style_tool for implementation)', + endpoint: '/styles/v1/{username}/{style_id}', + method: 'GET', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'style_id', + type: 'string', + required: true, + description: 'Style ID' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + } + ], + requiredScopes: ['styles:read'], + rateLimit: { + requests: 300, + period: 'minute' + } + }, + { + name: 'Update Style', + operationId: 'update-style', + description: + 'Update an existing map style (use update_style_tool for implementation)', + endpoint: '/styles/v1/{username}/{style_id}', + method: 'PATCH', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'style_id', + type: 'string', + required: true, + description: 'Style ID' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + } + ], + bodyParameters: [ + { + name: 'name', + type: 'string', + required: false, + description: 'Updated style name' + }, + { + name: 'sources', + type: 'object', + required: false, + description: 'Updated data sources' + }, + { + name: 'layers', + type: 'array', + required: false, + description: 'Updated style layers' + } + ], + requiredScopes: ['styles:write'], + rateLimit: { + requests: 100, + period: 'minute' + } + }, + { + name: 'Delete Style', + operationId: 'delete-style', + description: 'Delete a map style', + endpoint: '/styles/v1/{username}/{style_id}', + method: 'DELETE', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'style_id', + type: 'string', + required: true, + description: 'Style ID' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + } + ], + requiredScopes: ['styles:write'], + rateLimit: { + requests: 100, + period: 'minute' + } + }, + { + name: 'List Styles', + operationId: 'list-styles', + description: + 'List all styles for a user (use list_styles_tool for implementation)', + endpoint: '/styles/v1/{username}', + method: 'GET', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + } + ], + requiredScopes: ['styles:list'], + rateLimit: { + requests: 300, + period: 'minute' + } + } + ] + }, + { + api: 'tokens', + category: 'Account', + description: + 'Manage access tokens and their scopes. See also: list_tokens_tool, create_token_tool, update_token_tool, delete_token_tool, retrieve_token_tool', + docsUrl: 'https://docs.mapbox.com/api/accounts/tokens/', + operations: [ + { + name: 'List Tokens', + operationId: 'list-tokens', + description: + 'List all access tokens for your account (use list_tokens_tool for implementation)', + endpoint: '/tokens/v2/{username}', + method: 'GET', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token with tokens:read scope' + } + ], + requiredScopes: ['tokens:read'], + rateLimit: { + requests: 600, + period: 'minute' + } + }, + { + name: 'Create Token', + operationId: 'create-token', + description: + 'Create a new access token (use create_token_tool for implementation)', + endpoint: '/tokens/v2/{username}', + method: 'POST', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token with tokens:write scope' + } + ], + bodyParameters: [ + { + name: 'note', + type: 'string', + required: false, + description: 'Human-readable description' + }, + { + name: 'scopes', + type: 'array', + required: true, + description: + 'Array of token scopes (e.g., ["styles:read", "fonts:read"])' + }, + { + name: 'resources', + type: 'array', + required: false, + description: 'URL restrictions for token usage' + }, + { + name: 'allowedUrls', + type: 'array', + required: false, + description: 'Allowed referrer URLs' + } + ], + requiredScopes: ['tokens:write'], + rateLimit: { + requests: 100, + period: 'minute' + } + }, + { + name: 'Update Token', + operationId: 'update-token', + description: + 'Update an existing token (use update_token_tool for implementation)', + endpoint: '/tokens/v2/{username}/{token_id}', + method: 'PATCH', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'token_id', + type: 'string', + required: true, + description: 'Token ID to update' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token with tokens:write scope' + } + ], + bodyParameters: [ + { + name: 'note', + type: 'string', + required: false, + description: 'Updated description' + }, + { + name: 'scopes', + type: 'array', + required: false, + description: 'Updated scopes array' + } + ], + requiredScopes: ['tokens:write'], + rateLimit: { + requests: 100, + period: 'minute' + } + }, + { + name: 'Delete Token', + operationId: 'delete-token', + description: + 'Delete an access token (use delete_token_tool for implementation)', + endpoint: '/tokens/v2/{username}/{token_id}', + method: 'DELETE', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'token_id', + type: 'string', + required: true, + description: 'Token ID to delete' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token with tokens:write scope' + } + ], + requiredScopes: ['tokens:write'], + rateLimit: { + requests: 100, + period: 'minute' + } + }, + { + name: 'Retrieve Token', + operationId: 'retrieve-token', + description: + 'Get details about a specific token (use retrieve_token_tool for implementation)', + endpoint: '/tokens/v2/{username}/{token_id}', + method: 'GET', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'token_id', + type: 'string', + required: true, + description: 'Token ID' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token with tokens:read scope' + } + ], + requiredScopes: ['tokens:read'], + rateLimit: { + requests: 600, + period: 'minute' + } + } + ] + }, + { + api: 'static-images', + category: 'Maps', + description: 'Generate static map images from styles', + docsUrl: 'https://docs.mapbox.com/api/maps/static-images/', + operations: [ + { + name: 'Static Map Image', + operationId: 'static-image', + description: 'Request a static map image', + endpoint: + '/styles/v1/{username}/{style_id}/static/{overlay}/{lon},{lat},{zoom},{bearing},{pitch}/{width}x{height}{@2x}', + method: 'GET', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'style_id', + type: 'string', + required: true, + description: 'Style ID or Mapbox style (e.g., "streets-v12")' + }, + { + name: 'overlay', + type: 'string', + required: false, + description: 'GeoJSON overlay, marker, or path to add to the map' + }, + { + name: 'lon', + type: 'number', + required: true, + description: 'Longitude for map center' + }, + { + name: 'lat', + type: 'number', + required: true, + description: 'Latitude for map center' + }, + { + name: 'zoom', + type: 'number', + required: true, + description: 'Zoom level (0-22)' + }, + { + name: 'bearing', + type: 'number', + required: false, + description: 'Map bearing in degrees (0-359)', + default: 0 + }, + { + name: 'pitch', + type: 'number', + required: false, + description: 'Map pitch in degrees (0-60)', + default: 0 + }, + { + name: 'width', + type: 'number', + required: true, + description: 'Image width in pixels (1-1280)' + }, + { + name: 'height', + type: 'number', + required: true, + description: 'Image height in pixels (1-1280)' + }, + { + name: '@2x', + type: 'string', + required: false, + description: 'Add "@2x" for retina/high-DPI display' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + }, + { + name: 'attribution', + type: 'boolean', + required: false, + description: 'Whether to include attribution', + default: true + }, + { + name: 'logo', + type: 'boolean', + required: false, + description: 'Whether to include Mapbox logo', + default: true + } + ], + requiredScopes: ['styles:tiles'], + rateLimit: { + requests: 1200, + period: 'minute', + notes: 'Free tier: 50,000 requests/month' + }, + exampleRequest: + 'https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/-122.4194,37.7749,12,0,0/600x400@2x?access_token=YOUR_TOKEN', + exampleResponse: '(Binary PNG image data)' + }, + { + name: 'Static Map with Overlay', + operationId: 'static-image-overlay', + description: 'Request a static map with GeoJSON overlay', + endpoint: + '/styles/v1/{username}/{style_id}/static/{overlay}/{lon},{lat},{zoom}/{width}x{height}{@2x}', + method: 'GET', + pathParameters: [ + { + name: 'username', + type: 'string', + required: true, + description: 'Mapbox username' + }, + { + name: 'style_id', + type: 'string', + required: true, + description: 'Style ID' + }, + { + name: 'overlay', + type: 'string', + required: true, + description: + 'URL-encoded GeoJSON or marker syntax (e.g., "geojson({...})" or "pin-s+f00(-122.4,37.8)")' + }, + { + name: 'lon', + type: 'number', + required: true, + description: 'Longitude for map center' + }, + { + name: 'lat', + type: 'number', + required: true, + description: 'Latitude for map center' + }, + { + name: 'zoom', + type: 'number', + required: true, + description: 'Zoom level (0-22)' + }, + { + name: 'width', + type: 'number', + required: true, + description: 'Image width in pixels (1-1280)' + }, + { + name: 'height', + type: 'number', + required: true, + description: 'Image height in pixels (1-1280)' + }, + { + name: '@2x', + type: 'string', + required: false, + description: 'Add "@2x" for retina display' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + } + ], + requiredScopes: ['styles:tiles'], + rateLimit: { + requests: 1200, + period: 'minute' + } + } + ] + }, + { + api: 'directions', + category: 'Navigation', + description: + 'Calculate routes between coordinates with turn-by-turn directions', + docsUrl: 'https://docs.mapbox.com/api/navigation/directions/', + operations: [ + { + name: 'Directions', + operationId: 'directions', + description: 'Calculate a route between waypoints', + endpoint: '/directions/v5/mapbox/{profile}/{coordinates}', + method: 'GET', + pathParameters: [ + { + name: 'profile', + type: 'string', + required: true, + description: 'Routing profile', + enum: ['driving-traffic', 'driving', 'walking', 'cycling'] + }, + { + name: 'coordinates', + type: 'string', + required: true, + description: + 'Semicolon-separated list of {longitude},{latitude} coordinates (2-25 waypoints)' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + }, + { + name: 'alternatives', + type: 'boolean', + required: false, + description: 'Whether to return alternative routes', + default: false + }, + { + name: 'geometries', + type: 'string', + required: false, + description: 'Route geometry format', + enum: ['geojson', 'polyline', 'polyline6'], + default: 'polyline' + }, + { + name: 'overview', + type: 'string', + required: false, + description: 'Level of detail for route geometry', + enum: ['full', 'simplified', 'false'], + default: 'simplified' + }, + { + name: 'steps', + type: 'boolean', + required: false, + description: 'Whether to include turn-by-turn instructions', + default: false + }, + { + name: 'continue_straight', + type: 'boolean', + required: false, + description: 'Force route to go straight at waypoints', + default: false + }, + { + name: 'waypoint_names', + type: 'string', + required: false, + description: 'Semicolon-separated list of custom waypoint names' + }, + { + name: 'banner_instructions', + type: 'boolean', + required: false, + description: 'Whether to include banner instructions', + default: false + }, + { + name: 'language', + type: 'string', + required: false, + description: 'Language for instructions (ISO 639-1 code)', + default: 'en' + }, + { + name: 'voice_instructions', + type: 'boolean', + required: false, + description: 'Whether to include voice instructions', + default: false + } + ], + requiredScopes: ['directions:read'], + rateLimit: { + requests: 300, + period: 'minute', + notes: 'Free tier: 100,000 requests/month' + }, + exampleRequest: + 'https://api.mapbox.com/directions/v5/mapbox/driving/-122.42,37.78;-122.45,37.76?steps=true&access_token=YOUR_TOKEN', + exampleResponse: JSON.stringify( + { + routes: [ + { + distance: 3492.9, + duration: 645.2, + geometry: 'encoded-polyline-string', + legs: [ + { + distance: 3492.9, + duration: 645.2, + steps: [ + { + distance: 150.5, + duration: 23.4, + geometry: 'encoded-polyline', + name: 'Market Street', + maneuver: { + type: 'depart', + instruction: 'Head east on Market Street' + } + } + ] + } + ] + } + ], + waypoints: [ + { name: 'Market Street', location: [-122.42, 37.78] }, + { name: 'Valencia Street', location: [-122.45, 37.76] } + ] + }, + null, + 2 + ) + } + ] + }, + { + api: 'tilequery', + category: 'Maps', + description: + 'Query vector tile data at specific coordinates. See also: query_mapbox_tilesets_tool', + docsUrl: 'https://docs.mapbox.com/api/maps/tilequery/', + operations: [ + { + name: 'Tilequery', + operationId: 'tilequery', + description: + 'Retrieve features from vector tiles at a point (use query_mapbox_tilesets_tool for implementation)', + endpoint: '/v4/{tileset_id}/tilequery/{lon},{lat}.json', + method: 'GET', + pathParameters: [ + { + name: 'tileset_id', + type: 'string', + required: true, + description: 'Tileset identifier (e.g., "mapbox.mapbox-streets-v8")' + }, + { + name: 'lon', + type: 'number', + required: true, + description: 'Longitude coordinate' + }, + { + name: 'lat', + type: 'number', + required: true, + description: 'Latitude coordinate' + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + }, + { + name: 'radius', + type: 'number', + required: false, + description: 'Search radius in meters (0-1000)', + default: 0 + }, + { + name: 'limit', + type: 'number', + required: false, + description: 'Maximum number of results (1-50)', + default: 5 + }, + { + name: 'dedupe', + type: 'boolean', + required: false, + description: 'Whether to remove duplicate results', + default: true + }, + { + name: 'geometry', + type: 'string', + required: false, + description: 'Filter by geometry type', + enum: ['point', 'linestring', 'polygon'] + }, + { + name: 'layers', + type: 'string', + required: false, + description: 'Comma-separated list of layer IDs to query' + } + ], + requiredScopes: ['styles:tiles'], + rateLimit: { + requests: 600, + period: 'minute', + notes: 'Free tier: 100,000 requests/month' + }, + exampleRequest: + 'https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/tilequery/-122.42,37.78.json?radius=10&limit=5&access_token=YOUR_TOKEN', + exampleResponse: JSON.stringify( + { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + id: 123, + geometry: { + type: 'Point', + coordinates: [-122.42, 37.78] + }, + properties: { + class: 'street', + name: 'Market Street', + type: 'primary' + } + } + ] + }, + null, + 2 + ) + } + ] + }, + { + api: 'feedback', + category: 'Maps', + description: + 'Submit user feedback events for map data quality. See also: create_feedback_event_tool, get_feedback_events_tool', + docsUrl: 'https://docs.mapbox.com/api/navigation/feedback/', + operations: [ + { + name: 'Create Feedback Event', + operationId: 'create-feedback', + description: + 'Submit a map feedback event (use create_feedback_event_tool for implementation)', + endpoint: '/feedback/v1/{eventType}', + method: 'POST', + pathParameters: [ + { + name: 'eventType', + type: 'string', + required: true, + description: 'Type of feedback event', + enum: [ + 'incorrect-navigation', + 'not-allowed', + 'road-closure', + 'other' + ] + } + ], + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + } + ], + bodyParameters: [ + { + name: 'coordinates', + type: 'array', + required: true, + description: 'Location of feedback [longitude, latitude]' + }, + { + name: 'description', + type: 'string', + required: false, + description: 'Detailed description of the issue' + }, + { + name: 'userId', + type: 'string', + required: false, + description: 'Anonymous user identifier' + } + ], + requiredScopes: ['feedback:write'], + rateLimit: { + requests: 100, + period: 'minute' + } + }, + { + name: 'Get Feedback Events', + operationId: 'list-feedback', + description: + 'Retrieve submitted feedback events (use get_feedback_events_tool for implementation)', + endpoint: '/feedback/v1/events', + method: 'GET', + queryParameters: [ + { + name: 'access_token', + type: 'string', + required: true, + description: 'Mapbox access token' + }, + { + name: 'start', + type: 'string', + required: false, + description: 'Start date (ISO 8601)' + }, + { + name: 'end', + type: 'string', + required: false, + description: 'End date (ISO 8601)' + } + ], + requiredScopes: ['feedback:read'], + rateLimit: { + requests: 300, + period: 'minute' + } + } + ] + } +]; diff --git a/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.input.schema.ts b/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.input.schema.ts new file mode 100644 index 0000000..b2878a5 --- /dev/null +++ b/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.input.schema.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +export const ExploreMapboxApiInputSchema = z.object({ + api: z + .string() + .optional() + .describe( + 'API name to explore: geocoding, styles, tokens, static-images, directions, tilequery, or feedback. Omit to list all APIs.' + ), + operation: z + .string() + .optional() + .describe( + 'Specific operation ID within the API (e.g., "forward-geocode", "create-style"). Requires api parameter.' + ), + details: z + .boolean() + .optional() + .default(false) + .describe( + 'Include full parameter descriptions and example request/response. Default: false.' + ) +}); + +export type ExploreMapboxApiInput = z.infer; diff --git a/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.output.schema.ts b/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.output.schema.ts new file mode 100644 index 0000000..26e0f93 --- /dev/null +++ b/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.output.schema.ts @@ -0,0 +1,59 @@ +import { z } from 'zod'; + +const ParameterSchema = z.object({ + name: z.string(), + type: z.string(), + required: z.boolean(), + description: z.string(), + default: z.any().optional(), + enum: z.array(z.string()).optional() +}); + +const RateLimitSchema = z.object({ + requests: z.number(), + period: z.string(), + notes: z.string().optional() +}); + +const ApiOperationSchema = z.object({ + name: z.string(), + operationId: z.string(), + description: z.string(), + endpoint: z.string(), + method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']), + pathParameters: z.array(ParameterSchema).optional(), + queryParameters: z.array(ParameterSchema).optional(), + bodyParameters: z.array(ParameterSchema).optional(), + requiredScopes: z.array(z.string()), + rateLimit: RateLimitSchema.optional(), + exampleRequest: z.string().optional(), + exampleResponse: z.string().optional() +}); + +const ApiSummarySchema = z.object({ + api: z.string(), + category: z.string(), + description: z.string(), + docsUrl: z.string(), + operationCount: z.number() +}); + +const OperationSummarySchema = z.object({ + operationId: z.string(), + name: z.string(), + method: z.string(), + endpoint: z.string(), + description: z.string() +}); + +// Output schema defines only the structured content portion +// The text content is returned separately in the standard 'content' field +export const ExploreMapboxApiOutputSchema = z.object({ + apis: z.array(ApiSummarySchema).optional(), + operations: z.array(OperationSummarySchema).optional(), + operationDetails: ApiOperationSchema.optional() +}); + +export type ExploreMapboxApiOutput = z.infer< + typeof ExploreMapboxApiOutputSchema +>; diff --git a/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.ts b/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.ts new file mode 100644 index 0000000..e42edf1 --- /dev/null +++ b/src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.ts @@ -0,0 +1,292 @@ +import { BaseTool } from '../BaseTool.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { + ExploreMapboxApiInputSchema, + type ExploreMapboxApiInput +} from './ExploreMapboxApiTool.input.schema.js'; +import { ExploreMapboxApiOutputSchema } from './ExploreMapboxApiTool.output.schema.js'; +import { + MAPBOX_API_ENDPOINTS, + type MapboxApiEndpoint, + type ApiOperation, + type Parameter +} from '../../constants/mapboxApiEndpoints.js'; + +/** + * Tool for exploring Mapbox API endpoints and operations. + * + * Provides structured, queryable information about Mapbox APIs including: + * - Available APIs and their operations + * - Endpoint URLs and HTTP methods + * - Required parameters and their types + * - Authentication scopes + * - Rate limits + * - Example requests and responses + * + * This complements search_mapbox_docs_tool by providing structured API reference + * data instead of prose documentation. + */ +export class ExploreMapboxApiTool extends BaseTool< + typeof ExploreMapboxApiInputSchema, + typeof ExploreMapboxApiOutputSchema +> { + name = 'explore_mapbox_api_tool' as const; + description = + 'Explore Mapbox API endpoints, operations, parameters, and authentication requirements. Get structured information about available APIs, their operations, required parameters, token scopes, and rate limits. Use without parameters to list all APIs, with api parameter to list operations, or with api + operation to get full endpoint details.'; + + annotations = { + title: 'Explore Mapbox API' + }; + + constructor() { + super({ + inputSchema: ExploreMapboxApiInputSchema, + outputSchema: ExploreMapboxApiOutputSchema + }); + } + + async execute(input: ExploreMapboxApiInput): Promise { + // Case 1: List all available APIs + if (!input.api) { + return this.listAllApis(); + } + + // Find the requested API + const apiEndpoint = MAPBOX_API_ENDPOINTS.find( + (endpoint) => endpoint.api.toLowerCase() === input.api!.toLowerCase() + ); + + if (!apiEndpoint) { + const availableApis = MAPBOX_API_ENDPOINTS.map((e) => e.api).join(', '); + return { + content: [ + { + type: 'text', + text: `❌ API "${input.api}" not found.\n\nAvailable APIs: ${availableApis}\n\nUse explore_mapbox_api_tool without parameters to see all APIs with descriptions.` + } + ] + }; + } + + // Case 2: List operations for a specific API + if (!input.operation) { + return this.listApiOperations(apiEndpoint); + } + + // Case 3: Get details for a specific operation + const operation = apiEndpoint.operations.find( + (op) => op.operationId.toLowerCase() === input.operation!.toLowerCase() + ); + + if (!operation) { + const availableOps = apiEndpoint.operations + .map((op) => op.operationId) + .join(', '); + return { + content: [ + { + type: 'text', + text: `❌ Operation "${input.operation}" not found in ${apiEndpoint.api} API.\n\nAvailable operations: ${availableOps}` + } + ] + }; + } + + return this.getOperationDetails( + apiEndpoint, + operation, + input.details || false + ); + } + + /** + * List all available Mapbox APIs with summary information + */ + private listAllApis(): CallToolResult { + const apiSummaries = MAPBOX_API_ENDPOINTS.map((endpoint) => ({ + api: endpoint.api, + category: endpoint.category, + description: endpoint.description, + docsUrl: endpoint.docsUrl, + operationCount: endpoint.operations.length + })); + + let text = '# Mapbox APIs\n\n'; + text += 'Available APIs for geospatial operations:\n\n'; + + for (const api of apiSummaries) { + text += `## ${api.api}\n`; + text += `**Category:** ${api.category}\n`; + text += `**Operations:** ${api.operationCount}\n`; + text += `**Description:** ${api.description}\n`; + text += `**Documentation:** ${api.docsUrl}\n\n`; + } + + text += '\n---\n\n'; + text += '**Next steps:**\n'; + text += + '- Use `{ api: "api-name" }` to list operations for a specific API\n'; + text += + '- Use `{ api: "api-name", operation: "operation-id" }` for full endpoint details\n'; + text += + '- Add `{ details: true }` to include example requests and responses\n'; + + return { + content: [{ type: 'text', text }], + structuredContent: { + apis: apiSummaries + } + }; + } + + /** + * List all operations for a specific API + */ + private listApiOperations(apiEndpoint: MapboxApiEndpoint): CallToolResult { + const operationSummaries = apiEndpoint.operations.map((op) => ({ + operationId: op.operationId, + name: op.name, + method: op.method, + endpoint: op.endpoint, + description: op.description + })); + + let text = `# ${apiEndpoint.api} API\n\n`; + text += `**Category:** ${apiEndpoint.category}\n`; + text += `**Description:** ${apiEndpoint.description}\n`; + text += `**Documentation:** ${apiEndpoint.docsUrl}\n\n`; + text += `## Operations (${apiEndpoint.operations.length})\n\n`; + + for (const op of operationSummaries) { + text += `### ${op.name}\n`; + text += `**Operation ID:** \`${op.operationId}\`\n`; + text += `**Method:** \`${op.method}\`\n`; + text += `**Endpoint:** \`${op.endpoint}\`\n`; + text += `**Description:** ${op.description}\n\n`; + } + + text += '\n---\n\n'; + text += '**Next steps:**\n'; + text += `- Use \`{ api: "${apiEndpoint.api}", operation: "operation-id" }\` for full details\n`; + text += + '- Add `{ details: true }` to include example requests and responses\n'; + + return { + content: [{ type: 'text', text }], + structuredContent: { + operations: operationSummaries + } + }; + } + + /** + * Get full details for a specific operation + */ + private getOperationDetails( + apiEndpoint: MapboxApiEndpoint, + operation: ApiOperation, + includeDetails: boolean + ): CallToolResult { + let text = `# ${operation.name}\n\n`; + text += `**API:** ${apiEndpoint.api}\n`; + text += `**Operation ID:** \`${operation.operationId}\`\n`; + text += `**Description:** ${operation.description}\n\n`; + + // HTTP Details + text += `## HTTP Request\n\n`; + text += `**Method:** \`${operation.method}\`\n`; + text += `**Endpoint:** \`${operation.endpoint}\`\n`; + text += `**Base URL:** \`https://api.mapbox.com\`\n\n`; + + // Parameters + if (operation.pathParameters && operation.pathParameters.length > 0) { + text += `### Path Parameters\n\n`; + text += this.formatParameters(operation.pathParameters); + } + + if (operation.queryParameters && operation.queryParameters.length > 0) { + text += `### Query Parameters\n\n`; + text += this.formatParameters(operation.queryParameters); + } + + if (operation.bodyParameters && operation.bodyParameters.length > 0) { + text += `### Body Parameters\n\n`; + text += this.formatParameters(operation.bodyParameters); + } + + // Authentication + text += `## Authentication\n\n`; + text += `**Required Scopes:** ${operation.requiredScopes.map((s) => `\`${s}\``).join(', ')}\n\n`; + text += + 'Your access token must have these scopes. Use `list_tokens_tool` to check token scopes.\n\n'; + + // Rate Limits + if (operation.rateLimit) { + text += `## Rate Limits\n\n`; + text += `**Limit:** ${operation.rateLimit.requests} requests per ${operation.rateLimit.period}\n`; + if (operation.rateLimit.notes) { + text += `**Notes:** ${operation.rateLimit.notes}\n`; + } + text += '\n'; + } + + // Examples (only if details=true) + if (includeDetails) { + if (operation.exampleRequest) { + text += `## Example Request\n\n`; + text += '```\n'; + text += operation.exampleRequest; + text += '\n```\n\n'; + } + + if (operation.exampleResponse) { + text += `## Example Response\n\n`; + text += '```json\n'; + text += operation.exampleResponse; + text += '\n```\n\n'; + } + } else { + text += '\n---\n\n'; + text += + '**Tip:** Add `{ details: true }` to see example request and response.\n'; + } + + // Documentation link + text += `\n**Full Documentation:** ${apiEndpoint.docsUrl}\n`; + + return { + content: [{ type: 'text', text }], + structuredContent: { + operationDetails: operation + } + }; + } + + /** + * Format parameter list as markdown table + */ + private formatParameters(parameters: Parameter[]): string { + let text = '| Name | Type | Required | Description |\n'; + text += '|------|------|----------|-------------|\n'; + + for (const param of parameters) { + const name = `\`${param.name}\``; + const type = `\`${param.type}\``; + const required = param.required ? '✅ Yes' : '❌ No'; + let description = param.description; + + if (param.default !== undefined) { + description += ` (default: \`${JSON.stringify(param.default)}\`)`; + } + + if (param.enum && param.enum.length > 0) { + description += ` Options: ${param.enum.map((e) => `\`${e}\``).join(', ')}`; + } + + text += `| ${name} | ${type} | ${required} | ${description} |\n`; + } + + text += '\n'; + return text; + } +} diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index 481ea7e..e3d723b 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -9,6 +9,7 @@ import { CoordinateConversionTool } from './coordinate-conversion-tool/Coordinat 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 { ExploreMapboxApiTool } from './explore-mapbox-api-tool/ExploreMapboxApiTool.js'; import { GetFeedbackTool } from './get-feedback-tool/GetFeedbackTool.js'; import { ListFeedbackTool } from './list-feedback-tool/ListFeedbackTool.js'; import { GeojsonPreviewTool } from './geojson-preview-tool/GeojsonPreviewTool.js'; @@ -50,6 +51,7 @@ export const CORE_TOOLS = [ new BoundingBoxTool(), new CountryBoundingBoxTool(), new CoordinateConversionTool(), + new ExploreMapboxApiTool(), new GetFeedbackTool({ httpRequest }), new ListFeedbackTool({ httpRequest }), new TilequeryTool({ httpRequest }), diff --git a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap index 5b517de..f7b65c4 100644 --- a/test/tools/__snapshots__/tool-naming-convention.test.ts.snap +++ b/test/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -42,6 +42,11 @@ exports[`Tool Naming Convention > should maintain consistent tool list (snapshot "description": "Delete a Mapbox style by ID", "toolName": "delete_style_tool", }, + { + "className": "ExploreMapboxApiTool", + "description": "Explore Mapbox API endpoints, operations, parameters, and authentication requirements. Get structured information about available APIs, their operations, required parameters, token scopes, and rate limits. Use without parameters to list all APIs, with api parameter to list operations, or with api + operation to get full endpoint details.", + "toolName": "explore_mapbox_api_tool", + }, { "className": "GeojsonPreviewTool", "description": "Generate a geojson.io URL to visualize GeoJSON data. Returns only the URL link.", diff --git a/test/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.test.ts b/test/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.test.ts new file mode 100644 index 0000000..70fca7a --- /dev/null +++ b/test/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.test.ts @@ -0,0 +1,321 @@ +import { describe, it, expect } from 'vitest'; +import { ExploreMapboxApiTool } from '../../../src/tools/explore-mapbox-api-tool/ExploreMapboxApiTool.js'; + +describe('ExploreMapboxApiTool', () => { + const tool = new ExploreMapboxApiTool(); + + describe('listAllApis', () => { + it('should list all available APIs when no input provided', async () => { + const result = await tool.execute({}); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('Mapbox APIs'); + expect(result.content[0].text).toContain('geocoding'); + expect(result.content[0].text).toContain('styles'); + expect(result.content[0].text).toContain('tokens'); + expect(result.content[0].text).toContain('static-images'); + expect(result.content[0].text).toContain('directions'); + expect(result.content[0].text).toContain('tilequery'); + expect(result.content[0].text).toContain('feedback'); + + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.apis).toBeDefined(); + expect(result.structuredContent!.apis!.length).toBe(7); + expect(result.structuredContent!.apis![0]).toHaveProperty('api'); + expect(result.structuredContent!.apis![0]).toHaveProperty('category'); + expect(result.structuredContent!.apis![0]).toHaveProperty('description'); + expect(result.structuredContent!.apis![0]).toHaveProperty('docsUrl'); + expect(result.structuredContent!.apis![0]).toHaveProperty( + 'operationCount' + ); + }); + }); + + describe('listApiOperations', () => { + it('should list operations for geocoding API', async () => { + const result = await tool.execute({ api: 'geocoding' }); + + expect(result.content).toHaveLength(1); + expect(result.content[0].text).toContain('geocoding API'); + expect(result.content[0].text).toContain('Forward Geocoding'); + expect(result.content[0].text).toContain('Reverse Geocoding'); + expect(result.content[0].text).toContain('forward-geocode'); + expect(result.content[0].text).toContain('reverse-geocode'); + + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.operations).toBeDefined(); + expect(result.structuredContent!.operations!.length).toBe(2); + expect(result.structuredContent!.operations![0]).toHaveProperty( + 'operationId' + ); + expect(result.structuredContent!.operations![0]).toHaveProperty('name'); + expect(result.structuredContent!.operations![0]).toHaveProperty('method'); + expect(result.structuredContent!.operations![0]).toHaveProperty( + 'endpoint' + ); + }); + + it('should list operations for styles API', async () => { + const result = await tool.execute({ api: 'styles' }); + + expect(result.content[0].text).toContain('styles API'); + expect(result.content[0].text).toContain('Create Style'); + expect(result.content[0].text).toContain('Retrieve Style'); + expect(result.content[0].text).toContain('Update Style'); + expect(result.content[0].text).toContain('Delete Style'); + expect(result.content[0].text).toContain('List Styles'); + + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.operations).toBeDefined(); + expect(result.structuredContent!.operations!.length).toBe(5); + }); + + it('should handle case-insensitive API names', async () => { + const result = await tool.execute({ api: 'GEOCODING' }); + + expect(result.content[0].text).toContain('geocoding API'); + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.operations).toBeDefined(); + }); + + it('should return error for invalid API', async () => { + const result = await tool.execute({ api: 'invalid-api' }); + + expect(result.content[0].text).toContain('❌'); + expect(result.content[0].text).toContain('not found'); + expect(result.content[0].text).toContain('Available APIs'); + }); + }); + + describe('getOperationDetails', () => { + it('should get details for forward-geocode operation', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode' + }); + + expect(result.content[0].text).toContain('Forward Geocoding'); + expect(result.content[0].text).toContain('HTTP Request'); + expect(result.content[0].text).toContain('GET'); + expect(result.content[0].text).toContain( + '/geocoding/v5/{mode}/{query}.json' + ); + expect(result.content[0].text).toContain('Path Parameters'); + expect(result.content[0].text).toContain('Query Parameters'); + expect(result.content[0].text).toContain('Authentication'); + expect(result.content[0].text).toContain('Required Scopes'); + expect(result.content[0].text).toContain('Rate Limits'); + + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.operationDetails).toBeDefined(); + expect(result.structuredContent!.operationDetails!.operationId).toBe( + 'forward-geocode' + ); + expect(result.structuredContent!.operationDetails!.method).toBe('GET'); + expect( + result.structuredContent!.operationDetails!.requiredScopes + ).toContain('geocoding:read'); + }); + + it('should include examples when details=true', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode', + details: true + }); + + expect(result.content[0].text).toContain('Example Request'); + expect(result.content[0].text).toContain('Example Response'); + expect(result.content[0].text).toContain('api.mapbox.com'); + }); + + it('should not include examples when details=false', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode', + details: false + }); + + expect(result.content[0].text).not.toContain('Example Request'); + expect(result.content[0].text).not.toContain('Example Response'); + expect(result.content[0].text).toContain('Add `{ details: true }`'); + }); + + it('should handle case-insensitive operation names', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'FORWARD-GEOCODE' + }); + + expect(result.content[0].text).toContain('Forward Geocoding'); + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.operationDetails).toBeDefined(); + }); + + it('should return error for invalid operation', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'invalid-operation' + }); + + expect(result.content[0].text).toContain('❌'); + expect(result.content[0].text).toContain('not found'); + expect(result.content[0].text).toContain('Available operations'); + }); + }); + + describe('output schema validation', () => { + it('should validate output for listing all APIs', async () => { + const result = await tool.execute({}); + expect(result.structuredContent).toBeDefined(); + const validation = tool.outputSchema!.safeParse(result.structuredContent); + + expect(validation.success).toBe(true); + }); + + it('should validate output for listing operations', async () => { + const result = await tool.execute({ api: 'geocoding' }); + expect(result.structuredContent).toBeDefined(); + const validation = tool.outputSchema!.safeParse(result.structuredContent); + + expect(validation.success).toBe(true); + }); + + it('should validate output for operation details', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode' + }); + expect(result.structuredContent).toBeDefined(); + const validation = tool.outputSchema!.safeParse(result.structuredContent); + + expect(validation.success).toBe(true); + }); + + it('should validate output for operation details with examples', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode', + details: true + }); + expect(result.structuredContent).toBeDefined(); + const validation = tool.outputSchema!.safeParse(result.structuredContent); + + expect(validation.success).toBe(true); + }); + }); + + describe('API coverage', () => { + it('should include all 7 priority APIs', async () => { + const result = await tool.execute({}); + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.apis).toBeDefined(); + const apis = result.structuredContent!.apis!.map((api) => api.api); + + expect(apis).toContain('geocoding'); + expect(apis).toContain('styles'); + expect(apis).toContain('tokens'); + expect(apis).toContain('static-images'); + expect(apis).toContain('directions'); + expect(apis).toContain('tilequery'); + expect(apis).toContain('feedback'); + expect(apis).toHaveLength(7); + }); + + it('should include operations for all APIs', async () => { + const apis = [ + 'geocoding', + 'styles', + 'tokens', + 'static-images', + 'directions', + 'tilequery', + 'feedback' + ]; + + for (const api of apis) { + const result = await tool.execute({ api }); + expect(result.structuredContent).toBeDefined(); + expect(result.structuredContent!.operations).toBeDefined(); + expect(result.structuredContent!.operations!.length).toBeGreaterThan(0); + } + }); + }); + + describe('parameter formatting', () => { + it('should format required and optional parameters correctly', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode' + }); + + const text = result.content[0].text; + + // Required parameters should show ✅ + expect(text).toContain('✅ Yes'); + // Optional parameters should show ❌ + expect(text).toContain('❌ No'); + // Should show parameter types + expect(text).toContain('`string`'); + expect(text).toContain('`boolean`'); + expect(text).toContain('`number`'); + }); + + it('should show enum values for parameters with enums', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode' + }); + + const text = result.content[0].text; + + // Should show enum options + expect(text).toContain('Options:'); + expect(text).toContain('mapbox.places'); + }); + + it('should show default values for parameters', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode' + }); + + const text = result.content[0].text; + + // Should show defaults + expect(text).toContain('default:'); + }); + }); + + describe('rate limits', () => { + it('should display rate limit information', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode' + }); + + const text = result.content[0].text; + + expect(text).toContain('Rate Limits'); + expect(text).toContain('600 requests per minute'); + expect(text).toContain('Free tier: 100,000 requests/month'); + }); + }); + + describe('authentication scopes', () => { + it('should display required scopes for each operation', async () => { + const result = await tool.execute({ + api: 'geocoding', + operation: 'forward-geocode' + }); + + const text = result.content[0].text; + + expect(text).toContain('Authentication'); + expect(text).toContain('Required Scopes'); + expect(text).toContain('geocoding:read'); + expect(text).toContain('styles:read'); + }); + }); +});