From b747a1f12171f9ccb224437dbfde86f61ef7cfc6 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 17 Dec 2025 00:18:32 -0500 Subject: [PATCH 1/2] [prompts] Add MCP prompts support with 3 initial workflow templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements base prompt infrastructure and 3 common geospatial workflow prompts that capture domain expertise and enable AI agents to follow best practices for multi-step tasks. Infrastructure: - BasePrompt class with argument validation and metadata generation - Prompt registry for managing available prompts - ListPrompts and GetPrompt request handlers in MCP server - Prompts capability added to server configuration Prompts added: 1. find-places-nearby: Search for places near a location with map visualization - Guides geocoding → category search → map visualization workflow - Example: "Find coffee shops near downtown Seattle" 2. get-directions: Turn-by-turn directions with route visualization - Guides geocoding → routing → map visualization workflow - Supports driving, walking, cycling modes - Example: "Get directions from LAX to Hollywood" 3. show-reachable-areas: Isochrone visualization for accessibility analysis - Guides geocoding → isochrone → map visualization workflow - Example: "Show me areas within 30 minutes of downtown" These prompts help AI agents: - Follow consistent multi-step workflows - Use the right tools in the right order - Generate comprehensive, user-friendly outputs - Combine multiple tools effectively Benefits for RAG-based agents: - Prompts can be semantically matched to user intents - Pre-built templates reduce error rates - Domain expertise captured in reusable workflows - Consistent output formatting across similar tasks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/index.ts | 39 ++++++++++++- src/prompts/BasePrompt.ts | 64 ++++++++++++++++++++ src/prompts/FindPlacesNearbyPrompt.ts | 73 +++++++++++++++++++++++ src/prompts/GetDirectionsPrompt.ts | 74 ++++++++++++++++++++++++ src/prompts/ShowReachableAreasPrompt.ts | 77 +++++++++++++++++++++++++ src/prompts/promptRegistry.ts | 44 ++++++++++++++ 6 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 src/prompts/BasePrompt.ts create mode 100644 src/prompts/FindPlacesNearbyPrompt.ts create mode 100644 src/prompts/GetDirectionsPrompt.ts create mode 100644 src/prompts/ShowReachableAreasPrompt.ts create mode 100644 src/prompts/promptRegistry.ts diff --git a/src/index.ts b/src/index.ts index 6f8552c..2489a23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,9 +11,14 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + ListPromptsRequestSchema, + GetPromptRequestSchema +} from '@modelcontextprotocol/sdk/types.js'; import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; import { getAllTools } from './tools/toolRegistry.js'; import { getAllResources } from './resources/resourceRegistry.js'; +import { getAllPrompts, getPromptByName } from './prompts/promptRegistry.js'; import { getVersionInfo } from './utils/versionUtils.js'; import { initializeTracing, @@ -66,7 +71,8 @@ const server = new McpServer( { capabilities: { tools: {}, - resources: {} + resources: {}, + prompts: {} } } ); @@ -81,6 +87,37 @@ allResources.forEach((resource) => { resource.installTo(server); }); +// Register prompt handlers +server.server.setRequestHandler(ListPromptsRequestSchema, async () => { + const allPrompts = getAllPrompts(); + return { + prompts: allPrompts.map((prompt) => prompt.getMetadata()) + }; +}); + +server.server.setRequestHandler(GetPromptRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + const prompt = getPromptByName(name); + if (!prompt) { + throw new Error(`Prompt not found: ${name}`); + } + + // Convert args to object for easier access + const argsObj: Record = {}; + if (args && typeof args === 'object') { + Object.assign(argsObj, args); + } + + // Get the prompt messages with filled-in arguments + const messages = prompt.getMessages(argsObj); + + return { + description: prompt.description, + messages + }; +}); + async function main() { // Initialize OpenTelemetry tracing if not in test mode let tracingInitialized = false; diff --git a/src/prompts/BasePrompt.ts b/src/prompts/BasePrompt.ts new file mode 100644 index 0000000..70f1f28 --- /dev/null +++ b/src/prompts/BasePrompt.ts @@ -0,0 +1,64 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { + type Prompt, + type PromptArgument, + type PromptMessage +} from '@modelcontextprotocol/sdk/types.js'; + +/** + * Base class for all MCP prompts. + * + * Prompts are pre-built, parameterized workflows that guide multi-step geospatial tasks. + * They capture domain expertise and best practices for common use cases. + */ +export abstract class BasePrompt { + /** + * Unique identifier for this prompt (e.g., "find-places-nearby") + */ + abstract readonly name: string; + + /** + * Human-readable description of what this prompt does + */ + abstract readonly description: string; + + /** + * Arguments this prompt accepts + */ + abstract readonly arguments: PromptArgument[]; + + /** + * Get the prompt metadata for listing + */ + getMetadata(): Prompt { + return { + name: this.name, + description: this.description, + arguments: this.arguments + }; + } + + /** + * Generate the prompt messages with filled-in arguments + * + * @param args - The argument values provided by the user/agent + * @returns Array of messages to send to the LLM + */ + abstract getMessages(args: Record): PromptMessage[]; + + /** + * Validate that all required arguments are provided + * + * @param args - The argument values to validate + * @throws Error if required arguments are missing + */ + protected validateArguments(args: Record): void { + for (const arg of this.arguments) { + if (arg.required && !args[arg.name]) { + throw new Error(`Missing required argument: ${arg.name}`); + } + } + } +} diff --git a/src/prompts/FindPlacesNearbyPrompt.ts b/src/prompts/FindPlacesNearbyPrompt.ts new file mode 100644 index 0000000..cb9eb0f --- /dev/null +++ b/src/prompts/FindPlacesNearbyPrompt.ts @@ -0,0 +1,73 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { BasePrompt } from './BasePrompt.js'; +import type { + PromptArgument, + PromptMessage +} from '@modelcontextprotocol/sdk/types.js'; + +/** + * Prompt for finding places near a location with optional map visualization. + * + * This prompt guides the agent through: + * 1. Geocoding the location (if needed) + * 2. Searching for places by category + * 3. Formatting results with map visualization + * + * Example queries: + * - "Find coffee shops near downtown Seattle" + * - "Show me restaurants near 123 Main St" + * - "What museums are near the Eiffel Tower?" + */ +export class FindPlacesNearbyPrompt extends BasePrompt { + readonly name = 'find-places-nearby'; + readonly description = + 'Helps you search for specific types of places near a location with optional map visualization'; + + readonly arguments: PromptArgument[] = [ + { + name: 'location', + description: + 'The location to search near (address, place name, or coordinates)', + required: true + }, + { + name: 'category', + description: + 'Type of place to search for (e.g., "coffee shops", "restaurants", "museums")', + required: false + }, + { + name: 'radius', + description: 'Search radius in meters (default: 1000)', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + this.validateArguments(args); + + const { location, category, radius } = args; + const radiusText = radius ? ` within ${radius} meters` : ''; + const categoryText = category || 'places'; + + return [ + { + role: 'user', + content: { + type: 'text', + text: `Find ${categoryText} near ${location}${radiusText}. + +Please follow these steps: +1. If the location is not in coordinate format, geocode it first using search_and_geocode_tool +2. Use category_search_tool or search_tool to find ${categoryText} near the location +3. Display the results on a map showing the location and the found places +4. Provide a summary of the top results with key details (name, address, distance) + +Make the output clear and actionable.` + } + } + ]; + } +} diff --git a/src/prompts/GetDirectionsPrompt.ts b/src/prompts/GetDirectionsPrompt.ts new file mode 100644 index 0000000..57e391f --- /dev/null +++ b/src/prompts/GetDirectionsPrompt.ts @@ -0,0 +1,74 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { BasePrompt } from './BasePrompt.js'; +import type { + PromptArgument, + PromptMessage +} from '@modelcontextprotocol/sdk/types.js'; + +/** + * Prompt for getting turn-by-turn directions between two locations. + * + * This prompt guides the agent through: + * 1. Geocoding start and end locations (if needed) + * 2. Getting directions via the appropriate routing profile + * 3. Visualizing the route on a map + * 4. Providing clear turn-by-turn instructions + * + * Example queries: + * - "Get directions from my office to the airport" + * - "How do I drive from Seattle to Portland?" + * - "Walking directions from here to the museum" + */ +export class GetDirectionsPrompt extends BasePrompt { + readonly name = 'get-directions'; + readonly description = + 'Provides turn-by-turn directions between two locations with options for different travel modes'; + + readonly arguments: PromptArgument[] = [ + { + name: 'from', + description: 'Starting location (address, place name, or coordinates)', + required: true + }, + { + name: 'to', + description: 'Destination location (address, place name, or coordinates)', + required: true + }, + { + name: 'mode', + description: + 'Travel mode: driving, walking, or cycling (default: driving)', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + this.validateArguments(args); + + const { from, to, mode = 'driving' } = args; + + return [ + { + role: 'user', + content: { + type: 'text', + text: `Get ${mode} directions from ${from} to ${to}. + +Please follow these steps: +1. Geocode both the starting point and destination if they're not in coordinate format +2. Use directions_tool to get the route with profile set to ${mode} +3. Display the route on a map with clear start and end markers +4. Provide: + - Total distance and estimated travel time + - Turn-by-turn directions (summarized if very long) + - Any notable features along the route (tolls, ferries, etc.) + +Format the output to be clear and easy to follow.` + } + } + ]; + } +} diff --git a/src/prompts/ShowReachableAreasPrompt.ts b/src/prompts/ShowReachableAreasPrompt.ts new file mode 100644 index 0000000..64445d1 --- /dev/null +++ b/src/prompts/ShowReachableAreasPrompt.ts @@ -0,0 +1,77 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { BasePrompt } from './BasePrompt.js'; +import type { + PromptArgument, + PromptMessage +} from '@modelcontextprotocol/sdk/types.js'; + +/** + * Prompt for visualizing areas reachable within a specified time from a location. + * + * This prompt guides the agent through: + * 1. Geocoding the starting location (if needed) + * 2. Calculating isochrones for the specified travel time + * 3. Visualizing the reachable areas on a map + * 4. Providing context about what the isochrone represents + * + * Example queries: + * - "Show me areas I can reach in 15 minutes from downtown" + * - "What's the 30-minute driving range from our warehouse?" + * - "Display my 10-minute walk radius from home" + */ +export class ShowReachableAreasPrompt extends BasePrompt { + readonly name = 'show-reachable-areas'; + readonly description = + 'Visualizes areas that can be reached from a location within a specified time using isochrones'; + + readonly arguments: PromptArgument[] = [ + { + name: 'location', + description: 'Starting location (address, place name, or coordinates)', + required: true + }, + { + name: 'time_minutes', + description: 'Travel time in minutes (default: 15)', + required: false + }, + { + name: 'mode', + description: + 'Travel mode: driving, walking, or cycling (default: driving)', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + this.validateArguments(args); + + const { location, time_minutes = '15', mode = 'driving' } = args; + + return [ + { + role: 'user', + content: { + type: 'text', + text: `Show areas reachable within ${time_minutes} minutes of ${mode} from ${location}. + +Please follow these steps: +1. Geocode the location if it's not in coordinate format +2. Use isochrone_tool to calculate the ${time_minutes}-minute ${mode} isochrone +3. Visualize the reachable area on a map with: + - The starting location clearly marked + - The isochrone polygon showing the reachable area + - Appropriate styling to make it easy to understand +4. Provide context explaining: + - What area is covered (approximate square miles/km) + - What this means practically (e.g., "You can reach X locations within ${time_minutes} minutes") + - Any limitations or caveats (traffic conditions, time of day, etc.) + +Make the visualization clear and the explanation actionable.` + } + } + ]; + } +} diff --git a/src/prompts/promptRegistry.ts b/src/prompts/promptRegistry.ts new file mode 100644 index 0000000..2371b2c --- /dev/null +++ b/src/prompts/promptRegistry.ts @@ -0,0 +1,44 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { FindPlacesNearbyPrompt } from './FindPlacesNearbyPrompt.js'; +import { GetDirectionsPrompt } from './GetDirectionsPrompt.js'; +import { ShowReachableAreasPrompt } from './ShowReachableAreasPrompt.js'; + +/** + * Central registry of all available prompts. + * + * This module maintains a readonly collection of prompt instances and provides + * type-safe access methods. + */ + +// Instantiate all prompts +const ALL_PROMPTS = [ + new FindPlacesNearbyPrompt(), + new GetDirectionsPrompt(), + new ShowReachableAreasPrompt() +] as const; + +/** + * Type representing any prompt instance + */ +export type PromptInstance = (typeof ALL_PROMPTS)[number]; + +/** + * Get all registered prompts + * + * @returns Readonly array of all prompt instances + */ +export function getAllPrompts(): readonly PromptInstance[] { + return ALL_PROMPTS; +} + +/** + * Get a prompt by name + * + * @param name - The prompt name to look up + * @returns The prompt instance, or undefined if not found + */ +export function getPromptByName(name: string): PromptInstance | undefined { + return ALL_PROMPTS.find((prompt) => prompt.name === name); +} From a982016b6a872713bf69a8bf3f67295c58f3f05a Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 17 Dec 2025 00:26:04 -0500 Subject: [PATCH 2/2] [tests] Add unit tests for prompts infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds comprehensive tests for the prompts feature focusing on high-ROI areas: core infrastructure and registration. Tests added (27 total): 1. BasePrompt validation (17 tests) - Metadata structure compliance with MCP protocol - Argument validation (required vs optional) - Message generation with various argument combinations - Edge cases (no arguments, multiple required args) - Error handling for missing required arguments 2. Prompt Registry (10 tests) - getAllPrompts returns all registered prompts - getPromptByName lookup (valid and invalid names) - Unique naming validation - Metadata structure for all prompts - Kebab-case naming convention enforcement - Per-prompt metadata validation (arguments, descriptions) Test philosophy: - Focus on infrastructure and registration (high value, stable API) - Skip end-to-end workflow tests (low value, high maintenance) - Skip message content parsing (too brittle, prompts change often) All 27 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- test/prompts/BasePrompt.test.ts | 288 ++++++++++++++++++++++++++++ test/prompts/promptRegistry.test.ts | 189 ++++++++++++++++++ 2 files changed, 477 insertions(+) create mode 100644 test/prompts/BasePrompt.test.ts create mode 100644 test/prompts/promptRegistry.test.ts diff --git a/test/prompts/BasePrompt.test.ts b/test/prompts/BasePrompt.test.ts new file mode 100644 index 0000000..4d3416d --- /dev/null +++ b/test/prompts/BasePrompt.test.ts @@ -0,0 +1,288 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, test, expect, beforeEach } from 'vitest'; +import { BasePrompt } from '../../src/prompts/BasePrompt.js'; +import type { + PromptArgument, + PromptMessage +} from '@modelcontextprotocol/sdk/types.js'; + +// Create a concrete test implementation of BasePrompt +class TestPrompt extends BasePrompt { + readonly name = 'test-prompt'; + readonly description = 'A test prompt for unit testing'; + readonly arguments: PromptArgument[] = [ + { + name: 'required_arg', + description: 'A required argument', + required: true + }, + { + name: 'optional_arg', + description: 'An optional argument', + required: false + } + ]; + + getMessages(args: Record): PromptMessage[] { + this.validateArguments(args); + + return [ + { + role: 'user', + content: { + type: 'text', + text: `Test message with required: ${args.required_arg}, optional: ${args.optional_arg || 'not provided'}` + } + } + ]; + } +} + +describe('BasePrompt', () => { + let prompt: TestPrompt; + + beforeEach(() => { + prompt = new TestPrompt(); + }); + + describe('getMetadata', () => { + test('returns correct metadata structure', () => { + const metadata = prompt.getMetadata(); + + expect(metadata.name).toBe('test-prompt'); + expect(metadata.description).toBe('A test prompt for unit testing'); + expect(metadata.arguments).toBeDefined(); + expect(Array.isArray(metadata.arguments)).toBe(true); + expect(metadata.arguments?.length).toBe(2); + }); + + test('metadata includes all argument definitions', () => { + const metadata = prompt.getMetadata(); + + const argNames = metadata.arguments?.map((a) => a.name) || []; + expect(argNames).toContain('required_arg'); + expect(argNames).toContain('optional_arg'); + }); + + test('metadata preserves argument required flags', () => { + const metadata = prompt.getMetadata(); + + const requiredArg = metadata.arguments?.find( + (a) => a.name === 'required_arg' + ); + expect(requiredArg?.required).toBe(true); + + const optionalArg = metadata.arguments?.find( + (a) => a.name === 'optional_arg' + ); + expect(optionalArg?.required).toBe(false); + }); + }); + + describe('validateArguments', () => { + test('accepts valid arguments with all fields', () => { + const args = { + required_arg: 'value1', + optional_arg: 'value2' + }; + + expect(() => prompt.validateArguments(args)).not.toThrow(); + }); + + test('accepts valid arguments with only required fields', () => { + const args = { + required_arg: 'value1' + }; + + expect(() => prompt.validateArguments(args)).not.toThrow(); + }); + + test('throws error when required argument is missing', () => { + const args = { + optional_arg: 'value2' + }; + + expect(() => prompt.validateArguments(args)).toThrow( + 'Missing required argument: required_arg' + ); + }); + + test('throws error when required argument is empty string', () => { + const args = { + required_arg: '', + optional_arg: 'value2' + }; + + expect(() => prompt.validateArguments(args)).toThrow( + 'Missing required argument: required_arg' + ); + }); + + test('accepts whitespace-only arguments (no trimming performed)', () => { + const args = { + required_arg: ' ', + optional_arg: 'value2' + }; + + // Note: BasePrompt does not trim whitespace, so this is considered valid + expect(() => prompt.validateArguments(args)).not.toThrow(); + }); + + test('accepts empty string for optional arguments', () => { + const args = { + required_arg: 'value1', + optional_arg: '' + }; + + expect(() => prompt.validateArguments(args)).not.toThrow(); + }); + + test('accepts missing optional arguments', () => { + const args = { + required_arg: 'value1' + }; + + expect(() => prompt.validateArguments(args)).not.toThrow(); + }); + + test('ignores extra arguments not in definition', () => { + const args = { + required_arg: 'value1', + optional_arg: 'value2', + extra_arg: 'should be ignored' + }; + + expect(() => prompt.validateArguments(args)).not.toThrow(); + }); + }); + + describe('getMessages', () => { + test('returns messages with correct structure', () => { + const args = { + required_arg: 'test_value' + }; + + const messages = prompt.getMessages(args); + + expect(Array.isArray(messages)).toBe(true); + expect(messages.length).toBeGreaterThan(0); + + messages.forEach((message) => { + expect(message.role).toBeDefined(); + expect(message.content).toBeDefined(); + expect(message.content.type).toBe('text'); + expect(typeof message.content.text).toBe('string'); + }); + }); + + test('throws when required arguments are missing', () => { + const args = { + optional_arg: 'value' + }; + + expect(() => prompt.getMessages(args)).toThrow( + 'Missing required argument: required_arg' + ); + }); + + test('includes argument values in generated messages', () => { + const args = { + required_arg: 'test_required', + optional_arg: 'test_optional' + }; + + const messages = prompt.getMessages(args); + const messageText = messages[0].content.text; + + expect(messageText).toContain('test_required'); + expect(messageText).toContain('test_optional'); + }); + + test('handles missing optional arguments gracefully', () => { + const args = { + required_arg: 'test_required' + }; + + const messages = prompt.getMessages(args); + const messageText = messages[0].content.text; + + expect(messageText).toContain('test_required'); + expect(messageText).toContain('not provided'); + }); + }); + + describe('edge cases', () => { + test('validates empty args object when no arguments are required', () => { + class NoArgsPrompt extends BasePrompt { + readonly name = 'no-args'; + readonly description = 'No arguments needed'; + readonly arguments: PromptArgument[] = []; + + getMessages(_args: Record): PromptMessage[] { + return [ + { + role: 'user', + content: { + type: 'text', + text: 'No arguments needed' + } + } + ]; + } + } + + const noArgsPrompt = new NoArgsPrompt(); + expect(() => noArgsPrompt.validateArguments({})).not.toThrow(); + expect(() => noArgsPrompt.getMessages({})).not.toThrow(); + }); + + test('validates all required arguments', () => { + class MultiRequiredPrompt extends BasePrompt { + readonly name = 'multi-required'; + readonly description = 'Multiple required arguments'; + readonly arguments: PromptArgument[] = [ + { name: 'arg1', description: 'First', required: true }, + { name: 'arg2', description: 'Second', required: true }, + { name: 'arg3', description: 'Third', required: true } + ]; + + getMessages(_args: Record): PromptMessage[] { + return [ + { + role: 'user', + content: { type: 'text', text: 'Test' } + } + ]; + } + } + + const multiPrompt = new MultiRequiredPrompt(); + + // Missing arg1 + expect(() => + multiPrompt.validateArguments({ arg2: 'val2', arg3: 'val3' }) + ).toThrow('Missing required argument: arg1'); + + // Missing arg2 + expect(() => + multiPrompt.validateArguments({ arg1: 'val1', arg3: 'val3' }) + ).toThrow('Missing required argument: arg2'); + + // Missing arg3 + expect(() => + multiPrompt.validateArguments({ arg1: 'val1', arg2: 'val2' }) + ).toThrow('Missing required argument: arg3'); + + // All present - should not throw + expect(() => + multiPrompt.validateArguments({ + arg1: 'val1', + arg2: 'val2', + arg3: 'val3' + }) + ).not.toThrow(); + }); + }); +}); diff --git a/test/prompts/promptRegistry.test.ts b/test/prompts/promptRegistry.test.ts new file mode 100644 index 0000000..b119588 --- /dev/null +++ b/test/prompts/promptRegistry.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, test, expect } from 'vitest'; +import { + getAllPrompts, + getPromptByName +} from '../../src/prompts/promptRegistry.js'; + +describe('Prompt Registry', () => { + describe('getAllPrompts', () => { + test('returns all registered prompts', () => { + const prompts = getAllPrompts(); + + // Should have at least the 3 initial prompts + expect(prompts.length).toBeGreaterThanOrEqual(3); + + // Verify expected prompts are present + const promptNames = prompts.map((p) => p.name); + expect(promptNames).toContain('find-places-nearby'); + expect(promptNames).toContain('get-directions'); + expect(promptNames).toContain('show-reachable-areas'); + }); + + test('all prompts have unique names', () => { + const prompts = getAllPrompts(); + const names = prompts.map((p) => p.name); + const uniqueNames = new Set(names); + + expect(uniqueNames.size).toBe(names.length); + }); + + test('all prompts have valid metadata', () => { + const prompts = getAllPrompts(); + + prompts.forEach((prompt) => { + const metadata = prompt.getMetadata(); + + // Required fields + expect(metadata.name).toBeTruthy(); + expect(typeof metadata.name).toBe('string'); + expect(metadata.description).toBeTruthy(); + expect(typeof metadata.description).toBe('string'); + + // Arguments should be an array + expect(Array.isArray(metadata.arguments)).toBe(true); + + // Each argument should have required fields + metadata.arguments?.forEach((arg) => { + expect(arg.name).toBeTruthy(); + expect(typeof arg.name).toBe('string'); + expect(arg.description).toBeTruthy(); + expect(typeof arg.description).toBe('string'); + expect(typeof arg.required).toBe('boolean'); + }); + }); + }); + + test('all prompts follow kebab-case naming convention', () => { + const prompts = getAllPrompts(); + + prompts.forEach((prompt) => { + const name = prompt.name; + // Should be lowercase with hyphens only + expect(name).toMatch(/^[a-z]+(-[a-z]+)*$/); + }); + }); + }); + + describe('getPromptByName', () => { + test('returns prompt for valid name', () => { + const prompt = getPromptByName('find-places-nearby'); + + expect(prompt).toBeDefined(); + expect(prompt?.name).toBe('find-places-nearby'); + }); + + test('returns undefined for invalid name', () => { + const prompt = getPromptByName('nonexistent-prompt'); + + expect(prompt).toBeUndefined(); + }); + + test('returns correct prompt instances', () => { + const promptNames = [ + 'find-places-nearby', + 'get-directions', + 'show-reachable-areas' + ]; + + promptNames.forEach((name) => { + const prompt = getPromptByName(name); + expect(prompt).toBeDefined(); + expect(prompt?.name).toBe(name); + }); + }); + }); + + describe('Prompt metadata structure', () => { + test('find-places-nearby has correct metadata', () => { + const prompt = getPromptByName('find-places-nearby'); + expect(prompt).toBeDefined(); + + const metadata = prompt!.getMetadata(); + + expect(metadata.name).toBe('find-places-nearby'); + expect(metadata.description).toContain('places near a location'); + + // Should have location, category, radius arguments + const argNames = metadata.arguments?.map((a) => a.name) || []; + expect(argNames).toContain('location'); + expect(argNames).toContain('category'); + expect(argNames).toContain('radius'); + + // location should be required + const locationArg = metadata.arguments?.find( + (a) => a.name === 'location' + ); + expect(locationArg?.required).toBe(true); + + // category and radius should be optional + const categoryArg = metadata.arguments?.find( + (a) => a.name === 'category' + ); + expect(categoryArg?.required).toBe(false); + + const radiusArg = metadata.arguments?.find((a) => a.name === 'radius'); + expect(radiusArg?.required).toBe(false); + }); + + test('get-directions has correct metadata', () => { + const prompt = getPromptByName('get-directions'); + expect(prompt).toBeDefined(); + + const metadata = prompt!.getMetadata(); + + expect(metadata.name).toBe('get-directions'); + expect(metadata.description).toContain('directions'); + + // Should have from, to, mode arguments + const argNames = metadata.arguments?.map((a) => a.name) || []; + expect(argNames).toContain('from'); + expect(argNames).toContain('to'); + expect(argNames).toContain('mode'); + + // from and to should be required + const fromArg = metadata.arguments?.find((a) => a.name === 'from'); + expect(fromArg?.required).toBe(true); + + const toArg = metadata.arguments?.find((a) => a.name === 'to'); + expect(toArg?.required).toBe(true); + + // mode should be optional + const modeArg = metadata.arguments?.find((a) => a.name === 'mode'); + expect(modeArg?.required).toBe(false); + }); + + test('show-reachable-areas has correct metadata', () => { + const prompt = getPromptByName('show-reachable-areas'); + expect(prompt).toBeDefined(); + + const metadata = prompt!.getMetadata(); + + expect(metadata.name).toBe('show-reachable-areas'); + expect(metadata.description).toContain('reached'); + + // Should have location, time_minutes, mode arguments + const argNames = metadata.arguments?.map((a) => a.name) || []; + expect(argNames).toContain('location'); + expect(argNames).toContain('time_minutes'); + expect(argNames).toContain('mode'); + + // location should be required + const locationArg = metadata.arguments?.find( + (a) => a.name === 'location' + ); + expect(locationArg?.required).toBe(true); + + // time_minutes and mode should be optional + const timeArg = metadata.arguments?.find( + (a) => a.name === 'time_minutes' + ); + expect(timeArg?.required).toBe(false); + + const modeArg = metadata.arguments?.find((a) => a.name === 'mode'); + expect(modeArg?.required).toBe(false); + }); + }); +});