From 07d59a470ab507550ef4f1633e2815315ca8e9df Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Thu, 18 Dec 2025 18:06:58 -0500 Subject: [PATCH 1/2] [prompts] Add MCP prompts for multi-step workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement three high-value prompts that orchestrate multiple tools: 1. create-and-preview-style: Create a map style with automatic token management. Checks for existing public tokens, creates one if needed, then creates the style and generates a preview link. 2. build-custom-map: Use conversational AI to build themed map styles (e.g., "dark cyberpunk", "nature-focused"). Leverages the Style Builder tool for AI-powered style generation. 3. analyze-geojson: Complete GeoJSON analysis workflow with validation, bounding box calculation, coordinate conversion, and visualization. Infrastructure: - BasePrompt abstract class for all prompts - promptRegistry for centralized prompt management - Integration with MCP server using Zod schemas - Full test coverage (37 new tests, all passing) Documentation updated in README.md and CLAUDE.md with usage examples. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 7 +- README.md | 93 ++++++++++++ src/index.ts | 36 ++++- src/prompts/AnalyzeGeojsonPrompt.ts | 139 ++++++++++++++++++ src/prompts/BasePrompt.ts | 87 +++++++++++ src/prompts/BuildCustomMapPrompt.ts | 125 ++++++++++++++++ src/prompts/CreateAndPreviewStylePrompt.ts | 136 +++++++++++++++++ src/prompts/promptRegistry.ts | 30 ++++ test/prompts/AnalyzeGeojsonPrompt.test.ts | 124 ++++++++++++++++ test/prompts/BuildCustomMapPrompt.test.ts | 93 ++++++++++++ .../CreateAndPreviewStylePrompt.test.ts | 82 +++++++++++ test/prompts/promptRegistry.test.ts | 89 +++++++++++ 12 files changed, 1039 insertions(+), 2 deletions(-) create mode 100644 src/prompts/AnalyzeGeojsonPrompt.ts create mode 100644 src/prompts/BasePrompt.ts create mode 100644 src/prompts/BuildCustomMapPrompt.ts create mode 100644 src/prompts/CreateAndPreviewStylePrompt.ts create mode 100644 src/prompts/promptRegistry.ts create mode 100644 test/prompts/AnalyzeGeojsonPrompt.test.ts create mode 100644 test/prompts/BuildCustomMapPrompt.test.ts create mode 100644 test/prompts/CreateAndPreviewStylePrompt.test.ts create mode 100644 test/prompts/promptRegistry.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 67db9d1..fac4a60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,7 @@ The codebase organizes into: - `src/index.ts` - Main entry point with .env loading and server initialization - `src/config/toolConfig.ts` - Configuration parser for tool filtering and MCP-UI toggles - `src/tools/` - MCP tool implementations with `BaseTool` abstract class and registry +- `src/prompts/` - MCP prompt implementations with `BasePrompt` abstract class and registry - `src/resources/` - Static reference data (style specs, token scopes, Streets v8 fields) - `src/utils/` - HTTP pipeline, JWT parsing, tracing, and version utilities @@ -25,13 +26,17 @@ The codebase organizes into: **Tool Architecture:** All tools extend `BaseTool`. Tools auto-validate inputs using Zod schemas. Each tool lives in `src/tools/tool-name-tool/` with separate `*.schema.ts` and `*.tool.ts` files. +**Prompt Architecture:** All prompts extend `BasePrompt` abstract class. Prompts orchestrate multi-step workflows, guiding AI assistants through complex tasks with best practices built-in. Each prompt lives in `src/prompts/` with separate files per prompt (e.g., `CreateAndPreviewStylePrompt.ts`). Prompts use kebab-case naming (e.g., `create-and-preview-style`). + **HTTP Pipeline System:** "Never patch global.fetch—use HttpPipeline with dependency injection instead." The `HttpPipeline` class applies policies (User-Agent, retry logic) via a chain-of-responsibility pattern. See `src/utils/httpPipeline.ts:20`. **Resource System:** Static reference data exposed as MCP resources using URI pattern `resource://mapbox-*`, including style layer specs, Streets v8 field definitions, and token scope documentation. **Token Management:** Tools receive `MAPBOX_ACCESS_TOKEN` via `extra.authInfo.token` or environment variable. Token scope validation is critical—most tool failures stem from insufficient scopes (see `README.md` for per-tool requirements). -**Tool Registry:** Tools are auto-discovered via `src/tools/index.ts` exports. No manual registration required—just export from index. +**Tool Registry:** Tools are auto-discovered via `src/tools/toolRegistry.ts` exports. No manual registration required—just export from registry. + +**Prompt Registry:** Prompts are registered in `src/prompts/promptRegistry.ts`. To add a new prompt, create the prompt class and add it to the `ALL_PROMPTS` array. The main server automatically registers all prompts with proper Zod schema conversion. ## Essential Workflows diff --git a/README.md b/README.md index 3d4a81b..057de17 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ https://github.com/user-attachments/assets/8b1b8ef2-9fba-4951-bc9a-beaed4f6aff6 - [Hosted MCP Endpoint](#hosted-mcp-endpoint) - [Getting Your Mapbox Access Token](#getting-your-mapbox-access-token) - [Tools](#tools) + - [Prompts](#prompts) - [Documentation Tools](#documentation-tools) - [Reference Tools](#reference-tools) - [Style Management Tools](#style-management-tools) @@ -453,6 +454,98 @@ An array of four numbers representing the bounding box: `[minX, minY, maxX, maxY - "Calculate the bounding box of this GeoJSON file" (then upload a .geojson file) - "What's the bounding box for the coordinates in the uploaded parks.geojson file?" +## Prompts + +MCP Prompts are pre-built workflow templates that guide AI assistants through multi-step tasks. They orchestrate multiple tools in the correct sequence, providing best practices and error handling built-in. + +**Available Prompts:** + +### create-and-preview-style + +Create a new Mapbox map style and generate a shareable preview link with automatic token management. + +**Arguments:** + +- `style_name` (required): Name for the new map style +- `style_description` (optional): Description of the style theme or purpose +- `base_style` (optional): Base style to start from (e.g., "streets-v12", "dark-v11") +- `preview_location` (optional): Location to center the preview map +- `preview_zoom` (optional): Zoom level for the preview (0-22, default: 12) + +**What it does:** + +1. Checks for an existing public token with `styles:read` scope +2. Creates a new public token if needed +3. Creates the map style +4. Generates a preview link + +**Example usage:** + +``` +Use prompt: create-and-preview-style +Arguments: + style_name: "My Custom Map" + style_description: "A dark-themed map for nighttime navigation" + base_style: "dark-v11" + preview_location: "San Francisco" + preview_zoom: "13" +``` + +### build-custom-map + +Use conversational AI to build a custom styled map based on a theme description. + +**Arguments:** + +- `theme` (required): Theme description (e.g., "dark cyberpunk", "nature-focused", "minimal monochrome") +- `emphasis` (optional): Features to emphasize (e.g., "parks and green spaces", "transit lines") +- `preview_location` (optional): Location to center the preview map +- `preview_zoom` (optional): Zoom level for the preview (0-22, default: 12) + +**What it does:** + +1. Uses the Style Builder tool to create a themed style based on your description +2. Creates the style in your Mapbox account +3. Generates a preview link + +**Example usage:** + +``` +Use prompt: build-custom-map +Arguments: + theme: "retro 80s neon" + emphasis: "nightlife and entertainment venues" + preview_location: "Tokyo" + preview_zoom: "14" +``` + +### analyze-geojson + +Analyze and visualize GeoJSON data with automatic validation and bounding box calculation. + +**Arguments:** + +- `geojson_data` (required): GeoJSON object or string to analyze +- `show_bounds` (optional): Calculate and display bounding box (true/false, default: true) +- `convert_coordinates` (optional): Provide Web Mercator conversion examples (true/false, default: false) + +**What it does:** + +1. Validates GeoJSON format +2. Calculates bounding box (if requested) +3. Provides coordinate conversion examples (if requested) +4. Generates an interactive visualization link + +**Example usage:** + +``` +Use prompt: analyze-geojson +Arguments: + geojson_data: {"type":"FeatureCollection","features":[...]} + show_bounds: "true" + convert_coordinates: "false" +``` + ## Resources This server exposes static reference documentation as MCP Resources. While these are primarily accessed through the `get_reference_tool`, MCP clients that fully support the resources protocol can access them directly. diff --git a/src/index.ts b/src/index.ts index 8bebca0..e0d6150 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,9 +11,11 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; import { getAllTools } from './tools/toolRegistry.js'; import { getAllResources } from './resources/resourceRegistry.js'; +import { getAllPrompts } from './prompts/promptRegistry.js'; import { getVersionInfo } from './utils/versionUtils.js'; import { initializeTracing, @@ -64,7 +66,8 @@ const server = new McpServer( { capabilities: { tools: {}, - resources: {} + resources: {}, + prompts: {} } } ); @@ -80,6 +83,37 @@ resources.forEach((resource) => { resource.installTo(server); }); +// Register prompts to the server +const prompts = getAllPrompts(); +prompts.forEach((prompt) => { + const argsSchema: Record> = + {}; + + // Convert prompt arguments to Zod schema format + prompt.arguments.forEach((arg) => { + const zodString = z.string().describe(arg.description); + argsSchema[arg.name] = arg.required ? zodString : zodString.optional(); + }); + + server.registerPrompt( + prompt.name, + { + description: prompt.description, + argsSchema: argsSchema + }, + async (args) => { + // Filter out undefined values from optional arguments + const filteredArgs: Record = {}; + for (const [key, value] of Object.entries(args || {})) { + if (value !== undefined) { + filteredArgs[key] = value; + } + } + return prompt.execute(filteredArgs); + } + ); +}); + async function main() { // Send MCP logging messages about .env loading if (envLoadError) { diff --git a/src/prompts/AnalyzeGeojsonPrompt.ts b/src/prompts/AnalyzeGeojsonPrompt.ts new file mode 100644 index 0000000..52a7f3b --- /dev/null +++ b/src/prompts/AnalyzeGeojsonPrompt.ts @@ -0,0 +1,139 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import { BasePrompt, type PromptArgument } from './BasePrompt.js'; + +/** + * Prompt for analyzing and visualizing GeoJSON data + * + * This prompt orchestrates multiple tools to: + * 1. Validate GeoJSON format + * 2. Calculate bounding box + * 3. Generate visualization link + * 4. Provide analysis summary + */ +export class AnalyzeGeojsonPrompt extends BasePrompt { + readonly name = 'analyze-geojson'; + readonly description = + 'Analyze and visualize GeoJSON data. Validates format, calculates bounding box, and generates an interactive map visualization.'; + + readonly arguments: ReadonlyArray = [ + { + name: 'geojson_data', + description: + 'GeoJSON object or string to analyze (Point, LineString, Polygon, Feature, FeatureCollection, etc.)', + required: true + }, + { + name: 'show_bounds', + description: + 'Whether to calculate and display the bounding box (true/false, default: true)', + required: false + }, + { + name: 'convert_coordinates', + description: + 'Whether to provide coordinate conversion examples for Web Mercator (true/false, default: false)', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + const geojsonData = args['geojson_data']; + const showBounds = args['show_bounds'] !== 'false'; // Default to true + const convertCoordinates = args['convert_coordinates'] === 'true'; // Default to false + + let instructionText = `Analyze the provided GeoJSON data and generate an interactive visualization. + +**GeoJSON Data:** +\`\`\`json +${geojsonData} +\`\`\` + +Follow these steps to analyze and visualize the data: + +1. **Parse and validate the GeoJSON** + - Parse the GeoJSON data (it may be provided as a string or object) + - Verify it's valid GeoJSON with proper structure + - Identify the geometry type (Point, LineString, Polygon, Feature, FeatureCollection, etc.) + - Count the number of features if it's a FeatureCollection`; + + if (showBounds) { + instructionText += ` + +2. **Calculate bounding box** + - Use the bounding_box_tool to calculate the geographic extent + - The tool will return [minX, minY, maxX, maxY] (west, south, east, north) + - Present the bounds in a clear format: + * Western edge (minX): [longitude] + * Eastern edge (maxX): [longitude] + * Southern edge (minY): [latitude] + * Northern edge (maxY): [latitude] + - Calculate and display the width and height in degrees`; + } + + if (convertCoordinates) { + instructionText += ` + +3. **Provide coordinate conversion examples** + - Use the coordinate_conversion_tool to show Web Mercator equivalents + - Convert a sample point from the GeoJSON from EPSG:4326 to EPSG:3857 + - Explain when Web Mercator coordinates might be useful (web mapping, tilesets)`; + } + + const nextStep = + showBounds && convertCoordinates + ? '4' + : showBounds || convertCoordinates + ? '3' + : '2'; + + instructionText += ` + +${nextStep}. **Generate visualization** + - Use the geojson_preview_tool to create an interactive map + - This will generate a geojson.io URL where the data can be viewed and edited + - The visualization will show: + * The geometry rendered on a map + * Feature properties in a side panel + * Interactive editing capabilities + +${parseInt(nextStep) + 1}. **Provide analysis summary** + Present a comprehensive summary including: + - **Geometry type**: [Point/LineString/Polygon/etc.] + - **Feature count**: [number of features] + - **Coordinate system**: WGS84 (EPSG:4326)`; + + if (showBounds) { + instructionText += `\n - **Geographic extent**: [bounding box summary]`; + } + + instructionText += `\n - **Visualization link**: [geojson.io URL - clickable] + - **Properties**: [list any feature properties found] + - **Data quality notes**: [any issues or observations] + +**Analysis guidelines:** +- Check for common issues: invalid coordinates, missing required fields, topology errors +- Note if coordinates are in the correct order (longitude, latitude) +- Identify if the data uses right-hand rule for polygon winding +- Suggest improvements if the GeoJSON could be optimized +- For large datasets, note that the preview URL may be long + +**Important notes:** +- GeoJSON coordinates must be [longitude, latitude], not [latitude, longitude] +- Valid longitude range: -180 to 180 +- Valid latitude range: -90 to 90 +- The preview tool works best with small to medium-sized datasets`; + + return [ + { + role: 'user', + content: { + type: 'text', + text: instructionText + } + } + ]; + } +} diff --git a/src/prompts/BasePrompt.ts b/src/prompts/BasePrompt.ts new file mode 100644 index 0000000..dbf287f --- /dev/null +++ b/src/prompts/BasePrompt.ts @@ -0,0 +1,87 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { + PromptMessage, + GetPromptResult +} from '@modelcontextprotocol/sdk/types.js'; + +/** + * Argument definition for a prompt + */ +export interface PromptArgument { + name: string; + description: string; + required: boolean; +} + +/** + * Base class for all MCP prompts + * Prompts represent multi-step workflows that guide AI agents through common tasks + */ +export abstract class BasePrompt { + /** + * Unique identifier for the prompt (snake_case) + */ + abstract readonly name: string; + + /** + * Human-readable description of what this prompt does + */ + abstract readonly description: string; + + /** + * Array of arguments this prompt accepts + */ + abstract readonly arguments: ReadonlyArray; + + /** + * Get prompt metadata for listing + */ + getMetadata(): { + name: string; + description: string; + arguments: ReadonlyArray; + } { + return { + name: this.name, + description: this.description, + arguments: this.arguments + }; + } + + /** + * Generate the prompt messages with the provided arguments + * @param args - Arguments provided by the user + * @returns Array of messages for the LLM + */ + abstract getMessages(args: Record): PromptMessage[]; + + /** + * Execute the prompt with the provided arguments + * @param args - Arguments provided by the user + * @returns GetPromptResult with messages + */ + execute(args: Record): GetPromptResult { + this.validateArguments(args); + return { + messages: this.getMessages(args) + }; + } + + /** + * Validate that required arguments are provided + * @param args - Arguments to validate + * @throws Error if required arguments are missing + */ + protected validateArguments(args: Record): void { + const requiredArgs = this.arguments.filter((arg) => arg.required); + const missingArgs = requiredArgs.filter((arg) => !args[arg.name]); + + if (missingArgs.length > 0) { + throw new Error( + `Missing required arguments: ${missingArgs.map((arg) => arg.name).join(', ')}` + ); + } + } +} diff --git a/src/prompts/BuildCustomMapPrompt.ts b/src/prompts/BuildCustomMapPrompt.ts new file mode 100644 index 0000000..e9a8951 --- /dev/null +++ b/src/prompts/BuildCustomMapPrompt.ts @@ -0,0 +1,125 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import { BasePrompt, type PromptArgument } from './BasePrompt.js'; + +/** + * Prompt for using conversational AI to build a custom styled map + * + * This prompt leverages the style_builder_tool to create themed map styles through + * natural language descriptions, then creates the style and generates a preview. + */ +export class BuildCustomMapPrompt extends BasePrompt { + readonly name = 'build-custom-map'; + readonly description = + 'Use conversational AI to build a custom styled map based on a theme description. Supports themes like "dark cyberpunk", "nature-focused", "minimal monochrome" and can emphasize specific features.'; + + readonly arguments: ReadonlyArray = [ + { + name: 'theme', + description: + 'Theme description for the map (e.g., "dark cyberpunk", "nature-focused", "minimal monochrome", "retro 80s neon")', + required: true + }, + { + name: 'emphasis', + description: + 'Optional features to emphasize (e.g., "parks and green spaces", "transit lines", "water bodies", "roads and highways")', + required: false + }, + { + name: 'preview_location', + description: + 'Optional location to center the preview map (e.g., "New York City", "Tokyo", or coordinates "-122.4,37.8")', + required: false + }, + { + name: 'preview_zoom', + description: 'Optional zoom level for the preview (0-22, default: 12)', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + const theme = args['theme']; + const emphasis = args['emphasis']; + const previewLocation = args['preview_location']; + const previewZoom = args['preview_zoom'] || '12'; + + let stylePrompt = `Create a custom map with a ${theme} theme`; + if (emphasis) { + stylePrompt += `, emphasizing ${emphasis}`; + } + stylePrompt += '.'; + + let instructionText = `Build a custom Mapbox map style with the theme: "${theme}" + +Follow these steps to create and preview the styled map: + +1. **Use the Style Builder** + - Use the style_builder_tool to create the themed map style + - Provide this prompt: "${stylePrompt}" + - The Style Builder will use AI to interpret your theme and create appropriate: + * Color schemes matching the theme + * Layer visibility and styling + * Typography and symbols + * Overall aesthetic + +2. **Review the generated style** + - The style_builder_tool will return a complete Mapbox GL JS style specification + - Review the style to ensure it matches the intended theme + - Note any specific customizations made (colors, layers emphasized, etc.) + +3. **Create the style** + - Use create_style_tool to save the generated style to your Mapbox account + - Provide a descriptive name like "Custom ${theme} Map" + - Include the complete style specification from step 1 + - Save the style ID from the response + +4. **Generate preview link** + - Use preview_style_tool with the newly created style ID`; + + if (previewLocation) { + instructionText += `\n - Center the preview on: ${previewLocation}`; + } else { + instructionText += `\n - Use an appropriate location that showcases the theme well`; + } + + instructionText += `\n - Set zoom level to: ${previewZoom} + - The preview will use an existing public token automatically + +5. **Present results** + - Show the user: + * A summary of the theme and customizations applied + * The style ID for future reference + * The preview URL to view the map + * Suggestions for further customization if desired + +**Theme interpretation tips:** +- "Dark cyberpunk": Dark backgrounds, neon colors (cyan, magenta, purple), high contrast +- "Nature-focused": Earth tones, emphasize parks/forests/water, soften urban features +- "Minimal monochrome": Grayscale palette, simplified geometry, clean lines +- "Retro 80s neon": Bright colors, high saturation, bold typography`; + + if (emphasis) { + instructionText += `\n- Custom emphasis on "${emphasis}": Ensure these features are visually prominent`; + } + + instructionText += `\n\n**Important notes:** +- The style_builder_tool is powered by AI and may need refinement +- You can iterate on the style by making additional calls to style_builder_tool +- If the initial result doesn't match expectations, try refining the theme description +- Consider the map's use case when choosing zoom levels and preview locations`; + + return [ + { + role: 'user', + content: { + type: 'text', + text: instructionText + } + } + ]; + } +} diff --git a/src/prompts/CreateAndPreviewStylePrompt.ts b/src/prompts/CreateAndPreviewStylePrompt.ts new file mode 100644 index 0000000..1d9e55d --- /dev/null +++ b/src/prompts/CreateAndPreviewStylePrompt.ts @@ -0,0 +1,136 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import { BasePrompt, type PromptArgument } from './BasePrompt.js'; + +/** + * Prompt for creating a new map style and immediately generating a shareable preview link + * + * This prompt orchestrates multiple tools to: + * 1. Check for an existing public token with styles:read scope + * 2. Create a new public token if needed + * 3. Create the map style + * 4. Generate a preview link using the public token + */ +export class CreateAndPreviewStylePrompt extends BasePrompt { + readonly name = 'create-and-preview-style'; + readonly description = + 'Create a new Mapbox map style and generate a shareable preview link. Automatically handles token management by checking for or creating a public token with the required scopes.'; + + readonly arguments: ReadonlyArray = [ + { + name: 'style_name', + description: 'Name for the new map style', + required: true + }, + { + name: 'style_description', + description: 'Optional description of the style theme or purpose', + required: false + }, + { + name: 'base_style', + description: + 'Optional base style to start from (e.g., "streets-v12", "outdoors-v12", "light-v11", "dark-v11")', + required: false + }, + { + name: 'preview_location', + description: + 'Optional location to center the preview map (e.g., "San Francisco" or "-122.4,37.8")', + required: false + }, + { + name: 'preview_zoom', + description: 'Optional zoom level for the preview (0-22, default: 12)', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + const styleName = args['style_name']; + const styleDescription = args['style_description']; + const baseStyle = args['base_style'] || 'streets-v12'; + const previewLocation = args['preview_location']; + const previewZoom = args['preview_zoom'] || '12'; + + let instructionText = `Create a new Mapbox map style named "${styleName}" and generate a shareable preview link. + +Follow these steps carefully: + +1. **Check for existing public token** + - Use the list_tokens_tool with usage="pk" to list all public tokens + - Look for a token that has the "styles:read" scope + - If you find one, note its token value for later use + +2. **Create public token if needed** + - If no public token with "styles:read" scope exists, create one using create_token_tool + - Use these parameters: + * note: "Public token for style previews" + * scopes: ["styles:read"] + - Save the token value from the response + +3. **Create the map style** + - Use the create_style_tool to create the new style + - Style name: "${styleName}"`; + + if (styleDescription) { + instructionText += `\n - Description: "${styleDescription}"`; + } + + instructionText += `\n - Base the style on Mapbox ${baseStyle} + - You can start with a basic style like: + \`\`\`json + { + "version": 8, + "name": "${styleName}", + "sources": { + "mapbox": { + "type": "vector", + "url": "mapbox://mapbox.mapbox-streets-v8" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { "background-color": "#f0f0f0" } + } + ] + } + \`\`\` + - Save the style ID from the response + +4. **Generate preview link** + - Use the preview_style_tool with the style ID you just created`; + + if (previewLocation) { + instructionText += `\n - Center the map on: ${previewLocation}`; + } + + instructionText += `\n - Set zoom level to: ${previewZoom} + - The tool will automatically use the public token you created/found earlier + +5. **Present results** + - Show the user: + * The created style ID + * The preview URL (they can click to open in browser) + * Instructions to share or embed the preview + +**Important notes:** +- The preview_style_tool will automatically fetch and use an available public token +- Make sure the style is created successfully before generating the preview +- If any step fails, provide clear error messages and suggest fixes`; + + return [ + { + role: 'user', + content: { + type: 'text', + text: instructionText + } + } + ]; + } +} diff --git a/src/prompts/promptRegistry.ts b/src/prompts/promptRegistry.ts new file mode 100644 index 0000000..82b3122 --- /dev/null +++ b/src/prompts/promptRegistry.ts @@ -0,0 +1,30 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { CreateAndPreviewStylePrompt } from './CreateAndPreviewStylePrompt.js'; +import { BuildCustomMapPrompt } from './BuildCustomMapPrompt.js'; +import { AnalyzeGeojsonPrompt } from './AnalyzeGeojsonPrompt.js'; + +// Central registry of all prompts +export const ALL_PROMPTS = [ + new CreateAndPreviewStylePrompt(), + new BuildCustomMapPrompt(), + new AnalyzeGeojsonPrompt() +] as const; + +export type PromptInstance = (typeof ALL_PROMPTS)[number]; + +/** + * Get all registered prompts + */ +export function getAllPrompts(): readonly PromptInstance[] { + return ALL_PROMPTS; +} + +/** + * Get a specific prompt by name + * @param name - The name of the prompt to retrieve + */ +export function getPromptByName(name: string): PromptInstance | undefined { + return ALL_PROMPTS.find((prompt) => prompt.name === name); +} diff --git a/test/prompts/AnalyzeGeojsonPrompt.test.ts b/test/prompts/AnalyzeGeojsonPrompt.test.ts new file mode 100644 index 0000000..1fd9928 --- /dev/null +++ b/test/prompts/AnalyzeGeojsonPrompt.test.ts @@ -0,0 +1,124 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { AnalyzeGeojsonPrompt } from '../../src/prompts/AnalyzeGeojsonPrompt.js'; + +describe('AnalyzeGeojsonPrompt', () => { + const prompt = new AnalyzeGeojsonPrompt(); + + const sampleGeojson = JSON.stringify({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-122.4194, 37.7749] + }, + properties: { + name: 'San Francisco' + } + }); + + it('should have correct metadata', () => { + expect(prompt.name).toBe('analyze-geojson'); + expect(prompt.description).toContain('Analyze and visualize GeoJSON'); + expect(prompt.arguments).toHaveLength(3); + }); + + it('should require geojson_data argument', () => { + const requiredArg = prompt.arguments.find( + (arg) => arg.name === 'geojson_data' + ); + expect(requiredArg).toBeDefined(); + expect(requiredArg?.required).toBe(true); + }); + + it('should have optional arguments', () => { + const optionalArgs = ['show_bounds', 'convert_coordinates']; + optionalArgs.forEach((argName) => { + const arg = prompt.arguments.find((a) => a.name === argName); + expect(arg).toBeDefined(); + expect(arg?.required).toBe(false); + }); + }); + + it('should generate messages with required arguments', () => { + const result = prompt.execute({ + geojson_data: sampleGeojson + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + + const text = result.messages[0].content.text; + expect(text).toContain('Analyze the provided GeoJSON'); + expect(text).toContain('bounding_box_tool'); + expect(text).toContain('geojson_preview_tool'); + }); + + it('should include bounding box calculation by default', () => { + const result = prompt.execute({ + geojson_data: sampleGeojson + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Calculate bounding box'); + expect(text).toContain('bounding_box_tool'); + }); + + it('should skip bounding box when show_bounds is false', () => { + const result = prompt.execute({ + geojson_data: sampleGeojson, + show_bounds: 'false' + }); + + const text = result.messages[0].content.text; + // Should still mention visualization but with different step number + expect(text).toContain('Generate visualization'); + }); + + it('should include coordinate conversion when requested', () => { + const result = prompt.execute({ + geojson_data: sampleGeojson, + convert_coordinates: 'true' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('coordinate_conversion_tool'); + expect(text).toContain('Web Mercator'); + expect(text).toContain('EPSG:3857'); + }); + + it('should include the geojson data in messages', () => { + const result = prompt.execute({ + geojson_data: sampleGeojson + }); + + const text = result.messages[0].content.text; + expect(text).toContain(sampleGeojson); + }); + + it('should throw error if geojson_data is missing', () => { + expect(() => { + prompt.execute({}); + }).toThrow('Missing required arguments: geojson_data'); + }); + + it('should provide analysis guidelines', () => { + const result = prompt.execute({ + geojson_data: sampleGeojson + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Analysis guidelines'); + expect(text).toContain('longitude, latitude'); + expect(text).toContain('right-hand rule'); + }); + + it('should return proper metadata', () => { + const metadata = prompt.getMetadata(); + expect(metadata.name).toBe(prompt.name); + expect(metadata.description).toBe(prompt.description); + expect(metadata.arguments).toEqual(prompt.arguments); + }); +}); diff --git a/test/prompts/BuildCustomMapPrompt.test.ts b/test/prompts/BuildCustomMapPrompt.test.ts new file mode 100644 index 0000000..e2489f1 --- /dev/null +++ b/test/prompts/BuildCustomMapPrompt.test.ts @@ -0,0 +1,93 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { BuildCustomMapPrompt } from '../../src/prompts/BuildCustomMapPrompt.js'; + +describe('BuildCustomMapPrompt', () => { + const prompt = new BuildCustomMapPrompt(); + + it('should have correct metadata', () => { + expect(prompt.name).toBe('build-custom-map'); + expect(prompt.description).toContain('conversational AI'); + expect(prompt.arguments).toHaveLength(4); + }); + + it('should require theme argument', () => { + const requiredArg = prompt.arguments.find((arg) => arg.name === 'theme'); + expect(requiredArg).toBeDefined(); + expect(requiredArg?.required).toBe(true); + }); + + it('should have optional arguments', () => { + const optionalArgs = ['emphasis', 'preview_location', 'preview_zoom']; + optionalArgs.forEach((argName) => { + const arg = prompt.arguments.find((a) => a.name === argName); + expect(arg).toBeDefined(); + expect(arg?.required).toBe(false); + }); + }); + + it('should generate messages with required arguments', () => { + const result = prompt.execute({ + theme: 'dark cyberpunk' + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + + const text = result.messages[0].content.text; + expect(text).toContain('dark cyberpunk'); + expect(text).toContain('style_builder_tool'); + expect(text).toContain('create_style_tool'); + expect(text).toContain('preview_style_tool'); + }); + + it('should include emphasis in prompt', () => { + const result = prompt.execute({ + theme: 'nature-focused', + emphasis: 'parks and green spaces' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('parks and green spaces'); + expect(text).toContain('emphasizing'); + }); + + it('should include preview location when provided', () => { + const result = prompt.execute({ + theme: 'minimal monochrome', + preview_location: 'Tokyo', + preview_zoom: '15' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Tokyo'); + expect(text).toContain('15'); + }); + + it('should throw error if theme is missing', () => { + expect(() => { + prompt.execute({}); + }).toThrow('Missing required arguments: theme'); + }); + + it('should provide theme interpretation tips', () => { + const result = prompt.execute({ + theme: 'retro 80s neon' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Theme interpretation tips'); + expect(text).toContain('Dark cyberpunk'); + expect(text).toContain('Nature-focused'); + }); + + it('should return proper metadata', () => { + const metadata = prompt.getMetadata(); + expect(metadata.name).toBe(prompt.name); + expect(metadata.description).toBe(prompt.description); + expect(metadata.arguments).toEqual(prompt.arguments); + }); +}); diff --git a/test/prompts/CreateAndPreviewStylePrompt.test.ts b/test/prompts/CreateAndPreviewStylePrompt.test.ts new file mode 100644 index 0000000..900c72c --- /dev/null +++ b/test/prompts/CreateAndPreviewStylePrompt.test.ts @@ -0,0 +1,82 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { CreateAndPreviewStylePrompt } from '../../src/prompts/CreateAndPreviewStylePrompt.js'; + +describe('CreateAndPreviewStylePrompt', () => { + const prompt = new CreateAndPreviewStylePrompt(); + + it('should have correct metadata', () => { + expect(prompt.name).toBe('create-and-preview-style'); + expect(prompt.description).toContain('Create a new Mapbox map style'); + expect(prompt.arguments).toHaveLength(5); + }); + + it('should require style_name argument', () => { + const requiredArg = prompt.arguments.find( + (arg) => arg.name === 'style_name' + ); + expect(requiredArg).toBeDefined(); + expect(requiredArg?.required).toBe(true); + }); + + it('should have optional arguments', () => { + const optionalArgs = [ + 'style_description', + 'base_style', + 'preview_location', + 'preview_zoom' + ]; + optionalArgs.forEach((argName) => { + const arg = prompt.arguments.find((a) => a.name === argName); + expect(arg).toBeDefined(); + expect(arg?.required).toBe(false); + }); + }); + + it('should generate messages with required arguments', () => { + const result = prompt.execute({ + style_name: 'Test Style' + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + + const text = result.messages[0].content.text; + expect(text).toContain('Test Style'); + expect(text).toContain('list_tokens_tool'); + expect(text).toContain('create_style_tool'); + expect(text).toContain('preview_style_tool'); + }); + + it('should include optional arguments in messages', () => { + const result = prompt.execute({ + style_name: 'Test Style', + style_description: 'A beautiful test style', + base_style: 'dark-v11', + preview_location: 'San Francisco', + preview_zoom: '14' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('A beautiful test style'); + expect(text).toContain('dark-v11'); + expect(text).toContain('San Francisco'); + expect(text).toContain('14'); + }); + + it('should throw error if required argument is missing', () => { + expect(() => { + prompt.execute({}); + }).toThrow('Missing required arguments: style_name'); + }); + + it('should return proper metadata', () => { + const metadata = prompt.getMetadata(); + expect(metadata.name).toBe(prompt.name); + expect(metadata.description).toBe(prompt.description); + expect(metadata.arguments).toEqual(prompt.arguments); + }); +}); diff --git a/test/prompts/promptRegistry.test.ts b/test/prompts/promptRegistry.test.ts new file mode 100644 index 0000000..453e429 --- /dev/null +++ b/test/prompts/promptRegistry.test.ts @@ -0,0 +1,89 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { + getAllPrompts, + getPromptByName +} from '../../src/prompts/promptRegistry.js'; + +describe('promptRegistry', () => { + describe('getAllPrompts', () => { + it('should return all registered prompts', () => { + const prompts = getAllPrompts(); + expect(prompts).toHaveLength(3); + }); + + it('should include create-and-preview-style prompt', () => { + const prompts = getAllPrompts(); + const prompt = prompts.find((p) => p.name === 'create-and-preview-style'); + expect(prompt).toBeDefined(); + expect(prompt?.description).toContain('Create a new Mapbox map style'); + }); + + it('should include build-custom-map prompt', () => { + const prompts = getAllPrompts(); + const prompt = prompts.find((p) => p.name === 'build-custom-map'); + expect(prompt).toBeDefined(); + expect(prompt?.description).toContain('conversational AI'); + }); + + it('should include analyze-geojson prompt', () => { + const prompts = getAllPrompts(); + const prompt = prompts.find((p) => p.name === 'analyze-geojson'); + expect(prompt).toBeDefined(); + expect(prompt?.description).toContain('Analyze and visualize GeoJSON'); + }); + + it('should return readonly array', () => { + const prompts = getAllPrompts(); + expect(Object.isFrozen(prompts)).toBe(false); // ReadonlyArray is not frozen, just typed + expect(Array.isArray(prompts)).toBe(true); + }); + }); + + describe('getPromptByName', () => { + it('should return prompt by name', () => { + const prompt = getPromptByName('create-and-preview-style'); + expect(prompt).toBeDefined(); + expect(prompt?.name).toBe('create-and-preview-style'); + }); + + it('should return undefined for unknown prompt', () => { + const prompt = getPromptByName('non-existent-prompt'); + expect(prompt).toBeUndefined(); + }); + + it('should find all registered prompts by name', () => { + const promptNames = [ + 'create-and-preview-style', + 'build-custom-map', + 'analyze-geojson' + ]; + + promptNames.forEach((name) => { + const prompt = getPromptByName(name); + expect(prompt).toBeDefined(); + expect(prompt?.name).toBe(name); + }); + }); + }); + + describe('prompt naming convention', () => { + it('should follow snake_case naming', () => { + const prompts = getAllPrompts(); + prompts.forEach((prompt) => { + expect(prompt.name).toMatch(/^[a-z]+(-[a-z]+)*$/); + expect(prompt.name).not.toMatch(/[A-Z]/); + expect(prompt.name).not.toMatch(/_/); + }); + }); + + it('should have unique names', () => { + const prompts = getAllPrompts(); + const names = prompts.map((p) => p.name); + const uniqueNames = new Set(names); + expect(uniqueNames.size).toBe(names.length); + }); + }); +}); From 942b30e57e94a5dfa4e70a6fb42220c8d2810ab2 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Fri, 19 Dec 2025 01:34:39 -0500 Subject: [PATCH 2/2] [prompts] Add three new high-value prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three essential workflow prompts to complement the existing three: New Prompts: - setup-mapbox-project: Complete project initialization with tokens, styles, and integration guidance. Handles web, mobile, backend, and fullstack projects with proper security (URL restrictions, secret token management). - debug-mapbox-integration: Systematic 6-phase troubleshooting workflow. Diagnoses token issues, style problems, and API errors. Provides specific solutions for 401, 403, and layer/source errors. - design-data-driven-style: Guide for creating styles with data-driven expressions. Supports color/size/heatmap visualizations with sequential, diverging, and categorical color schemes. Includes advanced expressions and best practices. Test Coverage: - 3 new prompt implementation files - 3 new comprehensive test files (44 tests total) - Updated promptRegistry test (now validates 6 prompts) - All 370 tests passing ✅ Documentation: - Updated README with detailed prompt descriptions, arguments, and examples - Each prompt includes workflow steps and example usage 🤖 Generated with Claude Code --- README.md | 94 ++++ src/prompts/DebugMapboxIntegrationPrompt.ts | 273 ++++++++++++ src/prompts/DesignDataDrivenStylePrompt.ts | 419 ++++++++++++++++++ src/prompts/SetupMapboxProjectPrompt.ts | 269 +++++++++++ src/prompts/promptRegistry.ts | 8 +- .../DebugMapboxIntegrationPrompt.test.ts | 150 +++++++ .../DesignDataDrivenStylePrompt.test.ts | 226 ++++++++++ test/prompts/SetupMapboxProjectPrompt.test.ts | 126 ++++++ test/prompts/promptRegistry.test.ts | 28 +- 9 files changed, 1590 insertions(+), 3 deletions(-) create mode 100644 src/prompts/DebugMapboxIntegrationPrompt.ts create mode 100644 src/prompts/DesignDataDrivenStylePrompt.ts create mode 100644 src/prompts/SetupMapboxProjectPrompt.ts create mode 100644 test/prompts/DebugMapboxIntegrationPrompt.test.ts create mode 100644 test/prompts/DesignDataDrivenStylePrompt.test.ts create mode 100644 test/prompts/SetupMapboxProjectPrompt.test.ts diff --git a/README.md b/README.md index 057de17..deb24ba 100644 --- a/README.md +++ b/README.md @@ -546,6 +546,100 @@ Arguments: convert_coordinates: "false" ``` +### setup-mapbox-project + +Complete setup workflow for a new Mapbox project with proper token security and style initialization. + +**Arguments:** + +- `project_name` (required): Name of the project or application +- `project_type` (optional): Type of project: "web", "mobile", "backend", or "fullstack" (default: "web") +- `production_domain` (optional): Production domain for URL restrictions (e.g., "myapp.com") +- `style_theme` (optional): Initial style theme: "light", "dark", "streets", "outdoors", "satellite" (default: "light") + +**What it does:** + +1. Creates development token with localhost URL restrictions +2. Creates production token with domain URL restrictions (if provided) +3. Creates backend secret token for server-side operations (if needed) +4. Creates an initial map style using the specified theme +5. Generates preview link and provides integration guidance + +**Example usage:** + +``` +Use prompt: setup-mapbox-project +Arguments: + project_name: "Restaurant Finder" + project_type: "fullstack" + production_domain: "restaurantfinder.com" + style_theme: "light" +``` + +### debug-mapbox-integration + +Systematic troubleshooting workflow for diagnosing and fixing Mapbox integration issues. + +**Arguments:** + +- `issue_description` (required): Description of the problem (e.g., "map not loading", "401 error") +- `error_message` (optional): Exact error message from console or logs +- `style_id` (optional): Mapbox style ID being used, if applicable +- `environment` (optional): Where the issue occurs: "development", "production", "staging" + +**What it does:** + +1. Verifies token validity and required scopes +2. Checks style configuration and existence +3. Analyzes error messages and provides specific solutions +4. Tests API endpoints to isolate the problem +5. Provides step-by-step fix instructions +6. Offers prevention strategies + +**Example usage:** + +``` +Use prompt: debug-mapbox-integration +Arguments: + issue_description: "Getting 401 errors when map loads" + error_message: "401 Unauthorized" + style_id: "my-style-id" + environment: "production" +``` + +### design-data-driven-style + +Create a map style with data-driven properties that respond dynamically to feature data using expressions. + +**Arguments:** + +- `style_name` (required): Name for the data-driven style +- `data_description` (required): Description of the data (e.g., "population by city", "earthquake magnitudes") +- `property_name` (required): Name of the data property to visualize (e.g., "population", "magnitude") +- `visualization_type` (optional): How to visualize: "color", "size", "both", "heatmap" (default: "color") +- `color_scheme` (optional): Color scheme: "sequential", "diverging", "categorical" (default: "sequential") + +**What it does:** + +1. Explains data-driven styling concepts and expressions +2. Provides appropriate expression templates for your use case +3. Offers color scales and size ranges based on visualization type +4. Creates the style with data-driven layers +5. Includes advanced expression examples (zoom-based, conditional) +6. Provides best practices for accessibility and performance + +**Example usage:** + +``` +Use prompt: design-data-driven-style +Arguments: + style_name: "Population Density Map" + data_description: "City population data" + property_name: "population" + visualization_type: "both" + color_scheme: "sequential" +``` + ## Resources This server exposes static reference documentation as MCP Resources. While these are primarily accessed through the `get_reference_tool`, MCP clients that fully support the resources protocol can access them directly. diff --git a/src/prompts/DebugMapboxIntegrationPrompt.ts b/src/prompts/DebugMapboxIntegrationPrompt.ts new file mode 100644 index 0000000..fe7d22f --- /dev/null +++ b/src/prompts/DebugMapboxIntegrationPrompt.ts @@ -0,0 +1,273 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import { BasePrompt, type PromptArgument } from './BasePrompt.js'; + +/** + * Prompt for systematically debugging Mapbox integration issues + * + * This prompt guides users through a comprehensive troubleshooting workflow: + * 1. Verify token validity and scopes + * 2. Check style configuration and existence + * 3. Validate GeoJSON if applicable + * 4. Test API endpoints + * 5. Review error messages and provide solutions + */ +export class DebugMapboxIntegrationPrompt extends BasePrompt { + readonly name = 'debug-mapbox-integration'; + readonly description = + 'Systematic troubleshooting workflow for Mapbox integration issues. Diagnoses token problems, style errors, API issues, and provides actionable solutions.'; + + readonly arguments: ReadonlyArray = [ + { + name: 'issue_description', + description: + 'Description of the problem (e.g., "map not loading", "401 error")', + required: true + }, + { + name: 'error_message', + description: 'Exact error message from console or logs, if available', + required: false + }, + { + name: 'style_id', + description: 'Mapbox style ID being used, if applicable', + required: false + }, + { + name: 'environment', + description: + 'Where the issue occurs: "development", "production", "staging"', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + const issueDescription = args['issue_description']; + const errorMessage = args['error_message']; + const styleId = args['style_id']; + const environment = args['environment'] || 'development'; + + let instructionText = `Debug Mapbox integration issue: "${issueDescription}" +${errorMessage ? `\nError message: "${errorMessage}"` : ''} +${styleId ? `Style ID: ${styleId}` : ''} +Environment: ${environment} + +Let's systematically diagnose and fix this issue. + +## Phase 1: Token Verification + +First, verify that tokens are properly configured: + +1. **List all tokens** + - Use list_tokens_tool to see all available tokens + - Check the output carefully + +2. **Analyze token issues** + Look for common problems: + - ❌ **No tokens exist**: Need to create tokens first + - ❌ **Wrong token type**: Using secret token (sk.*) in client code + - ❌ **Missing scopes**: Token doesn't have required scopes for the operation + - ❌ **URL restrictions**: Token is restricted to different URLs than where it's being used + - ❌ **Revoked token**: Token may have been revoked + +3. **Check required scopes** + Based on the issue, verify the token has the correct scopes: +`; + + if (errorMessage && errorMessage.includes('401')) { + instructionText += ` - **401 Unauthorized** typically means: + * Token is invalid or revoked + * Token is missing required scopes + * Token is not set correctly (check: mapboxgl.accessToken = '...') +`; + } else if (errorMessage && errorMessage.includes('403')) { + instructionText += ` - **403 Forbidden** typically means: + * Token lacks required scope for this operation + * URL restriction blocks this domain + * Rate limit exceeded +`; + } else { + instructionText += ` - Displaying maps: needs \`styles:read\`, \`fonts:read\` + - Creating styles: needs \`styles:write\` + - Listing styles: needs \`styles:list\` + - Managing tokens: needs \`tokens:read\` or \`tokens:write\` +`; + } + + if (styleId) { + instructionText += ` +## Phase 2: Style Verification + +Since a style ID was provided, let's verify it exists and is valid: + +1. **List user's styles** + - Use list_styles_tool to see all available styles + - Check if "${styleId}" appears in the list + +2. **Analyze style issues** + - ❌ **Style not found**: Style ID may be incorrect or from different account + - ❌ **Style from wrong account**: Using someone else's private style + - ❌ **Malformed style ID**: Should be in format "mapbox://styles/username/style-id" + +`; + } else { + instructionText += ` +## Phase 2: Style Verification + +No style ID provided. If the issue involves a specific style: +1. Ask the user for the style ID they're trying to use +2. Use list_styles_tool to verify it exists +3. Check the style configuration + +`; + } + + instructionText += `## Phase 3: Common Error Pattern Matching + +Analyze the error and provide specific solutions: + +`; + + if (errorMessage) { + instructionText += `Based on the error message "${errorMessage}", check for: + +`; + + if ( + errorMessage.toLowerCase().includes('401') || + errorMessage.toLowerCase().includes('unauthorized') + ) { + instructionText += `### 401 Unauthorized Solutions: +1. **Token not set**: Verify \`mapboxgl.accessToken = 'your-token'\` is called before creating the map +2. **Invalid token**: The token may be malformed, revoked, or expired +3. **Check token validity**: Use list_tokens_tool to verify the token exists +4. **Environment variable**: If using env vars, ensure MAPBOX_ACCESS_TOKEN is set correctly + +`; + } + + if ( + errorMessage.toLowerCase().includes('403') || + errorMessage.toLowerCase().includes('forbidden') + ) { + instructionText += `### 403 Forbidden Solutions: +1. **URL restriction**: Token is restricted to different URLs + - Development: Check token allows http://localhost:* + - Production: Check token allows your domain +2. **Missing scope**: Token needs additional scopes + - Use list_tokens_tool to check current scopes + - Use update_token_tool to add required scopes +3. **Rate limit**: Check if you've exceeded API rate limits + +`; + } + + if ( + errorMessage.toLowerCase().includes('style') || + errorMessage.toLowerCase().includes('404') + ) { + instructionText += `### Style Not Found Solutions: +1. **Wrong style URL**: Verify format is "mapbox://styles/username/style-id" +2. **Private style**: Ensure token has access to this style +3. **Style deleted**: Check if style still exists using list_styles_tool +4. **Typo in style ID**: Double-check the style ID spelling + +`; + } + + if ( + errorMessage.toLowerCase().includes('source') || + errorMessage.toLowerCase().includes('layer') + ) { + instructionText += `### Layer/Source Error Solutions: +1. **Source not defined**: Ensure source is added before layers that reference it +2. **Invalid layer type**: Check layer type matches source geometry +3. **Missing required property**: Verify all required layer properties are set +4. **Use get_reference_tool**: Get 'style-spec-reference' for layer requirements + +`; + } + } else { + instructionText += `### General debugging steps: + +**If map isn't displaying:** +1. Check browser console for errors +2. Verify container div exists: \`
\` +3. Ensure container has dimensions set in CSS: \`#map { height: 400px; }\` +4. Confirm token is set before map initialization +5. Check network tab for failed API requests + +**If map loads but looks wrong:** +1. Verify style ID is correct +2. Check if custom layers are properly configured +3. Ensure zoom/center coordinates are valid +4. Review layer order and visibility + +**If getting rate limit errors:** +1. Check token usage in Mapbox dashboard +2. Consider implementing request caching +3. Review rate limit documentation +4. Upgrade plan if needed + +`; + } + + instructionText += `## Phase 4: Testing & Validation + +Run these diagnostic checks: + +1. **Test token directly** + - Use the token to list_styles_tool or list_tokens_tool + - If these calls fail, the token itself is the issue + +2. **Generate test preview** + - If a style exists, use preview_style_tool to generate a working preview + - Compare the working preview with the broken implementation + +3. **Check documentation** + - Use get_reference_tool with: + * 'style-spec-reference' for style JSON issues + * 'token-scopes-reference' for token scope questions + * 'streets-v8-fields-reference' for data layer questions + +## Phase 5: Solution Summary + +After completing diagnostics, provide: + +1. **Root cause identified**: Clearly state what the problem is +2. **Immediate fix**: Step-by-step instructions to resolve the issue +3. **Prevention**: How to avoid this issue in the future +4. **Additional resources**: Relevant documentation links + +## Phase 6: Verification + +After applying fixes: + +1. **Retest the integration** + - Clear browser cache if testing in development + - Check console for any new errors + - Verify map loads and displays correctly + +2. **Monitor for issues** + - Watch for similar errors in production + - Set up error tracking (Sentry, etc.) + - Review Mapbox dashboard for usage patterns + +--- + +Begin the diagnostic process now. Run each phase systematically and present findings clearly to the user.`; + + return [ + { + role: 'user', + content: { + type: 'text', + text: instructionText + } + } + ]; + } +} diff --git a/src/prompts/DesignDataDrivenStylePrompt.ts b/src/prompts/DesignDataDrivenStylePrompt.ts new file mode 100644 index 0000000..7109801 --- /dev/null +++ b/src/prompts/DesignDataDrivenStylePrompt.ts @@ -0,0 +1,419 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import { BasePrompt, type PromptArgument } from './BasePrompt.js'; + +/** + * Prompt for creating data-driven map styles with dynamic properties + * + * This prompt guides users through creating styles that respond to data properties: + * 1. Understand the data structure and available properties + * 2. Choose appropriate data-driven styling approach + * 3. Design expressions for colors, sizes, and other properties + * 4. Create the style with data-driven layers + * 5. Test and preview the result + */ +export class DesignDataDrivenStylePrompt extends BasePrompt { + readonly name = 'design-data-driven-style'; + readonly description = + 'Create a map style with data-driven properties that respond dynamically to feature data. Guides you through expressions, color scales, and property-based styling.'; + + readonly arguments: ReadonlyArray = [ + { + name: 'style_name', + description: 'Name for the data-driven style', + required: true + }, + { + name: 'data_description', + description: + 'Description of the data (e.g., "population by city", "earthquake magnitudes", "property prices")', + required: true + }, + { + name: 'property_name', + description: + 'Name of the data property to visualize (e.g., "population", "magnitude", "price")', + required: true + }, + { + name: 'visualization_type', + description: + 'How to visualize: "color" (choropleth), "size" (proportional symbols), "both", "heatmap" (default: "color")', + required: false + }, + { + name: 'color_scheme', + description: + 'Color scheme: "sequential" (low to high), "diverging" (two extremes), "categorical" (distinct categories) (default: "sequential")', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + const styleName = args['style_name']; + const dataDescription = args['data_description']; + const propertyName = args['property_name']; + const visualizationType = args['visualization_type'] || 'color'; + const colorScheme = args['color_scheme'] || 'sequential'; + + let instructionText = `Create a data-driven map style: "${styleName}" + +Data: ${dataDescription} +Property to visualize: ${propertyName} +Visualization type: ${visualizationType} +Color scheme: ${colorScheme} + +This workflow will guide you through creating a map style with dynamic, data-driven properties. + +## Step 1: Understand Data-Driven Styling + +Data-driven styling in Mapbox uses **expressions** to calculate property values based on feature data. + +**Expression types:** +- \`["get", "${propertyName}"]\` - Get a feature property value +- \`["interpolate", ...]\` - Smoothly transition between values +- \`["step", ...]\` - Discrete steps/breaks in values +- \`["match", ...]\` - Match specific values (for categories) +- \`["case", ...]\` - Conditional logic + +## Step 2: Choose Your Data-Driven Approach + +Based on your requirements (${visualizationType} visualization with ${colorScheme} colors): + +`; + + if (visualizationType === 'color' || visualizationType === 'both') { + if (colorScheme === 'sequential') { + instructionText += `### Color by Value (Sequential) + +Use color to show values from low to high: + +\`\`\`json +{ + "type": "fill", + "paint": { + "fill-color": [ + "interpolate", + ["linear"], + ["get", "${propertyName}"], + 0, "#f7fbff", // Low values: light blue + 25, "#6baed6", // Medium-low: medium blue + 50, "#3182bd", // Medium: darker blue + 75, "#08519c", // Medium-high: deep blue + 100, "#08306b" // High values: darkest blue + ], + "fill-opacity": 0.7 + } +} +\`\`\` + +**Adjust the breakpoints** (0, 25, 50, 75, 100) based on your actual data range. + +`; + } else if (colorScheme === 'diverging') { + instructionText += `### Color by Value (Diverging) + +Use two colors to show deviation from a midpoint: + +\`\`\`json +{ + "type": "fill", + "paint": { + "fill-color": [ + "interpolate", + ["linear"], + ["get", "${propertyName}"], + 0, "#d7191c", // Low values: red + 25, "#fdae61", // Below average: orange + 50, "#ffffbf", // Average: yellow + 75, "#a6d96a", // Above average: light green + 100, "#1a9641" // High values: green + ], + "fill-opacity": 0.7 + } +} +\`\`\` + +**Use when**: Showing deviation from a norm (e.g., temperature above/below average). + +`; + } else if (colorScheme === 'categorical') { + instructionText += `### Color by Category + +Use distinct colors for different categories: + +\`\`\`json +{ + "type": "fill", + "paint": { + "fill-color": [ + "match", + ["get", "${propertyName}"], + "category1", "#e41a1c", // Red + "category2", "#377eb8", // Blue + "category3", "#4daf4a", // Green + "category4", "#984ea3", // Purple + "category5", "#ff7f00", // Orange + "#999999" // Default: gray + ], + "fill-opacity": 0.7 + } +} +\`\`\` + +**Replace** "category1", "category2", etc. with your actual category values. + +`; + } + } + + if (visualizationType === 'size' || visualizationType === 'both') { + instructionText += `### Size by Value (Proportional Symbols) + +Use circle size to represent magnitude: + +\`\`\`json +{ + "type": "circle", + "paint": { + "circle-radius": [ + "interpolate", + ["linear"], + ["get", "${propertyName}"], + 0, 5, // Low values: small circles (5px) + 25, 10, // Medium-low: 10px + 50, 15, // Medium: 15px + 75, 20, // Medium-high: 20px + 100, 30 // High values: large circles (30px) + ], + "circle-color": "#3182bd", + "circle-opacity": 0.6, + "circle-stroke-width": 1, + "circle-stroke-color": "#ffffff" + } +} +\`\`\` + +`; + } + + if (visualizationType === 'heatmap') { + instructionText += `### Heatmap Visualization + +Show density and intensity using a heatmap: + +\`\`\`json +{ + "type": "heatmap", + "paint": { + "heatmap-weight": [ + "interpolate", + ["linear"], + ["get", "${propertyName}"], + 0, 0, + 100, 1 + ], + "heatmap-intensity": [ + "interpolate", + ["linear"], + ["zoom"], + 0, 1, + 9, 3 + ], + "heatmap-color": [ + "interpolate", + ["linear"], + ["heatmap-density"], + 0, "rgba(33,102,172,0)", + 0.2, "rgb(103,169,207)", + 0.4, "rgb(209,229,240)", + 0.6, "rgb(253,219,199)", + 0.8, "rgb(239,138,98)", + 1, "rgb(178,24,43)" + ], + "heatmap-radius": [ + "interpolate", + ["linear"], + ["zoom"], + 0, 2, + 9, 20 + ] + } +} +\`\`\` + +`; + } + + instructionText += `## Step 3: Understand Your Data Range + +Before finalizing the style, you need to know: +1. **Minimum value** in your dataset for "${propertyName}" +2. **Maximum value** in your dataset for "${propertyName}" +3. **Typical distribution** (are most values low, high, or evenly distributed?) + +**If you have GeoJSON data:** +- You can provide it to validate_geojson_tool or visualize_geojson_tool +- These tools will show you the data structure and property values + +**If using Mapbox tilesets:** +- Use get_reference_tool with 'streets-v8-fields-reference' to see available fields +- Review typical value ranges in the documentation + +## Step 4: Create the Style + +Now create the data-driven style: + +1. **Start with a base style** + - Use style_builder_tool to generate a base style + - Provide instructions like: "Create a ${colorScheme} map for visualizing ${dataDescription}" + +2. **Add your data source** + - If using GeoJSON, you'll add it as a source: + \`\`\`json + "sources": { + "data": { + "type": "geojson", + "data": "YOUR_GEOJSON_URL_OR_INLINE_DATA" + } + } + \`\`\` + + - If using Mapbox tileset: + \`\`\`json + "sources": { + "data": { + "type": "vector", + "url": "mapbox://your.tileset" + } + } + \`\`\` + +3. **Add your data-driven layer** + - Use the expression examples from Step 2 + - Adjust breakpoints based on your actual data range + - Choose appropriate layer type (fill, circle, heatmap, etc.) + +4. **Create the style** + - Use create_style_tool with: + * name: "${styleName}" + * The style JSON you've built with data-driven properties + +## Step 5: Advanced Expressions (Optional) + +For more sophisticated styling: + +### Zoom-Based + Data-Driven + +Combine zoom level with data properties: + +\`\`\`json +{ + "circle-radius": [ + "interpolate", + ["linear"], + ["zoom"], + 5, [ + "interpolate", + ["linear"], + ["get", "${propertyName}"], + 0, 2, + 100, 8 + ], + 10, [ + "interpolate", + ["linear"], + ["get", "${propertyName}"], + 0, 5, + 100, 20 + ] + ] +} +\`\`\` + +### Conditional Styling + +Apply different styles based on conditions: + +\`\`\`json +{ + "fill-color": [ + "case", + ["<", ["get", "${propertyName}"], 10], "#fee5d9", // Low + ["<", ["get", "${propertyName}"], 50], "#fcae91", // Medium + ["<", ["get", "${propertyName}"], 100], "#fb6a4a", // High + "#de2d26" // Very high + ] +} +\`\`\` + +### Text Labels with Data + +Show property values as labels: + +\`\`\`json +{ + "type": "symbol", + "layout": { + "text-field": [ + "concat", + ["to-string", ["get", "${propertyName}"]], + " units" + ], + "text-size": 12 + } +} +\`\`\` + +## Step 6: Test and Preview + +1. **Generate preview** + - Use preview_style_tool with the style ID + - Check that colors/sizes reflect the data appropriately + - Verify the visualization is readable at different zoom levels + +2. **Iterate if needed** + - Adjust breakpoints if colors/sizes don't match data well + - Try different color schemes if readability is poor + - Consider adding labels or legends + +## Step 7: Best Practices Summary + +✅ **DO:** +- Use interpolate for smooth transitions (continuous data) +- Use step for clear breaks (ranked/classified data) +- Use match for categorical data +- Test at different zoom levels +- Ensure color contrasts are accessible (4.5:1 ratio) +- Document your data property names and ranges + +❌ **DON'T:** +- Use too many color breaks (5-7 is usually enough) +- Rely solely on color (add patterns or sizes for accessibility) +- Use red/green combinations (colorblind-unfriendly) +- Forget to handle null/undefined property values + +## Step 8: Documentation + +For more information on expressions: +- Use get_reference_tool with 'style-spec-reference' +- Search for "expressions" in the Mapbox documentation +- Review expression examples for your use case + +--- + +Begin creating your data-driven style now. Follow the steps systematically and present the resulting style to the user.`; + + return [ + { + role: 'user', + content: { + type: 'text', + text: instructionText + } + } + ]; + } +} diff --git a/src/prompts/SetupMapboxProjectPrompt.ts b/src/prompts/SetupMapboxProjectPrompt.ts new file mode 100644 index 0000000..71a9a25 --- /dev/null +++ b/src/prompts/SetupMapboxProjectPrompt.ts @@ -0,0 +1,269 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import { BasePrompt, type PromptArgument } from './BasePrompt.js'; + +/** + * Prompt for setting up a new Mapbox project from scratch + * + * This prompt orchestrates multiple tools to: + * 1. Create production and development tokens with appropriate scopes + * 2. Set up URL restrictions for security + * 3. Create an initial map style + * 4. Generate preview and test the integration + * 5. Provide implementation guidance + */ +export class SetupMapboxProjectPrompt extends BasePrompt { + readonly name = 'setup-mapbox-project'; + readonly description = + 'Complete setup workflow for a new Mapbox project. Creates tokens with proper security settings, initializes a map style, and provides integration guidance.'; + + readonly arguments: ReadonlyArray = [ + { + name: 'project_name', + description: 'Name of the project or application', + required: true + }, + { + name: 'project_type', + description: + 'Type of project: "web", "mobile", "backend", or "fullstack" (default: "web")', + required: false + }, + { + name: 'production_domain', + description: + 'Production domain for URL restrictions (e.g., "myapp.com"). Required for web/fullstack projects.', + required: false + }, + { + name: 'style_theme', + description: + 'Initial style theme: "light", "dark", "streets", "outdoors", "satellite" (default: "light")', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + const projectName = args['project_name']; + const projectType = args['project_type'] || 'web'; + const productionDomain = args['production_domain']; + const styleTheme = args['style_theme'] || 'light'; + + let instructionText = `Set up a complete Mapbox project for "${projectName}" (${projectType} application). + +Follow these steps carefully to ensure secure and proper configuration: + +## Step 1: Create Development Token + +Create a public token for local development: +- Use create_token_tool with these parameters: + * note: "${projectName} - Development" + * scopes: ["styles:read", "fonts:read"] + * allowedUrls: ["http://localhost:*", "http://127.0.0.1:*"] +- Save the token value and note the token ID + +`; + + if (projectType === 'web' || projectType === 'fullstack') { + if (productionDomain) { + instructionText += `## Step 2: Create Production Token + +Create a public token for production with URL restrictions: +- Use create_token_tool with these parameters: + * note: "${projectName} - Production" + * scopes: ["styles:read", "fonts:read"] + * allowedUrls: ["https://${productionDomain}/*", "https://www.${productionDomain}/*"] +- Save the token value and note the token ID + +`; + } else { + instructionText += `## Step 2: Create Production Token + +⚠️ Production domain not provided. Create a public token without URL restrictions (less secure): +- Use create_token_tool with these parameters: + * note: "${projectName} - Production (No URL Restrictions)" + * scopes: ["styles:read", "fonts:read"] +- **IMPORTANT**: Add URL restrictions later using update_token_tool once the domain is known +- Save the token value and note the token ID + +`; + } + } else if (projectType === 'mobile') { + instructionText += `## Step 2: Create Mobile Token + +Create a public token for mobile app: +- Use create_token_tool with these parameters: + * note: "${projectName} - Mobile" + * scopes: ["styles:read", "fonts:read", "vision:read"] +- **Note**: Mobile apps can't use URL restrictions, so monitor usage carefully +- Save the token value and note the token ID + +`; + } + + if (projectType === 'backend' || projectType === 'fullstack') { + instructionText += `## Step 3: Create Secret Token for Backend + +Create a secret token for server-side operations: +- Use create_token_tool with these parameters: + * note: "${projectName} - Backend (SECRET)" + * scopes: ["styles:read", "styles:write", "styles:list"] +- ⚠️ **CRITICAL**: This is a SECRET token - never expose in client code +- Store in environment variables or secret manager +- Save the token value and note the token ID + +`; + } + + const stepNumber = projectType === 'backend' ? 4 : 3; + instructionText += `## Step ${stepNumber}: Create Initial Map Style + +Create a starter map style for the project: +- Use style_builder_tool to generate a ${styleTheme} themed style: + * Provide a description like: "${styleTheme} theme for ${projectName}, optimized for ${projectType} use" + * Let the AI generate appropriate styling based on the theme +- Then use create_style_tool with: + * name: "${projectName} - ${styleTheme.charAt(0).toUpperCase() + styleTheme.slice(1)}" + * Use the style JSON generated by style_builder_tool +- Save the style ID from the response + +`; + + const nextStep = stepNumber + 1; + instructionText += `## Step ${nextStep}: Generate Preview + +Create a shareable preview of the map: +- Use preview_style_tool with the style ID you just created +- Use zoom level 12 and let it pick a nice default location +- The preview will automatically use your public token +- Save the preview URL + +## Step ${nextStep + 1}: Provide Integration Instructions + +Present the user with a complete setup summary: + +\`\`\` +🎉 Mapbox Project Setup Complete! + +Project: ${projectName} +Type: ${projectType} + +📋 Tokens Created: +`; + + if (projectType === 'web' || projectType === 'fullstack') { + instructionText += `- Development Token: [show token] (localhost only) +- Production Token: [show token] ${productionDomain ? `(${productionDomain} only)` : '(⚠️ no URL restrictions)'} +`; + } + + if (projectType === 'mobile') { + instructionText += `- Mobile Token: [show token] +`; + } + + if (projectType === 'backend' || projectType === 'fullstack') { + instructionText += `- Backend Secret Token: [show token] ⚠️ KEEP SECRET +`; + } + + instructionText += ` +🗺️ Map Style: +- Style ID: [show style ID] +- Theme: ${styleTheme} +- Preview: [show preview URL] + +📦 Next Steps: +`; + + if (projectType === 'web' || projectType === 'fullstack') { + instructionText += ` +1. Install Mapbox GL JS: + npm install mapbox-gl + +2. Add to your HTML: + + + +3. Initialize the map: + mapboxgl.accessToken = 'YOUR_TOKEN_HERE'; // Use dev token for localhost, prod token for production + const map = new mapboxgl.Map({ + container: 'map', + style: 'mapbox://styles/YOUR_USERNAME/YOUR_STYLE_ID', + center: [-74.5, 40], + zoom: 9 + }); +`; + } + + if (projectType === 'mobile') { + instructionText += ` +1. Install Mapbox Maps SDK for your platform: + - iOS: https://docs.mapbox.com/ios/maps/guides/install/ + - Android: https://docs.mapbox.com/android/maps/guides/install/ + +2. Configure your token in the app +3. Load the style using your style ID +`; + } + + if (projectType === 'backend') { + instructionText += ` +1. Store your secret token in environment variables: + export MAPBOX_ACCESS_TOKEN='YOUR_SECRET_TOKEN' + +2. Use the Mapbox APIs for server-side operations: + - Styles API: https://docs.mapbox.com/api/maps/styles/ + - Static Images: https://docs.mapbox.com/api/maps/static-images/ +`; + } + + instructionText += ` +🔒 Security Reminders: +- ✅ Public tokens (pk.*) are safe in client code with URL restrictions +- ❌ NEVER expose secret tokens (sk.*) in client code or version control +- 🔄 Rotate tokens every 90 days for production +- 📊 Monitor token usage in your Mapbox dashboard +\`\`\` + +**Important Security Notes:** +`; + + if ( + !productionDomain && + (projectType === 'web' || projectType === 'fullstack') + ) { + instructionText += ` +⚠️ Your production token has NO URL RESTRICTIONS. To secure it: +1. Use list_tokens_tool to find the production token ID +2. Use update_token_tool to add allowedUrls once your domain is ready +`; + } + + if (projectType === 'backend' || projectType === 'fullstack') { + instructionText += ` +⚠️ Your secret token has full API access. To protect it: +1. Store in environment variables (.env file) +2. Add .env to .gitignore +3. Use a secret manager (AWS Secrets Manager, HashiCorp Vault) for production +4. Never commit tokens to version control +`; + } + + instructionText += ` + +The setup is now complete! The user can start building their map application.`; + + return [ + { + role: 'user', + content: { + type: 'text', + text: instructionText + } + } + ]; + } +} diff --git a/src/prompts/promptRegistry.ts b/src/prompts/promptRegistry.ts index 82b3122..d2ec720 100644 --- a/src/prompts/promptRegistry.ts +++ b/src/prompts/promptRegistry.ts @@ -4,12 +4,18 @@ import { CreateAndPreviewStylePrompt } from './CreateAndPreviewStylePrompt.js'; import { BuildCustomMapPrompt } from './BuildCustomMapPrompt.js'; import { AnalyzeGeojsonPrompt } from './AnalyzeGeojsonPrompt.js'; +import { SetupMapboxProjectPrompt } from './SetupMapboxProjectPrompt.js'; +import { DebugMapboxIntegrationPrompt } from './DebugMapboxIntegrationPrompt.js'; +import { DesignDataDrivenStylePrompt } from './DesignDataDrivenStylePrompt.js'; // Central registry of all prompts export const ALL_PROMPTS = [ new CreateAndPreviewStylePrompt(), new BuildCustomMapPrompt(), - new AnalyzeGeojsonPrompt() + new AnalyzeGeojsonPrompt(), + new SetupMapboxProjectPrompt(), + new DebugMapboxIntegrationPrompt(), + new DesignDataDrivenStylePrompt() ] as const; export type PromptInstance = (typeof ALL_PROMPTS)[number]; diff --git a/test/prompts/DebugMapboxIntegrationPrompt.test.ts b/test/prompts/DebugMapboxIntegrationPrompt.test.ts new file mode 100644 index 0000000..8bb6292 --- /dev/null +++ b/test/prompts/DebugMapboxIntegrationPrompt.test.ts @@ -0,0 +1,150 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { DebugMapboxIntegrationPrompt } from '../../src/prompts/DebugMapboxIntegrationPrompt.js'; + +describe('DebugMapboxIntegrationPrompt', () => { + const prompt = new DebugMapboxIntegrationPrompt(); + + it('should have correct metadata', () => { + expect(prompt.name).toBe('debug-mapbox-integration'); + expect(prompt.description).toContain('troubleshooting workflow'); + expect(prompt.arguments).toHaveLength(4); + }); + + it('should require issue_description argument', () => { + const requiredArg = prompt.arguments.find( + (arg) => arg.name === 'issue_description' + ); + expect(requiredArg).toBeDefined(); + expect(requiredArg?.required).toBe(true); + }); + + it('should have optional arguments', () => { + const optionalArgs = ['error_message', 'style_id', 'environment']; + optionalArgs.forEach((argName) => { + const arg = prompt.arguments.find((a) => a.name === argName); + expect(arg).toBeDefined(); + expect(arg?.required).toBe(false); + }); + }); + + it('should generate messages with required arguments', () => { + const result = prompt.execute({ + issue_description: 'Map not loading' + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + + const text = result.messages[0].content.text; + expect(text).toContain('Map not loading'); + expect(text).toContain('list_tokens_tool'); + expect(text).toContain('Phase 1'); + }); + + it('should include error message when provided', () => { + const result = prompt.execute({ + issue_description: 'Map not loading', + error_message: '401 Unauthorized' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('401 Unauthorized'); + }); + + it('should provide specific 401 debugging steps', () => { + const result = prompt.execute({ + issue_description: 'Auth error', + error_message: '401 unauthorized' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('401 Unauthorized'); + expect(text).toContain('Token not set'); + expect(text).toContain('Invalid token'); + }); + + it('should provide specific 403 debugging steps', () => { + const result = prompt.execute({ + issue_description: 'Forbidden error', + error_message: '403 Forbidden' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('403 Forbidden'); + expect(text).toContain('URL restriction'); + expect(text).toContain('Missing scope'); + }); + + it('should include style verification when style_id provided', () => { + const result = prompt.execute({ + issue_description: 'Style not loading', + style_id: 'my-style-id' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('my-style-id'); + expect(text).toContain('list_styles_tool'); + expect(text).toContain('Style Verification'); + }); + + it('should mention environment in output', () => { + const result = prompt.execute({ + issue_description: 'Issue', + environment: 'production' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('production'); + }); + + it('should include style/layer error solutions for style-related errors', () => { + const result = prompt.execute({ + issue_description: 'Layer error', + error_message: 'Layer source not found' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Layer/Source Error Solutions'); + }); + + it('should include general debugging steps when no error message provided', () => { + const result = prompt.execute({ + issue_description: 'Something is wrong' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('General debugging'); + expect(text).toContain('browser console'); + }); + + it('should include all diagnostic phases', () => { + const result = prompt.execute({ + issue_description: 'Issue' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Phase 1'); + expect(text).toContain('Phase 2'); + expect(text).toContain('Phase 3'); + expect(text).toContain('Phase 4'); + expect(text).toContain('Phase 5'); + expect(text).toContain('Phase 6'); + }); + + it('should throw error if required argument is missing', () => { + expect(() => { + prompt.execute({}); + }).toThrow('Missing required arguments: issue_description'); + }); + + it('should return proper metadata', () => { + const metadata = prompt.getMetadata(); + expect(metadata.name).toBe(prompt.name); + expect(metadata.description).toBe(prompt.description); + expect(metadata.arguments).toEqual(prompt.arguments); + }); +}); diff --git a/test/prompts/DesignDataDrivenStylePrompt.test.ts b/test/prompts/DesignDataDrivenStylePrompt.test.ts new file mode 100644 index 0000000..6b7893a --- /dev/null +++ b/test/prompts/DesignDataDrivenStylePrompt.test.ts @@ -0,0 +1,226 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { DesignDataDrivenStylePrompt } from '../../src/prompts/DesignDataDrivenStylePrompt.js'; + +describe('DesignDataDrivenStylePrompt', () => { + const prompt = new DesignDataDrivenStylePrompt(); + + it('should have correct metadata', () => { + expect(prompt.name).toBe('design-data-driven-style'); + expect(prompt.description).toContain('data-driven properties'); + expect(prompt.arguments).toHaveLength(5); + }); + + it('should require essential arguments', () => { + const requiredArgs = ['style_name', 'data_description', 'property_name']; + requiredArgs.forEach((argName) => { + const arg = prompt.arguments.find((a) => a.name === argName); + expect(arg).toBeDefined(); + expect(arg?.required).toBe(true); + }); + }); + + it('should have optional arguments', () => { + const optionalArgs = ['visualization_type', 'color_scheme']; + optionalArgs.forEach((argName) => { + const arg = prompt.arguments.find((a) => a.name === argName); + expect(arg).toBeDefined(); + expect(arg?.required).toBe(false); + }); + }); + + it('should generate messages with required arguments', () => { + const result = prompt.execute({ + style_name: 'Population Map', + data_description: 'City populations', + property_name: 'population' + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + + const text = result.messages[0].content.text; + expect(text).toContain('Population Map'); + expect(text).toContain('City populations'); + expect(text).toContain('population'); + }); + + it('should include expression examples', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('interpolate'); + expect(text).toContain('["get", "value"]'); + expect(text).toContain('Expression types'); + }); + + it('should include color visualization for color type', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value', + visualization_type: 'color' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('fill-color'); + expect(text).toContain('Color by Value'); + }); + + it('should include size visualization for size type', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value', + visualization_type: 'size' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('circle-radius'); + expect(text).toContain('Size by Value'); + }); + + it('should include both color and size for both type', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value', + visualization_type: 'both' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('fill-color'); + expect(text).toContain('circle-radius'); + }); + + it('should include heatmap configuration for heatmap type', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value', + visualization_type: 'heatmap' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('heatmap'); + expect(text).toContain('heatmap-weight'); + expect(text).toContain('heatmap-color'); + }); + + it('should use sequential color scheme by default', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('sequential'); + }); + + it('should include diverging colors when specified', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value', + color_scheme: 'diverging' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Diverging'); + expect(text).toContain('midpoint'); + }); + + it('should include categorical colors when specified', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value', + color_scheme: 'categorical' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('match'); + expect(text).toContain('Category'); + }); + + it('should include all workflow steps', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Step 1'); + expect(text).toContain('Step 2'); + expect(text).toContain('Step 3'); + expect(text).toContain('Step 4'); + expect(text).toContain('Step 5'); + expect(text).toContain('Step 6'); + expect(text).toContain('Step 7'); + expect(text).toContain('Step 8'); + }); + + it('should include advanced expression examples', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Advanced Expressions'); + expect(text).toContain('Zoom-Based'); + expect(text).toContain('Conditional'); + }); + + it('should include best practices', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Best Practices'); + expect(text).toContain('DO:'); + expect(text).toContain("DON'T:"); + }); + + it('should reference style builder tool', () => { + const result = prompt.execute({ + style_name: 'Test', + data_description: 'Data', + property_name: 'value' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('style_builder_tool'); + expect(text).toContain('create_style_tool'); + }); + + it('should throw error if required arguments are missing', () => { + expect(() => { + prompt.execute({ style_name: 'Test' }); + }).toThrow('Missing required arguments'); + + expect(() => { + prompt.execute({}); + }).toThrow('Missing required arguments'); + }); + + it('should return proper metadata', () => { + const metadata = prompt.getMetadata(); + expect(metadata.name).toBe(prompt.name); + expect(metadata.description).toBe(prompt.description); + expect(metadata.arguments).toEqual(prompt.arguments); + }); +}); diff --git a/test/prompts/SetupMapboxProjectPrompt.test.ts b/test/prompts/SetupMapboxProjectPrompt.test.ts new file mode 100644 index 0000000..8857d79 --- /dev/null +++ b/test/prompts/SetupMapboxProjectPrompt.test.ts @@ -0,0 +1,126 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { SetupMapboxProjectPrompt } from '../../src/prompts/SetupMapboxProjectPrompt.js'; + +describe('SetupMapboxProjectPrompt', () => { + const prompt = new SetupMapboxProjectPrompt(); + + it('should have correct metadata', () => { + expect(prompt.name).toBe('setup-mapbox-project'); + expect(prompt.description).toContain('Complete setup workflow'); + expect(prompt.arguments).toHaveLength(4); + }); + + it('should require project_name argument', () => { + const requiredArg = prompt.arguments.find( + (arg) => arg.name === 'project_name' + ); + expect(requiredArg).toBeDefined(); + expect(requiredArg?.required).toBe(true); + }); + + it('should have optional arguments', () => { + const optionalArgs = ['project_type', 'production_domain', 'style_theme']; + optionalArgs.forEach((argName) => { + const arg = prompt.arguments.find((a) => a.name === argName); + expect(arg).toBeDefined(); + expect(arg?.required).toBe(false); + }); + }); + + it('should generate messages with required arguments', () => { + const result = prompt.execute({ + project_name: 'MyApp' + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + + const text = result.messages[0].content.text; + expect(text).toContain('MyApp'); + expect(text).toContain('create_token_tool'); + expect(text).toContain('style_builder_tool'); + }); + + it('should include development token instructions', () => { + const result = prompt.execute({ + project_name: 'MyApp' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Development'); + expect(text).toContain('localhost'); + }); + + it('should include production domain in URL restrictions when provided', () => { + const result = prompt.execute({ + project_name: 'MyApp', + project_type: 'web', + production_domain: 'myapp.com' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('myapp.com'); + expect(text).toContain('Production Token'); + }); + + it('should warn about missing URL restrictions when no domain provided', () => { + const result = prompt.execute({ + project_name: 'MyApp', + project_type: 'web' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('No URL Restrictions'); + expect(text).toContain('less secure'); + }); + + it('should include backend token instructions for fullstack projects', () => { + const result = prompt.execute({ + project_name: 'MyApp', + project_type: 'fullstack' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Backend'); + expect(text).toContain('SECRET'); + expect(text).toContain('environment variables'); + }); + + it('should include mobile-specific instructions', () => { + const result = prompt.execute({ + project_name: 'MyApp', + project_type: 'mobile' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('Mobile'); + expect(text).toContain('vision:read'); + }); + + it('should use specified style theme', () => { + const result = prompt.execute({ + project_name: 'MyApp', + style_theme: 'dark' + }); + + const text = result.messages[0].content.text; + expect(text).toContain('dark'); + }); + + it('should throw error if required argument is missing', () => { + expect(() => { + prompt.execute({}); + }).toThrow('Missing required arguments: project_name'); + }); + + it('should return proper metadata', () => { + const metadata = prompt.getMetadata(); + expect(metadata.name).toBe(prompt.name); + expect(metadata.description).toBe(prompt.description); + expect(metadata.arguments).toEqual(prompt.arguments); + }); +}); diff --git a/test/prompts/promptRegistry.test.ts b/test/prompts/promptRegistry.test.ts index 453e429..6ec4ce0 100644 --- a/test/prompts/promptRegistry.test.ts +++ b/test/prompts/promptRegistry.test.ts @@ -11,7 +11,7 @@ describe('promptRegistry', () => { describe('getAllPrompts', () => { it('should return all registered prompts', () => { const prompts = getAllPrompts(); - expect(prompts).toHaveLength(3); + expect(prompts).toHaveLength(6); }); it('should include create-and-preview-style prompt', () => { @@ -35,6 +35,27 @@ describe('promptRegistry', () => { expect(prompt?.description).toContain('Analyze and visualize GeoJSON'); }); + it('should include setup-mapbox-project prompt', () => { + const prompts = getAllPrompts(); + const prompt = prompts.find((p) => p.name === 'setup-mapbox-project'); + expect(prompt).toBeDefined(); + expect(prompt?.description).toContain('Complete setup workflow'); + }); + + it('should include debug-mapbox-integration prompt', () => { + const prompts = getAllPrompts(); + const prompt = prompts.find((p) => p.name === 'debug-mapbox-integration'); + expect(prompt).toBeDefined(); + expect(prompt?.description).toContain('troubleshooting workflow'); + }); + + it('should include design-data-driven-style prompt', () => { + const prompts = getAllPrompts(); + const prompt = prompts.find((p) => p.name === 'design-data-driven-style'); + expect(prompt).toBeDefined(); + expect(prompt?.description).toContain('data-driven properties'); + }); + it('should return readonly array', () => { const prompts = getAllPrompts(); expect(Object.isFrozen(prompts)).toBe(false); // ReadonlyArray is not frozen, just typed @@ -58,7 +79,10 @@ describe('promptRegistry', () => { const promptNames = [ 'create-and-preview-style', 'build-custom-map', - 'analyze-geojson' + 'analyze-geojson', + 'setup-mapbox-project', + 'debug-mapbox-integration', + 'design-data-driven-style' ]; promptNames.forEach((name) => {