Skip to content

Commit 640849b

Browse files
[prompts] Add MCP prompts support for workflow templates (#79)
* [prompts] Add MCP prompts support with 3 initial workflow templates 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 <[email protected]> * [tests] Add unit tests for prompts infrastructure 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 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent 6600df1 commit 640849b

File tree

8 files changed

+847
-1
lines changed

8 files changed

+847
-1
lines changed

src/index.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ import { SpanStatusCode } from '@opentelemetry/api';
1111

1212
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1313
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14+
import {
15+
ListPromptsRequestSchema,
16+
GetPromptRequestSchema
17+
} from '@modelcontextprotocol/sdk/types.js';
1418
import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js';
1519
import { getAllTools } from './tools/toolRegistry.js';
1620
import { getAllResources } from './resources/resourceRegistry.js';
21+
import { getAllPrompts, getPromptByName } from './prompts/promptRegistry.js';
1722
import { getVersionInfo } from './utils/versionUtils.js';
1823
import {
1924
initializeTracing,
@@ -66,7 +71,8 @@ const server = new McpServer(
6671
{
6772
capabilities: {
6873
tools: {},
69-
resources: {}
74+
resources: {},
75+
prompts: {}
7076
}
7177
}
7278
);
@@ -81,6 +87,37 @@ allResources.forEach((resource) => {
8187
resource.installTo(server);
8288
});
8389

90+
// Register prompt handlers
91+
server.server.setRequestHandler(ListPromptsRequestSchema, async () => {
92+
const allPrompts = getAllPrompts();
93+
return {
94+
prompts: allPrompts.map((prompt) => prompt.getMetadata())
95+
};
96+
});
97+
98+
server.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
99+
const { name, arguments: args } = request.params;
100+
101+
const prompt = getPromptByName(name);
102+
if (!prompt) {
103+
throw new Error(`Prompt not found: ${name}`);
104+
}
105+
106+
// Convert args to object for easier access
107+
const argsObj: Record<string, string> = {};
108+
if (args && typeof args === 'object') {
109+
Object.assign(argsObj, args);
110+
}
111+
112+
// Get the prompt messages with filled-in arguments
113+
const messages = prompt.getMessages(argsObj);
114+
115+
return {
116+
description: prompt.description,
117+
messages
118+
};
119+
});
120+
84121
async function main() {
85122
// Initialize OpenTelemetry tracing if not in test mode
86123
let tracingInitialized = false;

src/prompts/BasePrompt.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
type Prompt,
6+
type PromptArgument,
7+
type PromptMessage
8+
} from '@modelcontextprotocol/sdk/types.js';
9+
10+
/**
11+
* Base class for all MCP prompts.
12+
*
13+
* Prompts are pre-built, parameterized workflows that guide multi-step geospatial tasks.
14+
* They capture domain expertise and best practices for common use cases.
15+
*/
16+
export abstract class BasePrompt {
17+
/**
18+
* Unique identifier for this prompt (e.g., "find-places-nearby")
19+
*/
20+
abstract readonly name: string;
21+
22+
/**
23+
* Human-readable description of what this prompt does
24+
*/
25+
abstract readonly description: string;
26+
27+
/**
28+
* Arguments this prompt accepts
29+
*/
30+
abstract readonly arguments: PromptArgument[];
31+
32+
/**
33+
* Get the prompt metadata for listing
34+
*/
35+
getMetadata(): Prompt {
36+
return {
37+
name: this.name,
38+
description: this.description,
39+
arguments: this.arguments
40+
};
41+
}
42+
43+
/**
44+
* Generate the prompt messages with filled-in arguments
45+
*
46+
* @param args - The argument values provided by the user/agent
47+
* @returns Array of messages to send to the LLM
48+
*/
49+
abstract getMessages(args: Record<string, string>): PromptMessage[];
50+
51+
/**
52+
* Validate that all required arguments are provided
53+
*
54+
* @param args - The argument values to validate
55+
* @throws Error if required arguments are missing
56+
*/
57+
protected validateArguments(args: Record<string, string>): void {
58+
for (const arg of this.arguments) {
59+
if (arg.required && !args[arg.name]) {
60+
throw new Error(`Missing required argument: ${arg.name}`);
61+
}
62+
}
63+
}
64+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { BasePrompt } from './BasePrompt.js';
5+
import type {
6+
PromptArgument,
7+
PromptMessage
8+
} from '@modelcontextprotocol/sdk/types.js';
9+
10+
/**
11+
* Prompt for finding places near a location with optional map visualization.
12+
*
13+
* This prompt guides the agent through:
14+
* 1. Geocoding the location (if needed)
15+
* 2. Searching for places by category
16+
* 3. Formatting results with map visualization
17+
*
18+
* Example queries:
19+
* - "Find coffee shops near downtown Seattle"
20+
* - "Show me restaurants near 123 Main St"
21+
* - "What museums are near the Eiffel Tower?"
22+
*/
23+
export class FindPlacesNearbyPrompt extends BasePrompt {
24+
readonly name = 'find-places-nearby';
25+
readonly description =
26+
'Helps you search for specific types of places near a location with optional map visualization';
27+
28+
readonly arguments: PromptArgument[] = [
29+
{
30+
name: 'location',
31+
description:
32+
'The location to search near (address, place name, or coordinates)',
33+
required: true
34+
},
35+
{
36+
name: 'category',
37+
description:
38+
'Type of place to search for (e.g., "coffee shops", "restaurants", "museums")',
39+
required: false
40+
},
41+
{
42+
name: 'radius',
43+
description: 'Search radius in meters (default: 1000)',
44+
required: false
45+
}
46+
];
47+
48+
getMessages(args: Record<string, string>): PromptMessage[] {
49+
this.validateArguments(args);
50+
51+
const { location, category, radius } = args;
52+
const radiusText = radius ? ` within ${radius} meters` : '';
53+
const categoryText = category || 'places';
54+
55+
return [
56+
{
57+
role: 'user',
58+
content: {
59+
type: 'text',
60+
text: `Find ${categoryText} near ${location}${radiusText}.
61+
62+
Please follow these steps:
63+
1. If the location is not in coordinate format, geocode it first using search_and_geocode_tool
64+
2. Use category_search_tool or search_tool to find ${categoryText} near the location
65+
3. Display the results on a map showing the location and the found places
66+
4. Provide a summary of the top results with key details (name, address, distance)
67+
68+
Make the output clear and actionable.`
69+
}
70+
}
71+
];
72+
}
73+
}

src/prompts/GetDirectionsPrompt.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { BasePrompt } from './BasePrompt.js';
5+
import type {
6+
PromptArgument,
7+
PromptMessage
8+
} from '@modelcontextprotocol/sdk/types.js';
9+
10+
/**
11+
* Prompt for getting turn-by-turn directions between two locations.
12+
*
13+
* This prompt guides the agent through:
14+
* 1. Geocoding start and end locations (if needed)
15+
* 2. Getting directions via the appropriate routing profile
16+
* 3. Visualizing the route on a map
17+
* 4. Providing clear turn-by-turn instructions
18+
*
19+
* Example queries:
20+
* - "Get directions from my office to the airport"
21+
* - "How do I drive from Seattle to Portland?"
22+
* - "Walking directions from here to the museum"
23+
*/
24+
export class GetDirectionsPrompt extends BasePrompt {
25+
readonly name = 'get-directions';
26+
readonly description =
27+
'Provides turn-by-turn directions between two locations with options for different travel modes';
28+
29+
readonly arguments: PromptArgument[] = [
30+
{
31+
name: 'from',
32+
description: 'Starting location (address, place name, or coordinates)',
33+
required: true
34+
},
35+
{
36+
name: 'to',
37+
description: 'Destination location (address, place name, or coordinates)',
38+
required: true
39+
},
40+
{
41+
name: 'mode',
42+
description:
43+
'Travel mode: driving, walking, or cycling (default: driving)',
44+
required: false
45+
}
46+
];
47+
48+
getMessages(args: Record<string, string>): PromptMessage[] {
49+
this.validateArguments(args);
50+
51+
const { from, to, mode = 'driving' } = args;
52+
53+
return [
54+
{
55+
role: 'user',
56+
content: {
57+
type: 'text',
58+
text: `Get ${mode} directions from ${from} to ${to}.
59+
60+
Please follow these steps:
61+
1. Geocode both the starting point and destination if they're not in coordinate format
62+
2. Use directions_tool to get the route with profile set to ${mode}
63+
3. Display the route on a map with clear start and end markers
64+
4. Provide:
65+
- Total distance and estimated travel time
66+
- Turn-by-turn directions (summarized if very long)
67+
- Any notable features along the route (tolls, ferries, etc.)
68+
69+
Format the output to be clear and easy to follow.`
70+
}
71+
}
72+
];
73+
}
74+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { BasePrompt } from './BasePrompt.js';
5+
import type {
6+
PromptArgument,
7+
PromptMessage
8+
} from '@modelcontextprotocol/sdk/types.js';
9+
10+
/**
11+
* Prompt for visualizing areas reachable within a specified time from a location.
12+
*
13+
* This prompt guides the agent through:
14+
* 1. Geocoding the starting location (if needed)
15+
* 2. Calculating isochrones for the specified travel time
16+
* 3. Visualizing the reachable areas on a map
17+
* 4. Providing context about what the isochrone represents
18+
*
19+
* Example queries:
20+
* - "Show me areas I can reach in 15 minutes from downtown"
21+
* - "What's the 30-minute driving range from our warehouse?"
22+
* - "Display my 10-minute walk radius from home"
23+
*/
24+
export class ShowReachableAreasPrompt extends BasePrompt {
25+
readonly name = 'show-reachable-areas';
26+
readonly description =
27+
'Visualizes areas that can be reached from a location within a specified time using isochrones';
28+
29+
readonly arguments: PromptArgument[] = [
30+
{
31+
name: 'location',
32+
description: 'Starting location (address, place name, or coordinates)',
33+
required: true
34+
},
35+
{
36+
name: 'time_minutes',
37+
description: 'Travel time in minutes (default: 15)',
38+
required: false
39+
},
40+
{
41+
name: 'mode',
42+
description:
43+
'Travel mode: driving, walking, or cycling (default: driving)',
44+
required: false
45+
}
46+
];
47+
48+
getMessages(args: Record<string, string>): PromptMessage[] {
49+
this.validateArguments(args);
50+
51+
const { location, time_minutes = '15', mode = 'driving' } = args;
52+
53+
return [
54+
{
55+
role: 'user',
56+
content: {
57+
type: 'text',
58+
text: `Show areas reachable within ${time_minutes} minutes of ${mode} from ${location}.
59+
60+
Please follow these steps:
61+
1. Geocode the location if it's not in coordinate format
62+
2. Use isochrone_tool to calculate the ${time_minutes}-minute ${mode} isochrone
63+
3. Visualize the reachable area on a map with:
64+
- The starting location clearly marked
65+
- The isochrone polygon showing the reachable area
66+
- Appropriate styling to make it easy to understand
67+
4. Provide context explaining:
68+
- What area is covered (approximate square miles/km)
69+
- What this means practically (e.g., "You can reach X locations within ${time_minutes} minutes")
70+
- Any limitations or caveats (traffic conditions, time of day, etc.)
71+
72+
Make the visualization clear and the explanation actionable.`
73+
}
74+
}
75+
];
76+
}
77+
}

src/prompts/promptRegistry.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { FindPlacesNearbyPrompt } from './FindPlacesNearbyPrompt.js';
5+
import { GetDirectionsPrompt } from './GetDirectionsPrompt.js';
6+
import { ShowReachableAreasPrompt } from './ShowReachableAreasPrompt.js';
7+
8+
/**
9+
* Central registry of all available prompts.
10+
*
11+
* This module maintains a readonly collection of prompt instances and provides
12+
* type-safe access methods.
13+
*/
14+
15+
// Instantiate all prompts
16+
const ALL_PROMPTS = [
17+
new FindPlacesNearbyPrompt(),
18+
new GetDirectionsPrompt(),
19+
new ShowReachableAreasPrompt()
20+
] as const;
21+
22+
/**
23+
* Type representing any prompt instance
24+
*/
25+
export type PromptInstance = (typeof ALL_PROMPTS)[number];
26+
27+
/**
28+
* Get all registered prompts
29+
*
30+
* @returns Readonly array of all prompt instances
31+
*/
32+
export function getAllPrompts(): readonly PromptInstance[] {
33+
return ALL_PROMPTS;
34+
}
35+
36+
/**
37+
* Get a prompt by name
38+
*
39+
* @param name - The prompt name to look up
40+
* @returns The prompt instance, or undefined if not found
41+
*/
42+
export function getPromptByName(name: string): PromptInstance | undefined {
43+
return ALL_PROMPTS.find((prompt) => prompt.name === name);
44+
}

0 commit comments

Comments
 (0)