Chapter 11 · Mapbox MCP Runtime Patterns
Subchapter 11.9
AGENTS.mdMarkdown13 KBView on GitHub
Quick reference for integrating Mapbox MCP Server into AI applications for production use.
Runtime server providing geospatial tools to AI agents via Model Context Protocol.
| Category | Tools | Cost |
|---|---|---|
| Offline (Turf.js) | distance_tool, bearing_tool, midpoint_tool, point_in_polygon_tool, area_tool, buffer_tool, centroid_tool, bbox_tool, simplify_tool | Free, instant |
| Mapbox APIs | directions_tool, search_and_geocode_tool, reverse_geocode_tool, category_search_tool, isochrone_tool, matrix_tool, static_map_image_tool, map_matching_tool, optimization_tool | API costs apply |
| Utility | version_tool, category_list_tool | Free |
All tools use {longitude, latitude} object format — not arrays.
Object format {longitude: lng, latitude: lat}:
directions_tool - coordinates array of objectsisochrone_tool - coordinates parameterreverse_geocode_tool - coordinates parametercategory_search_tool - proximity parameterdistance_tool - from/to parametersbearing_tool - from/to parametersmidpoint_tool - from/to parameterspoint_in_polygon_tool - point parameterException — GeoJSON geometry (arrays only):
buffer_tool - geometry parameter uses [longitude, latitude] arrays (GeoJSON format)point_in_polygon_tool - polygon rings use [longitude, latitude] arraysNote: All coordinates use longitude before latitude order.
Use Mapbox’s hosted server - no installation needed:
https://mcp.mapbox.com/mcpConnect with your token in the Authorization: Bearer <token> header.
Note: Hosted server supports OAuth for interactive flows (coding assistants), but use token auth for programmatic runtime access.
npm install @mapbox/mcp-server
# Or: npx @mapbox/mcp-server
export MAPBOX_ACCESS_TOKEN="your_token"from pydantic_ai import Agent
import subprocess
# Start MCP server
mcp = subprocess.Popen(['npx', '@mapbox/mcp-server'],
env={'MAPBOX_ACCESS_TOKEN': token})
agent = Agent(
model='gateway/openai:gpt-5.2',
tools=[
lambda from_loc, to_loc: call_mcp('directions_tool', {
'origin': from_loc,
'destination': to_loc
})
]
)import { spawn } from 'child_process';
const mcp = spawn('npx', ['@mapbox/mcp-server'], {
env: { MAPBOX_ACCESS_TOKEN: process.env.MAPBOX_ACCESS_TOKEN }
});
const mastra = new Mastra({
workflows: {
findRestaurants: {
steps: [
{ tool: 'mapbox.category_search_tool', input: {...} },
{ tool: 'mapbox.matrix_tool', input: {...} }
]
}
}
});import { DynamicTool } from '@langchain/core/tools';
const tools = [
new DynamicTool({
name: 'directions_tool',
description: 'Get driving directions',
func: async (input) => {
const { origin, destination } = JSON.parse(input);
return await callMCP('directions_tool', { origin, destination });
}
})
];class MapboxAgent {
private mcpProcess: ChildProcess;
async initialize() {
this.mcpProcess = spawn('npx', ['@mapbox/mcp-server'], {
env: { MAPBOX_ACCESS_TOKEN: process.env.MAPBOX_ACCESS_TOKEN }
});
}
async callTool(name: string, params: any): Promise<any> {
const request = {
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: { name, arguments: params }
};
this.mcpProcess.stdin.write(JSON.stringify(request) + '\n');
return new Promise((resolve) => {
this.mcpProcess.stdout.once('data', (data) => {
const response = JSON.parse(data.toString());
resolve(response.result);
});
});
}
}// Find properties with good commute
async findByCommute(home: Point, work: Point, maxMinutes: number) {
// 1. Get reachable area from work
const isochrone = await mcp.call('isochrone_tool', {
coordinates: work,
contours_minutes: [maxMinutes]
});
// 2. Check if home is within range
const inRange = await mcp.call('point_in_polygon_tool', {
point: home,
polygon: isochrone
});
// 3. Get exact commute time
if (inRange) {
const route = await mcp.call('directions_tool', {
coordinates: [
{ longitude: home[0], latitude: home[1] },
{ longitude: work[0], latitude: work[1] }
],
routing_profile: 'mapbox/driving-traffic'
});
return { commuteMinutes: route.duration / 60 };
}
}// Check delivery availability
async canDeliver(restaurant: Point, address: Point, maxTime: number) {
// 1. Calculate delivery zone
const zone = await mcp.call('isochrone_tool', {
coordinates: restaurant,
contours_minutes: [maxTime],
profile: 'mapbox/driving'
});
// 2. Check if address is in zone
const canDeliver = await mcp.call('point_in_polygon_tool', {
point: address,
polygon: zone
});
// 3. Get delivery time with traffic
if (canDeliver) {
const route = await mcp.call('directions_tool', {
coordinates: [
{ longitude: restaurant[0], latitude: restaurant[1] },
{ longitude: address[0], latitude: address[1] }
],
routing_profile: 'mapbox/driving-traffic'
});
return { eta: route.duration / 60, distance: route.distance };
}
}// Find nearby attractions with travel times
async findAttractions(hotel: Point, category: string) {
// 1. Search nearby
const places = await mcp.call('category_search_tool', {
category,
proximity: hotel
});
// 2. Calculate distances (offline, free)
const withDistances = await Promise.all(
places.map(async (place) => ({
...place,
distance: await mcp.call('distance_tool', {
from: hotel,
to: place.coordinates,
units: 'miles'
})
}))
);
// 3. Get travel times (batch API call)
const matrix = await mcp.call('matrix_tool', {
origins: [hotel],
destinations: places.map(p => p.coordinates),
profile: 'mapbox/walking'
});
return withDistances.map((place, i) => ({
...place,
walkingMinutes: matrix.durations[0][i] / 60
}));
}Application Layer
↓
AI Agent Layer (pydantic-ai, mastra, custom)
↓
MCP Server (geospatial tools)
↓
↙ ↘
Turf.js Mapbox APIs
(free) (API costs)| Need | Use | Reason |
|---|---|---|
| Distance calculation | distance_tool (offline) | Free, instant |
| Point in polygon | point_in_polygon_tool (offline) | Free, instant |
| Bounding box | bbox_tool (offline) | Free, instant |
| Simplify geometry | simplify_tool (offline) | Free, instant |
| Directions with traffic | directions_tool (API) | Real-time data |
| Geocoding | reverse_geocode_tool (API) | Requires database |
| Isochrones | isochrone_tool (API) | Complex calculation |
| Multi-stop optimization | optimization_tool (API) | Complex calculation |
| GPS trace matching | map_matching_tool (API) | Requires routing data |
| Bearing/midpoint | bearing_tool/midpoint_tool (offline) | Free, instant |
| POI categories | category_list_tool (utility) | Metadata lookup |
class CachedMCP {
private cache = new Map();
private offlineTools = ['distance_tool', 'point_in_polygon_tool'];
async callTool(name: string, params: any) {
// Cache offline tools forever (deterministic)
const ttl = this.offlineTools.includes(name) ? Infinity : 3600000;
const key = JSON.stringify({ name, params });
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.result;
}
const result = await this.mcp.callTool(name, params);
this.cache.set(key, { result, timestamp: Date.now() });
return result;
}
}// ❌ Bad: Sequential calls
for (const location of locations) {
await mcp.call('distance_tool', { from: user, to: location });
}
// ✅ Good: Parallel
await Promise.all(locations.map((loc) => mcp.call('distance_tool', { from: user, to: loc })));
// ✅ Better: Use matrix tool
await mcp.call('matrix_tool', {
origins: [user],
destinations: locations
});class RobustMCP {
async callWithRetry(name: string, params: any, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await this.mcp.callTool(name, params);
} catch (error) {
if (error.code === 'RATE_LIMIT') {
await this.sleep(Math.pow(2, i) * 1000); // Exponential backoff
continue;
}
throw error; // Non-retryable
}
}
}
}// ✅ Good: Environment variables
const token = process.env.MAPBOX_ACCESS_TOKEN;
// ✅ Good: Scoped tokens (minimal permissions)
// directions:read, geocoding:read only
// ✅ Good: Rate limiting
class RateLimitedMCP {
private requestsPerMinute = 300;
private requestCount = 0;
async callTool(name: string, params: any) {
if (this.requestCount >= this.requestsPerMinute) {
await this.waitForNextMinute();
}
this.requestCount++;
return await this.mcp.callTool(name, params);
}
}// Mock MCP for tests
class MockMCP {
async callTool(name: string, params: any) {
const mocks = {
distance_tool: () => '2.5',
directions_tool: () => ({ duration: 1200, distance: 5000 }),
point_in_polygon_tool: () => true
};
return mocks[name]?.();
}
}
// Use in tests
const agent = new MapboxAgent(new MockMCP());| Use MCP ✅ | Use Direct API ❌ |
|---|---|
| AI agent interactions | Simple operations |
| Complex workflows | Performance-critical |
| Offline calculations | Client-side rendering |
| Multi-step geospatial logic | Map display |
| Prototyping | Production maps |
// Prefer offline tools (free)
const freeOps = [
'distance_tool',
'point_in_polygon_tool',
'bearing_tool',
'area_tool',
'centroid_tool',
'bbox_tool',
'simplify_tool',
'midpoint_tool',
'buffer_tool'
];
// Use API tools only when necessary
const apiOps = [
'directions_tool', // Need traffic data
'reverse_geocode_tool', // Need address database
'isochrone_tool', // Complex calculation
'category_search_tool', // Need POI database
'matrix_tool', // Travel time matrix
'static_map_image_tool', // Static map generation
'map_matching_tool', // GPS trace matching
'optimization_tool' // Route optimization
];
// Utility tools
const utilityOps = [
'version_tool', // Server version info
'category_list_tool' // Available POI categories
];
function chooseTool(operation: string, needsRealtime: boolean) {
if (needsRealtime) return apiOps[operation];
return freeOps.includes(operation) ? operation : apiOps[operation];
}