Skip to content

Commit 036bd3e

Browse files
Add Map Matching and Optimization tools (V1 API) (#97)
* Add Map Matching and Optimization tools Implements two new Mapbox Navigation API tools: - Map Matching Tool (map_matching_tool): Snaps GPS traces to roads - Supports 2-100 coordinates with optional timestamps and radiuses - Returns confidence scores, matched geometry, and annotations - Handles driving, cycling, walking, and driving-traffic profiles - Optimization Tool (optimization_tool): Solves vehicle routing problems - Supports up to 1000 coordinates - Simplified mode: auto-generates vehicle and services - Advanced mode: custom vehicles, services, shipments, time windows - Async polling mechanism (POST + GET) Both tools include: - Complete input/output schemas with Zod validation - Comprehensive unit tests (19 tests total) - Proper annotations (readOnlyHint, openWorldHint, etc.) All 422 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Update @modelcontextprotocol/sdk to 1.25.2 (CVE fix) Update SDK from 1.25.1 to 1.25.2 which includes security fixes. Update patch file to match new SDK version. All 457 tests passing with updated SDK. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Refactor optimization tool to use V1 API with V2 ready for future - Switch optimization_tool to use V1 synchronous GET API (publicly available) - V1 supports 2-12 coordinates, simple TSP, works immediately - Preserve V2 implementation as OptimizationV2Tool (not registered) - V2 code ready to enable when API becomes public - Remove task-based infrastructure from main registration - All 456 tests passing V1 endpoint: GET /optimized-trips/v1/{profile}/{coordinates} V2 endpoint: POST /optimized-trips/v2 (requires beta access) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Document Map Matching and Optimization tools in README - Add Map Matching tool section with features and example usage - Add Optimization tool section with features and example usage - Note about V2 Optimization API implementation (beta access required) - Update intro to mention route optimization and map matching capabilities - Add new "GPS & Route Matching" example prompts section - Add optimization example to Analysis & Planning section 🤖 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 31fe224 commit 036bd3e

16 files changed

+2030
-5
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ The Mapbox MCP Server transforms any AI agent or application into a geospatially
1212
- **Points of interest (POI) search** across millions of businesses, landmarks, and places worldwide
1313
- **Multi-modal routing** for driving, walking, and cycling with real-time traffic
1414
- **Travel time matrices** to analyze accessibility and optimize logistics
15+
- **Route optimization** to find the optimal visiting order for multiple stops (traveling salesman problem)
16+
- **Map matching** to snap GPS traces to the road network for clean route visualization
1517
- **Isochrone generation** to visualize areas reachable within specific time or distance constraints
1618
- **Static map images** to create visual representations of locations, routes, and geographic data
1719
- **Offline geospatial calculations** for distance, area, bearing, buffers, and spatial analysis without requiring API calls
@@ -77,6 +79,13 @@ Try these prompts with Claude Desktop or other MCP clients after setup:
7779
- "Show me areas reachable within 30 minutes of downtown Portland by car"
7880
- "Calculate a travel time matrix between these 3 hotel locations (Marriott, Sheraton and Hilton) and the convention center in Denver"
7981
- "Find the optimal route visiting these 3 tourist attractions (Golden Gate, Musical Stairs and Fisherman's Wharf) in San Francisco"
82+
- "Optimize a delivery route for these 8 addresses: [list of addresses]"
83+
84+
### GPS & Route Matching
85+
86+
- "Clean up this GPS trace and show the actual route on roads: [list of coordinates with timestamps]"
87+
- "Snap this recorded bicycle ride to the cycling network: [GPS coordinates]"
88+
- "Match this driving route to the road network and show traffic congestion levels"
8089

8190
### Offline Geospatial Calculations
8291

@@ -386,6 +395,36 @@ Computes areas that are reachable within a specified amount of times from a loca
386395
Uses the [Mapbox Search Box Text Search API](https://docs.mapbox.com/api/search/search-box/#search-request) endpoint to power searching for and geocoding POIs, addresses, places, and any other types supported by that API.
387396
This tool consolidates the functionality that was previously provided by the ForwardGeocodeTool and PoiSearchTool (from earlier versions of this MCP server) into a single tool.
388397

398+
#### Map matching tool
399+
400+
Snaps GPS traces to the road network using the [Mapbox Map Matching API](https://docs.mapbox.com/api/navigation/map-matching/). Features include:
401+
402+
- Convert noisy GPS traces to clean routes on the road network
403+
- Support for different travel profiles (driving, driving-traffic, walking, cycling)
404+
- Handle up to 100 coordinate pairs per request
405+
- Optional timestamps for improved accuracy based on speed
406+
- Configurable snap radiuses for different GPS quality levels
407+
- Route annotations (speed limits, distance, duration, traffic congestion)
408+
- Multiple geometry output formats (GeoJSON, polyline)
409+
410+
**Example Usage**: "Clean up this GPS trace and snap it to roads: [coordinates with timestamps]"
411+
412+
#### Optimization tool
413+
414+
Finds the optimal route through multiple locations using the [Mapbox Optimization API](https://docs.mapbox.com/api/navigation/optimization/). Features include:
415+
416+
- Solve traveling salesman problem (TSP) for 2-12 locations
417+
- Support for different travel profiles (driving, driving-traffic, walking, cycling)
418+
- Flexible start and end point configuration
419+
- Roundtrip or one-way trip optimization
420+
- Turn-by-turn navigation instructions (optional)
421+
- Route annotations (distance, duration, speed)
422+
- Multiple geometry output formats (GeoJSON, polyline)
423+
424+
**Example Usage**: "Find the optimal route to visit these 5 stops: [list of addresses or coordinates]"
425+
426+
**Note**: A V2 API with advanced features (time windows, capacity constraints, multiple vehicles) is available but requires beta access. The V2 implementation is included in the codebase but not registered by default.
427+
389428
# Development
390429

391430
## Inspecting server

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
],
7777
"dependencies": {
7878
"@mcp-ui/server": "^5.13.1",
79-
"@modelcontextprotocol/sdk": "^1.25.1",
79+
"@modelcontextprotocol/sdk": "^1.25.2",
8080
"@opentelemetry/api": "^1.9.0",
8181
"@opentelemetry/auto-instrumentations-node": "^0.56.0",
8282
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
File renamed without changes.
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 { z } from 'zod';
5+
import { coordinateSchema } from '../../schemas/shared.js';
6+
7+
export const MapMatchingInputSchema = z.object({
8+
coordinates: z
9+
.array(coordinateSchema)
10+
.min(2, 'At least two coordinate pairs are required.')
11+
.max(100, 'Up to 100 coordinate pairs are supported.')
12+
.describe(
13+
'Array of coordinate objects with longitude and latitude properties representing a GPS trace. ' +
14+
'Must include at least 2 and up to 100 coordinate pairs. ' +
15+
'Coordinates should be in the order they were recorded.'
16+
),
17+
profile: z
18+
.enum(['driving', 'driving-traffic', 'walking', 'cycling'])
19+
.default('driving')
20+
.describe(
21+
'Routing profile for different modes of transport. Options: \n' +
22+
'- driving: automotive based on road network\n' +
23+
'- driving-traffic: automotive with current traffic conditions\n' +
24+
'- walking: pedestrian/hiking\n' +
25+
'- cycling: bicycle'
26+
),
27+
timestamps: z
28+
.array(z.number().int().positive())
29+
.optional()
30+
.describe(
31+
'Array of Unix timestamps (in seconds) corresponding to each coordinate. ' +
32+
'If provided, must have the same length as coordinates array. ' +
33+
'Used to improve matching accuracy based on speed.'
34+
),
35+
radiuses: z
36+
.array(z.number().min(0))
37+
.optional()
38+
.describe(
39+
'Array of maximum distances (in meters) each coordinate can snap to the road network. ' +
40+
'If provided, must have the same length as coordinates array. ' +
41+
'Default is unlimited. Use smaller values (5-25m) for high-quality GPS, ' +
42+
'larger values (50-100m) for noisy GPS traces.'
43+
),
44+
annotations: z
45+
.array(z.enum(['speed', 'distance', 'duration', 'congestion']))
46+
.optional()
47+
.describe(
48+
'Additional data to include in the response. Options: \n' +
49+
'- speed: Speed limit per segment (km/h)\n' +
50+
'- distance: Distance per segment (meters)\n' +
51+
'- duration: Duration per segment (seconds)\n' +
52+
'- congestion: Traffic level per segment (low, moderate, heavy, severe)'
53+
),
54+
overview: z
55+
.enum(['full', 'simplified', 'false'])
56+
.default('full')
57+
.describe(
58+
'Format of the returned geometry. Options: \n' +
59+
'- full: Returns full geometry with all points\n' +
60+
'- simplified: Returns simplified geometry\n' +
61+
'- false: No geometry returned'
62+
),
63+
geometries: z
64+
.enum(['geojson', 'polyline', 'polyline6'])
65+
.default('geojson')
66+
.describe(
67+
'Format of the returned geometry. Options: \n' +
68+
'- geojson: GeoJSON LineString (recommended)\n' +
69+
'- polyline: Polyline with precision 5\n' +
70+
'- polyline6: Polyline with precision 6'
71+
)
72+
});
73+
74+
export type MapMatchingInput = z.infer<typeof MapMatchingInputSchema>;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { z } from 'zod';
5+
6+
// GeoJSON LineString schema
7+
const GeoJSONLineStringSchema = z.object({
8+
type: z.literal('LineString'),
9+
coordinates: z.array(
10+
z
11+
.tuple([z.number(), z.number()])
12+
.or(z.tuple([z.number(), z.number(), z.number()]))
13+
)
14+
});
15+
16+
// Tracepoint schema - represents a snapped coordinate
17+
const TracepointSchema = z.object({
18+
name: z.string().optional(),
19+
location: z.tuple([z.number(), z.number()]),
20+
waypoint_index: z.number().optional(),
21+
matchings_index: z.number(),
22+
alternatives_count: z.number()
23+
});
24+
25+
// Matching schema - represents a matched route
26+
const MatchingSchema = z.object({
27+
confidence: z.number().min(0).max(1),
28+
distance: z.number(),
29+
duration: z.number(),
30+
geometry: z.union([GeoJSONLineStringSchema, z.string()]),
31+
legs: z
32+
.array(
33+
z.object({
34+
distance: z.number(),
35+
duration: z.number(),
36+
annotation: z
37+
.object({
38+
speed: z.array(z.number()).optional(),
39+
distance: z.array(z.number()).optional(),
40+
duration: z.array(z.number()).optional(),
41+
congestion: z.array(z.string()).optional()
42+
})
43+
.optional()
44+
})
45+
)
46+
.optional()
47+
});
48+
49+
// Main output schema
50+
export const MapMatchingOutputSchema = z.object({
51+
code: z.string(),
52+
matchings: z.array(MatchingSchema),
53+
tracepoints: z.array(TracepointSchema.nullable())
54+
});
55+
56+
export type MapMatchingOutput = z.infer<typeof MapMatchingOutputSchema>;
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { URLSearchParams } from 'node:url';
5+
import type { z } from 'zod';
6+
import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js';
7+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
8+
import { MapMatchingInputSchema } from './MapMatchingTool.input.schema.js';
9+
import {
10+
MapMatchingOutputSchema,
11+
type MapMatchingOutput
12+
} from './MapMatchingTool.output.schema.js';
13+
import type { HttpRequest } from '../../utils/types.js';
14+
15+
// Docs: https://docs.mapbox.com/api/navigation/map-matching/
16+
17+
export class MapMatchingTool extends MapboxApiBasedTool<
18+
typeof MapMatchingInputSchema,
19+
typeof MapMatchingOutputSchema
20+
> {
21+
name = 'map_matching_tool';
22+
description =
23+
'Snap GPS traces to roads using Mapbox Map Matching API. Takes noisy/inaccurate ' +
24+
'coordinate sequences (2-100 points) and returns clean routes aligned with actual ' +
25+
'roads, bike paths, or walkways. Useful for analyzing recorded trips, cleaning ' +
26+
'fleet tracking data, or processing fitness activity traces. Returns confidence ' +
27+
'scores, matched geometry, and optional traffic/speed annotations.';
28+
annotations = {
29+
title: 'Map Matching Tool',
30+
readOnlyHint: true,
31+
destructiveHint: false,
32+
idempotentHint: true,
33+
openWorldHint: true
34+
};
35+
36+
constructor(params: { httpRequest: HttpRequest }) {
37+
super({
38+
inputSchema: MapMatchingInputSchema,
39+
outputSchema: MapMatchingOutputSchema,
40+
httpRequest: params.httpRequest
41+
});
42+
}
43+
44+
protected async execute(
45+
input: z.infer<typeof MapMatchingInputSchema>,
46+
accessToken: string
47+
): Promise<CallToolResult> {
48+
// Validate timestamps array length matches coordinates
49+
if (
50+
input.timestamps &&
51+
input.timestamps.length !== input.coordinates.length
52+
) {
53+
return {
54+
content: [
55+
{
56+
type: 'text',
57+
text: 'The timestamps array must have the same length as the coordinates array'
58+
}
59+
],
60+
isError: true
61+
};
62+
}
63+
64+
// Validate radiuses array length matches coordinates
65+
if (input.radiuses && input.radiuses.length !== input.coordinates.length) {
66+
return {
67+
content: [
68+
{
69+
type: 'text',
70+
text: 'The radiuses array must have the same length as the coordinates array'
71+
}
72+
],
73+
isError: true
74+
};
75+
}
76+
77+
// Build coordinate string: "lon1,lat1;lon2,lat2;..."
78+
const coordsString = input.coordinates
79+
.map((coord) => `${coord.longitude},${coord.latitude}`)
80+
.join(';');
81+
82+
// Build query parameters
83+
const queryParams = new URLSearchParams();
84+
queryParams.append('access_token', accessToken);
85+
queryParams.append('geometries', input.geometries);
86+
queryParams.append('overview', input.overview);
87+
88+
// Add timestamps if provided (semicolon-separated)
89+
if (input.timestamps) {
90+
queryParams.append('timestamps', input.timestamps.join(';'));
91+
}
92+
93+
// Add radiuses if provided (semicolon-separated)
94+
if (input.radiuses) {
95+
queryParams.append('radiuses', input.radiuses.join(';'));
96+
}
97+
98+
// Add annotations if provided (comma-separated)
99+
if (input.annotations && input.annotations.length > 0) {
100+
queryParams.append('annotations', input.annotations.join(','));
101+
}
102+
103+
const url = `${MapboxApiBasedTool.mapboxApiEndpoint}matching/v5/mapbox/${input.profile}/${coordsString}?${queryParams.toString()}`;
104+
105+
const response = await this.httpRequest(url);
106+
107+
if (!response.ok) {
108+
const errorText = await response.text();
109+
let errorMessage = `Request failed with status ${response.status}: ${response.statusText}`;
110+
111+
try {
112+
const errorJson = JSON.parse(errorText);
113+
if (errorJson.message) {
114+
errorMessage = `${errorMessage} - ${errorJson.message}`;
115+
}
116+
} catch {
117+
if (errorText) {
118+
errorMessage = `${errorMessage} - ${errorText}`;
119+
}
120+
}
121+
122+
return {
123+
content: [{ type: 'text', text: errorMessage }],
124+
isError: true
125+
};
126+
}
127+
128+
const data = (await response.json()) as MapMatchingOutput;
129+
130+
// Validate the response against our output schema
131+
try {
132+
const validatedData = MapMatchingOutputSchema.parse(data);
133+
134+
return {
135+
content: [
136+
{ type: 'text', text: JSON.stringify(validatedData, null, 2) }
137+
],
138+
structuredContent: validatedData,
139+
isError: false
140+
};
141+
} catch (validationError) {
142+
// If validation fails, return the raw result anyway with a warning
143+
this.log(
144+
'warning',
145+
`Schema validation warning: ${validationError instanceof Error ? validationError.message : String(validationError)}`
146+
);
147+
148+
return {
149+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
150+
structuredContent: data,
151+
isError: false
152+
};
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)