Skip to content
Closed
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
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ server.server.setRequestHandler(ListPromptsRequestSchema, async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(server.server as any).setRequestHandler(
GetPromptRequestSchema,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async (request: any) => {
const { name, arguments: args } = request.params;

Expand Down
92 changes: 92 additions & 0 deletions src/prompts/CleanGpsTracePrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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 cleaning and snapping GPS traces to road networks.
*
* This prompt guides the agent through:
* 1. Processing raw GPS coordinates
* 2. Snapping them to the road network using Map Matching
* 3. Returning a clean, accurate route
*
* Example queries:
* - "Clean up this noisy GPS trace"
* - "Snap these GPS points to roads"
* - "Match my recorded GPS track to the road network"
*/
export class CleanGpsTracePrompt extends BasePrompt {
readonly name = 'clean-gps-trace';
readonly description =
'Cleans noisy GPS traces by snapping them to the road network and returning an accurate route';

readonly arguments: PromptArgument[] = [
{
name: 'coordinates',
description:
'GPS trace as coordinates in format "lng1,lat1;lng2,lat2;..." or as an array',
required: true
},
{
name: 'mode',
description:
'Travel mode: driving, walking, or cycling (default: driving)',
required: false
},
{
name: 'timestamps',
description:
'Optional Unix timestamps for each coordinate (comma-separated)',
required: false
}
];

getMessages(args: Record<string, string>): PromptMessage[] {
this.validateArguments(args);

const { coordinates, mode = 'driving', timestamps } = args;

const timestampNote = timestamps
? '\n - Include the provided timestamps to improve matching accuracy'
: '';

return [
{
role: 'user',
content: {
type: 'text',
text: `Clean this GPS trace and snap it to the road network: ${coordinates}

Please follow these steps:
1. Parse the GPS coordinates (convert to proper coordinate format if needed)
2. Use map_matching_tool to snap the trace to roads:
- Pass the coordinates array in order they were recorded
- Set profile to ${mode}${timestampNote}
- Request annotations for distance, duration, and speed if available
3. Display:
- Map visualization showing:
* Original GPS points (in one color)
* Matched route on roads (in another color)
- Statistics:
* Total matched distance
* Total duration
* Confidence score (if available)
* Number of points matched vs original
- Any anomalies or gaps in the trace

The map_matching_tool will:
- Snap noisy GPS coordinates to the nearest roads
- Fill in gaps where GPS signal was lost
- Return a clean route that follows the actual road network

Format the output clearly showing before/after comparison and route quality metrics.`
}
}
];
}
}
88 changes: 88 additions & 0 deletions src/prompts/OptimizeDeliveriesPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 optimizing delivery routes using the Optimization API v1.
*
* This prompt guides the agent through:
* 1. Geocoding delivery locations (if needed)
* 2. Using optimization_tool to find optimal route (2-12 locations)
* 3. Presenting the optimized waypoint sequence with trip statistics
*
* Example queries:
* - "Optimize my delivery route for these addresses"
* - "Find the best order to visit these locations"
* - "Plan the most efficient route for my deliveries today"
*
* Note: Limited to 12 coordinates per request (Optimization API v1 constraint)
*/
export class OptimizeDeliveriesPrompt extends BasePrompt {
readonly name = 'optimize-deliveries';
readonly description =
'Optimizes delivery routes to minimize travel time and find the best order to visit multiple locations';

readonly arguments: PromptArgument[] = [
{
name: 'locations',
description:
'Comma-separated list of addresses or coordinates to optimize',
required: true
},
{
name: 'mode',
description:
'Travel mode: driving, walking, or cycling (default: driving)',
required: false
},
{
name: 'start',
description: 'Starting location (defaults to first location in list)',
required: false
}
];

getMessages(args: Record<string, string>): PromptMessage[] {
this.validateArguments(args);

const { locations, mode = 'driving', start } = args;

return [
{
role: 'user',
content: {
type: 'text',
text: `Optimize a ${mode} route for these locations: ${locations}${start ? ` starting from ${start}` : ''}.

Please follow these steps:
1. Geocode all locations to get coordinates (if they're addresses)
- IMPORTANT: The Optimization API v1 supports 2-12 coordinates maximum
- If you have more than 12 locations, inform the user and ask which locations to prioritize

2. Use optimization_tool to find the optimal route:
- Pass coordinates array (2-12 coordinates)
- Set profile to mapbox/${mode}
- Optionally set geometries to "geojson" for map visualization
- Consider using roundtrip:false for one-way trips
- The tool returns results immediately (synchronous)

3. Display the optimized route:
- Show the waypoints in optimal visiting order (use waypoint_index to show original positions)
- Total distance (from trips[0].distance) and duration (from trips[0].duration)
- Map visualization if geometry was requested
- Individual leg details if relevant

Format the output to be clear with:
- Numbered list of stops in optimal order (e.g., "1. Stop 3 (original position 2) → 2. Stop 1 (original position 0)")
- Total trip statistics (distance in km, duration in minutes)
- Map showing the complete route if geometry is available`
}
}
];
}
}
6 changes: 5 additions & 1 deletion src/prompts/promptRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { FindPlacesNearbyPrompt } from './FindPlacesNearbyPrompt.js';
import { GetDirectionsPrompt } from './GetDirectionsPrompt.js';
import { ShowReachableAreasPrompt } from './ShowReachableAreasPrompt.js';
import { OptimizeDeliveriesPrompt } from './OptimizeDeliveriesPrompt.js';
import { CleanGpsTracePrompt } from './CleanGpsTracePrompt.js';

/**
* Central registry of all available prompts.
Expand All @@ -16,7 +18,9 @@ import { ShowReachableAreasPrompt } from './ShowReachableAreasPrompt.js';
const ALL_PROMPTS = [
new FindPlacesNearbyPrompt(),
new GetDirectionsPrompt(),
new ShowReachableAreasPrompt()
new ShowReachableAreasPrompt(),
new OptimizeDeliveriesPrompt(),
new CleanGpsTracePrompt()
] as const;

/**
Expand Down
8 changes: 7 additions & 1 deletion src/tools/BaseTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ export abstract class BaseTool<
(this.outputSchema as unknown as z.ZodObject<any>).shape;
}

return server.registerTool(
// Type assertion to avoid "Type instantiation is excessively deep" error
// This is a known issue in MCP SDK 1.25.1: https://github.com/modelcontextprotocol/typescript-sdk/issues/985
// TODO: Remove this workaround when SDK fixes their type definitions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const serverAny = server as any;

return serverAny.registerTool(
this.name,
config,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
74 changes: 74 additions & 0 deletions src/tools/map-matching-tool/MapMatchingTool.input.schema.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 { z } from 'zod';
import { coordinateSchema } from '../../schemas/shared.js';

export const MapMatchingInputSchema = z.object({
coordinates: z
.array(coordinateSchema)
.min(2, 'At least two coordinate pairs are required.')
.max(100, 'Up to 100 coordinate pairs are supported.')
.describe(
'Array of coordinate objects with longitude and latitude properties representing a GPS trace. ' +
'Must include at least 2 and up to 100 coordinate pairs. ' +
'Coordinates should be in the order they were recorded.'
),
profile: z
.enum(['driving', 'driving-traffic', 'walking', 'cycling'])
.default('driving')
.describe(
'Routing profile for different modes of transport. Options: \n' +
'- driving: automotive based on road network\n' +
'- driving-traffic: automotive with current traffic conditions\n' +
'- walking: pedestrian/hiking\n' +
'- cycling: bicycle'
),
timestamps: z
.array(z.number().int().positive())
.optional()
.describe(
'Array of Unix timestamps (in seconds) corresponding to each coordinate. ' +
'If provided, must have the same length as coordinates array. ' +
'Used to improve matching accuracy based on speed.'
),
radiuses: z
.array(z.number().min(0))
.optional()
.describe(
'Array of maximum distances (in meters) each coordinate can snap to the road network. ' +
'If provided, must have the same length as coordinates array. ' +
'Default is unlimited. Use smaller values (5-25m) for high-quality GPS, ' +
'larger values (50-100m) for noisy GPS traces.'
),
annotations: z
.array(z.enum(['speed', 'distance', 'duration', 'congestion']))
.optional()
.describe(
'Additional data to include in the response. Options: \n' +
'- speed: Speed limit per segment (km/h)\n' +
'- distance: Distance per segment (meters)\n' +
'- duration: Duration per segment (seconds)\n' +
'- congestion: Traffic level per segment (low, moderate, heavy, severe)'
),
overview: z
.enum(['full', 'simplified', 'false'])
.default('full')
.describe(
'Format of the returned geometry. Options: \n' +
'- full: Returns full geometry with all points\n' +
'- simplified: Returns simplified geometry\n' +
'- false: No geometry returned'
),
geometries: z
.enum(['geojson', 'polyline', 'polyline6'])
.default('geojson')
.describe(
'Format of the returned geometry. Options: \n' +
'- geojson: GeoJSON LineString (recommended)\n' +
'- polyline: Polyline with precision 5\n' +
'- polyline6: Polyline with precision 6'
)
});

export type MapMatchingInput = z.infer<typeof MapMatchingInputSchema>;
56 changes: 56 additions & 0 deletions src/tools/map-matching-tool/MapMatchingTool.output.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Mapbox, Inc.
// Licensed under the MIT License.

import { z } from 'zod';

// GeoJSON LineString schema
const GeoJSONLineStringSchema = z.object({
type: z.literal('LineString'),
coordinates: z.array(
z
.tuple([z.number(), z.number()])
.or(z.tuple([z.number(), z.number(), z.number()]))
)
});

// Tracepoint schema - represents a snapped coordinate
const TracepointSchema = z.object({
name: z.string().optional(),
location: z.tuple([z.number(), z.number()]),
waypoint_index: z.number().optional(),
matchings_index: z.number(),
alternatives_count: z.number()
});

// Matching schema - represents a matched route
const MatchingSchema = z.object({
confidence: z.number().min(0).max(1),
distance: z.number(),
duration: z.number(),
geometry: z.union([GeoJSONLineStringSchema, z.string()]),
legs: z
.array(
z.object({
distance: z.number(),
duration: z.number(),
annotation: z
.object({
speed: z.array(z.number()).optional(),
distance: z.array(z.number()).optional(),
duration: z.array(z.number()).optional(),
congestion: z.array(z.string()).optional()
})
.optional()
})
)
.optional()
});

// Main output schema
export const MapMatchingOutputSchema = z.object({
code: z.string(),
matchings: z.array(MatchingSchema),
tracepoints: z.array(TracepointSchema.nullable())
});

export type MapMatchingOutput = z.infer<typeof MapMatchingOutputSchema>;
Loading