Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -66,7 +71,8 @@ const server = new McpServer(
{
capabilities: {
tools: {},
resources: {}
resources: {},
prompts: {}
}
}
);
Expand All @@ -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<string, string> = {};
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;
Expand Down
64 changes: 64 additions & 0 deletions src/prompts/BasePrompt.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>): 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<string, string>): void {
for (const arg of this.arguments) {
if (arg.required && !args[arg.name]) {
throw new Error(`Missing required argument: ${arg.name}`);
}
}
}
}
73 changes: 73 additions & 0 deletions src/prompts/FindPlacesNearbyPrompt.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>): 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.`
}
}
];
}
}
74 changes: 74 additions & 0 deletions src/prompts/GetDirectionsPrompt.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>): 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.`
}
}
];
}
}
77 changes: 77 additions & 0 deletions src/prompts/ShowReachableAreasPrompt.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>): 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.`
}
}
];
}
}
44 changes: 44 additions & 0 deletions src/prompts/promptRegistry.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading