Skip to content
Open
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
- **Fields:** the text, a category (`bug`, `wrong_result`, `feature_request` or `other`) and an optional tool name. The endpoint requires a location, which feedback about the server doesn't have, so it sends 0, 0 as the CLI does.
- **Where it came from:** the server appends `— mapbox-mcp-server <version> via <client> <version>, about <tool>` to the text, so the team can tell these from the CLI's reports. The client comes from the server handling the call, which the hosted server needs, since one process serves many clients.
- **Confirmation:** like `feedback_tool`, it asks through elicitation when the client supports it and sends nothing if the user declines or the prompt fails. It sends its own id, so a retried POST doesn't make a second report.
- **`elevation_tool`: ground elevation at places, and climb and descent along a path or route (#293).** The server had no elevation at all, so "how hilly is this bike route?" or "how high is this town?" had no answer from Mapbox data. Part 4 of #289. A general `tilequery` tool stays in mcp-devkit-server, since raw tileset layers are a developer tool.
- **Inputs:** up to 25 `points`, a `path` of up to 1,000 coordinates, or the `route_ref` from a `directions_tool` or `map_matching_tool` result, which the tool turns back into the full line with `directionsRequestFromRef` (#276) or the new `mapMatchingRequestFromRef`, so a planned trail run or a recorded one both work, and the model never copies route geometry.
- **Data:** reads `mapbox.mapbox-terrain-dem-v1` raster tiles (`.pngraw`, terrain-RGB) and decodes each pixel's height, with bilinear interpolation between pixels. Tilequery can't read the DEM (it returns 422), and its Terrain v2 contours come in 10 m steps or coarser: 240 m near Twin Peaks against 249.5 m from the DEM, 10 m at Civic Center against 20.9 m. Longs Peak reads 4,346 m, its surveyed height. A small PNG decoder on Node's `zlib` handles the 8-bit RGB/RGBA tiles, so there's no new dependency.
- **Cost:** a path is sampled at up to 500 points, never closer than one DEM pixel. Samples share tiles, each tile is fetched once per call, and the zoom drops on long paths until the path needs at most 48 tiles. The result reports `zoom` and `resolution_m`. Ferry Building to Twin Peaks by bike: 1 tile request at 7.5 m resolution, 274 m of climb. San Francisco to Los Angeles: 1.3 s at about 120 m resolution, highest point 1,276 m near Tejon Pass.
- **Climb:** swings under 2 m don't count, so DEM noise on flat ground doesn't add up to phantom hills.
- **Result:** elevations, or distance, climb, descent, start, end, highest and lowest points and a profile of up to 50 points for a chart (12 in the text), plus a map ref with the line and ▲/▼ markers at the high and low points. For a directions ref, the text says to pass the route's ref alongside, rather than copying the route into this one.
- **Testing:** a PNG encoder in the tests covers every row filter, RGB and RGBA, and bad input (not a PNG, 16-bit, palette, truncated). Synthetic terrain tiles test point heights, interpolation, climb along a path, a directions ref, the zoom choice for a long route, the noise threshold, a refused token, and that `-0 m` never prints.
- **`ground_location_tool`: land use, water, elevation and live traffic at the point, picked with `include` (#324).** "Is this spot in a park?", "is this on the water?" or "how bad is traffic at this address?" had no answer: the tool named the neighborhood and nearby POIs, but not what the spot itself is. Folded in here rather than new tools, so the model doesn't have more "what's here" tools to pick between, and new layers become new `include` values instead of new tools.
- **`include`:** `land`, `water`, `elevation` and `traffic`, each its own request, run in parallel with the existing calls. The model picks what the question needs; left out, none are requested, so existing calls such as "coffee nearby" make no extra requests. A failed request, including a network error, leaves its field out instead of failing the answer.
- **`land`:** a point-in-polygon Tilequery on Mapbox Streets v8 for land use (park, cemetery, school, residential, wood...), protected areas (national parks, with names) and the building the point is in (type, height). Wetlands, which Streets keeps in the same overlay layer as national parks, are reported as land use. Streets park polygons have no names, so a city park is just "park".
- **`water`:** a 500 m radius Tilequery on `water,waterway`: on water, or the distance to the nearest water.
- **`elevation`:** one Terrain-DEM tile, read with `elevation_tool`'s decoder (#293).
- **`traffic`:** a 100 m radius Tilequery on Mapbox Traffic v1: live congestion (low, moderate, heavy, severe) per road class, at its worst, with the distance to that worst segment, nearest first. The tiles have road classes but no street names, and quiet streets often have no data, so an empty list says no data rather than no traffic.
- **Evals:** new tool-selection cases for land use, water, elevation and traffic questions.

### Bug Fixes

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Each of the four prompts below exercises a different strategy in one tool call:

- "Get driving directions from LAX to Hollywood with current traffic"
- "How long would it take to walk from Central Park to Times Square?"
- "How much climbing is on a bike ride from the Ferry Building to Twin Peaks?"
- "Calculate travel time from my hotel (Four Seasons) to JFK Airport by taxi during rush hour"

### Visualization & Maps
Expand Down Expand Up @@ -516,6 +517,18 @@ Finds the optimal route through multiple locations using the [Mapbox Optimizatio

**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.

#### Elevation tool

Gets ground elevation from Mapbox's terrain data ([Mapbox Terrain-DEM v1](https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/)). Give it up to 25 places for the height above sea level of each, or a line for:

- Total climb and descent
- The highest and lowest points, and how far along the line they are
- An elevation profile of up to 50 points

The line can be a `path` of coordinates, or the `route_ref` from a `directions_tool` or `map_matching_tool` result, which the tool turns back into the full route itself. The server reads the terrain tiles and decodes each pixel's height, so values aren't rounded to contour steps the way Tilequery's Terrain v2 contours are. Points along a line share tiles, and long lines are read at a coarser zoom so a call stays under 50 tile requests; `resolution_m` in the result says how coarse. Swings under 2 m don't count toward climb or descent, so DEM noise on flat ground doesn't add up. Heights are of the ground, not of buildings or bridges. The result includes a map ref with the line and its high and low points.

**Example Usage**: "How hilly is the bike route from the Ferry Building to Twin Peaks?" or "How high is Longs Peak?"

#### Feedback tool

Sends a user's feedback about the app or agent to its developer, through the [Mapbox Feedback API](https://docs.mapbox.com/api/feedback/) (Public Preview). It's filed under the account that owns the access token, so when you build an app or agent on this server with your own token, your users' feedback lands in the same feed as your apps' Feedback Agent reports, and you review it in Feedback Explorer. It does not report problems to Mapbox. Like [Feedback Agent](https://docs.mapbox.com/android/navigation/guides/feedback-agent/), it's for anything about your product that helps you understand what users need, for example:
Expand Down
1 change: 1 addition & 0 deletions docs/importing-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import {
// API tools (HTTP pre-configured)
categorySearch,
directions,
elevation,
feedback,
isochrone,
mapMatching,
Expand Down
112 changes: 112 additions & 0 deletions evals/tool-selection/cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,5 +293,117 @@
"stubs": {
"search_and_geocode_tool": "1. 410 Valencia St, San Francisco, CA 94103 (37.7655, -122.4218)\n2. 2001 Fillmore St, San Francisco, CA 94115 (37.7880, -122.4335)\n3. 1 Ferry Building, San Francisco, CA 94111 (37.7955, -122.3937)"
}
},
{
"id": "land-park",
"category": "land",
"prompt": "Is 37.7694, -122.4862 inside a park?",
"why": "Land use at a point: ground_location_tool reads the Streets land use polygons. Reverse geocoding only names the area, and can't say whether the point is in a park.",
"expect": {
"any_of": ["ground_location_tool"],
"must_not": [
"render_map_tool",
"static_map_image_tool",
"places_map_display_v0"
]
},
"stubs": {
"ground_location_tool": "**Outer Sunset** (neighborhood context · live Mapbox data)\nOuter Sunset, San Francisco, California, United States\n\nLand use: park, grass (meadow)\nNearest water: 292 m\nElevation: 45 m\n\nReachable by walking: 5 min, 10 min, 15 min\n\n*Sources: Mapbox Geocoding API, Mapbox Isochrone API, Mapbox Tilequery API (Streets v8), Mapbox Terrain-DEM*",
"reverse_geocode_tool": "Outer Sunset, San Francisco, California 94122, United States",
"category_search_tool": "1. Golden Gate Park (37.7694, -122.4862), 0 m\n2. Spreckels Lake (37.7704, -122.4955), 820 m"
}
},
{
"id": "land-water",
"category": "land",
"prompt": "I'm about to drop a meetup pin at 37.83, -122.42. Is that spot on land or in the water?",
"why": "Water at a point: ground_location_tool says on_water. No map asked for.",
"expect": {
"any_of": ["ground_location_tool"],
"must_not": [
"render_map_tool",
"static_map_image_tool",
"places_map_display_v0"
]
},
"stubs": {
"ground_location_tool": "**San Francisco** (neighborhood context · live Mapbox data)\nSan Francisco, California, United States\n\nOn water\nElevation: 0 m\n\n*Sources: Mapbox Geocoding API, Mapbox Tilequery API (Streets v8), Mapbox Terrain-DEM*",
"reverse_geocode_tool": "San Francisco, California, United States"
}
},
{
"id": "elevation-point",
"category": "elevation",
"prompt": "How high above sea level is 37.7544, -122.4475?",
"why": "Elevation at one point: ground_location_tool includes it, and elevation_tool with points is also right.",
"needs_tools": ["elevation_tool"],
"expect": {
"any_of": ["ground_location_tool", "elevation_tool"],
"must_not": [
"render_map_tool",
"static_map_image_tool",
"places_map_display_v0"
]
},
"stubs": {
"ground_location_tool": "**Twin Peaks** (neighborhood context · live Mapbox data)\nTwin Peaks, San Francisco, California, United States\n\nLand use: park\nNearest water: 182 m\nElevation: 250 m\n\n*Sources: Mapbox Geocoding API, Mapbox Tilequery API (Streets v8), Mapbox Terrain-DEM*",
"elevation_tool": "Ground elevation (mapbox.mapbox-terrain-dem-v1, about 7.5 m resolution):\n250 m at 37.75440, -122.44750",
"reverse_geocode_tool": "Twin Peaks, San Francisco, California 94131, United States"
}
},
{
"id": "elevation-summits",
"category": "elevation",
"prompt": "Which is higher: Mount Davidson or Twin Peaks in San Francisco? Use live map data, not memory.",
"why": "Elevation at several places: geocode both, then one elevation_tool call with points (or ground_location_tool for each).",
"needs_tools": ["elevation_tool"],
"expect": {
"any_of": ["elevation_tool", "ground_location_tool"],
"must_not": [
"render_map_tool",
"static_map_image_tool",
"places_map_display_v0"
]
},
"stubs": {
"search_and_geocode_tool": "1. Mount Davidson, San Francisco, CA (37.7383, -122.4542)\n2. Twin Peaks, San Francisco, CA (37.7544, -122.4475)",
"elevation_tool": "Ground elevation (mapbox.mapbox-terrain-dem-v1, about 7.5 m resolution):\n1. 282 m at 37.73830, -122.45420\n2. 250 m at 37.75440, -122.44750",
"ground_location_tool": "**Twin Peaks** (neighborhood context · live Mapbox data)\nTwin Peaks, San Francisco, California, United States\n\nLand use: park\nNearest water: 182 m\nElevation: 250 m\n\n*Sources: Mapbox Geocoding API, Mapbox Tilequery API (Streets v8), Mapbox Terrain-DEM*"
}
},
{
"id": "elevation-route",
"category": "elevation",
"prompt": "How much climbing is there biking from the Ferry Building to Twin Peaks in San Francisco?",
"why": "Climb along a route: directions_tool, then elevation_tool with the route's ref. No map asked for.",
"needs_tools": ["elevation_tool"],
"expect": {
"must": ["directions_tool", "elevation_tool"],
"must_not": [
"render_map_tool",
"static_map_image_tool",
"places_map_display_v0"
]
},
"stubs": {
"search_and_geocode_tool": "1. Ferry Building, 1 Ferry Building, San Francisco, CA (37.7955, -122.3937)\n2. Twin Peaks, San Francisco, CA (37.7544, -122.4475)",
"directions_tool": "Cycling route: 8.4 km, about 38 min via Market St and Clarendon Ave.",
"elevation_tool": "Elevation along 8.4 km (mapbox.mapbox-terrain-dem-v1, 500 samples, about 7.5 m resolution):\nClimb: 274 m, descent: 27 m\nStart: 3 m, end: 250 m\nHighest: 250 m at 8.4 km\nLowest: 2 m at 0.3 km"
}
},
{
"id": "traffic-address",
"category": "land",
"prompt": "How bad is traffic right now around 1 Market St, San Francisco?",
"why": "Live traffic at an address: geocode it, then ground_location_tool with include traffic. Showing the traffic layer on a map is fine too, but the reply needs numbers, not just a picture.",
"expect": {
"any_of": ["ground_location_tool"],
"must_not": ["static_map_image_tool", "places_map_display_v0"]
},
"stubs": {
"search_and_geocode_tool": "1. 1 Market St, San Francisco, CA 94105 (37.7942, -122.3949)",
"ground_location_tool": "**Financial District** (neighborhood context · live Mapbox data)\nFinancial District, San Francisco, California, United States\n\nLive traffic within 100 m: moderate on primary (22 m), heavy on motorway link (64 m), low on street (80 m)\n\n*Sources: Mapbox Geocoding API, Mapbox Tilequery API (Traffic v1)*",
"directions_tool": "Driving route: 1.2 km, 6 min (typical 4 min) via The Embarcadero."
}
}
]
1 change: 1 addition & 0 deletions evals/tool-selection/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ const HINTED_TOOLS = new Set([
'directions_tool',
'isochrone_tool',
'ground_location_tool',
'elevation_tool',
'map_matching_tool',
'optimization_tool',
'union_tool',
Expand Down
41 changes: 41 additions & 0 deletions src/tools/elevation-tool/ElevationTool.input.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Mapbox, Inc.
// Licensed under the MIT License.

import { z } from 'zod';

const coordinateSchema = z.object({
longitude: z.number().min(-180).max(180),
latitude: z.number().min(-90).max(90)
});

/**
* Input schema for ElevationTool. Exactly one of `points`, `path` or
* `route_ref` is required; the tool checks that itself so the
* published JSON schema stays a plain object.
*/
export const ElevationInputSchema = z.object({
points: z
.array(coordinateSchema)
.min(1)
.max(25)
.optional()
.describe(
'Up to 25 places to get the ground elevation of, e.g. a summit, a town or a campsite.'
),
path: z
.array(coordinateSchema)
.min(2)
.max(1000)
.optional()
.describe(
'A line (hike, ride, run) as ordered coordinates, for climb, descent and an elevation profile along it. For a route from directions_tool or a GPS trace matched with map_matching_tool, pass route_ref instead.'
),
route_ref: z
.string()
.optional()
.describe(
'The structuredContent.mapboxRender.ref from a directions_tool result (a planned route) or a map_matching_tool result (a recorded run, ride or hike), passed unchanged. The tool fetches the full line itself, so you never need to copy route geometry.'
)
});

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

import { z } from 'zod';

const elevationPointSchema = z.object({
longitude: z.number(),
latitude: z.number(),
elevation_m: z.number()
});

const pathPointSchema = elevationPointSchema.extend({
distance_m: z.number().describe('Distance along the path from its start')
});

export const ElevationOutputSchema = z.object({
source: z.string().describe('Tileset the elevations were read from'),
zoom: z.number().describe('Tile zoom used; lower on long paths'),
resolution_m: z
.number()
.describe('Ground size of one elevation pixel at that zoom'),
points: z.array(elevationPointSchema).optional(),
path: z
.object({
distance_m: z.number(),
sample_spacing_m: z.number(),
sample_count: z.number(),
climb_m: z.number(),
descent_m: z.number(),
start: pathPointSchema,
end: pathPointSchema,
highest: pathPointSchema,
lowest: pathPointSchema,
profile: z
.array(z.object({ distance_m: z.number(), elevation_m: z.number() }))
.describe('Up to 50 evenly spaced samples, for a chart')
})
.optional(),
mapboxRender: z.object({ ref: z.string() }).passthrough().optional()
});

export type ElevationOutput = z.infer<typeof ElevationOutputSchema>;
Loading
Loading