A few months ago I was deep in the middle of building the Mapbox Location Agent, and I kept running into the same problem over and over. Every time I wanted the agent to do something useful with location data, I was writing boilerplate. Tool wrappers, retry logic, conversation state, multi-turn orchestration loops. The kind of stuff that has nothing to do with the actual product and everything to do with just getting the infrastructure out of the way.
At some point I looked at what we had built and realized: this is a framework. A pretty good one. And it would be a shame to keep it locked inside Mapbox.
So we're open-sourcing it. This is the story of what we built, why we built it the way we did, and how you can use it today.
If you are trying to build an AI agent that works with location data in JavaScript or TypeScript, your options are not great.
LangChain is Python-first and the JS port feels like an afterthought. Vercel AI SDK is thin by design, which is fine, but you end up writing all the orchestration yourself. Most of the tutorials show you how to do one tool call and stop there. Nobody is showing you how to handle five parallel tool calls, maintain conversation history across requests, rate-limit against API quotas, or compose multiple agents together.
And the location data piece? Almost completely ignored. You either write your own Mapbox wrappers or you skip location data entirely. Neither of those is a good answer when companies like DoorDash, Zillow, and Airbnb are all trying to make their AI systems spatially aware.
We were already building this stuff inside Mapbox. It made sense to build it right and share it.
The Mapbox Location Agent was our first real production agent. Building it taught us things that no blog post had warned us about.
Tool definitions are the API surface. The way you describe a tool to an LLM is the most important engineering decision you make. A bad description means the LLM calls the wrong tool, passes the wrong arguments, or gives up entirely. We spent as much time on tool descriptions as we spent on the actual implementation.
Conversation state is harder than it looks. Most demos store conversation history in a variable and call it done. Production apps need persistence across requests, multi-tenant isolation, and the ability to recover from crashes without losing context. We built a ConversationManager abstraction with pluggable backends: in-memory for development, SQLite for single-server deployments, Postgres and Redis for production.
Parallel tool calls are a superpower. When a user asks "find me a hotel near the convention center with good Italian food nearby," that is at minimum a geocoding call, a hotel search call, and a restaurant search call. If you execute them sequentially, the agent is slow. If you execute them in parallel, it feels instant. The orchestrator handles this automatically.
You need guardrails. The agent will sometimes try to do things you did not intend. PII scrubbing, keyword blocking, human-in-the-loop approval for sensitive operations: these are not optional in production. We built them as middleware so you can compose them without modifying your tool implementations.
Multi-agent composition is where things get interesting. One orchestrator with 30 tools starts to feel like a mess. The LLM has too many options, the system prompt bloats, and you end up with a generalist that is mediocre at everything. The better pattern is specialization: a planner agent that delegates to specialist agents, each of which has a focused tool set and a tight system prompt.
@mapbox/agents-tools is the core package. It gives you:
Toolbase class for defining tools with Zod schemasToolRegistryfor managing which tools are availableToolExecutorfor running tools with telemetry and retry logicToolOrchestratorfor the full multi-turn agent loopAgentToolfor wrapping an orchestrator as a tool (more on this below)- Guardrails:
PiiScrubber,KeywordBlocker, and a HITL approval hook - LLM adapters for Anthropic, OpenAI, Cohere, and Amazon Bedrock
@mapbox/agents-llm-providers wraps each LLM SDK with a unified interface. You can switch from Anthropic to OpenAI to Bedrock by changing one line. Streaming works the same way across all of them.
@mapbox/agents-conversation-manager handles conversation state. Pick your backend: InMemoryStateStore, SqliteStateStore, PostgresStateStore, or RedisStateStore.
@mapbox/agents-intent-matcher is an optional RAG layer that routes each user message to the relevant subset of tools. If you have 50 tools but most requests only need 5 of them, intent matching keeps your LLM context lean and your tool calls accurate.
@mapbox/agents-embedding-providers gives you pluggable embeddings: OpenAI's text-embedding-3-small, Cohere's embed models, or TransformersEmbedding for fully local inference via ONNX with no API key required.
@mapbox/agents-vector-db-adapters provides a unified interface to Qdrant and Pinecone for semantic search.
And then there is the Mapbox MCP server, which exposes 30+ production-quality Mapbox tools via the Model Context Protocol. Any agent that supports MCP can pick it up: directions, geocoding, isochrones, POI category search, matrix routing, static maps, reverse geocoding, and more. You do not even need to use our JavaScript framework to use the MCP server.
Here is what a real tool looks like. This is from our Vegas Concierge example:
import { Tool } from '@mapbox/agents-tools';
import { z } from 'zod';
const inputSchema = z.object({
destination: z.string().describe('Hotel name, strip area, or Las Vegas address'),
date: z.string().describe('Check-in date in YYYY-MM-DD format'),
nights: z.number().describe('Number of nights to book')
});
export class BookHotelTool extends Tool<
z.infer<typeof inputSchema>,
{ confirmationNumber: string; totalCost: number }
> {
constructor() {
super({
name: 'book_hotel',
description:
'Book a hotel room in Las Vegas. Use this after the user has selected a specific hotel and confirmed they want to proceed.',
parameters: [
{ name: 'destination', type: 'string', description: 'Hotel name, strip area, or Las Vegas address', required: true },
{ name: 'date', type: 'string', description: 'Check-in date in YYYY-MM-DD format', required: true },
{ name: 'nights', type: 'number', description: 'Number of nights', required: true }
],
inputSchema,
tags: ['booking', 'hotel'],
category: 'travel',
deprecated: false
});
}
protected async execute(input: z.infer<typeof inputSchema>) {
// your booking logic here
return { confirmationNumber: 'VGS-123456', totalCost: input.nights * 249 };
}
}The Zod schema is your source of truth. The orchestrator uses it to validate LLM outputs before your execute method ever runs. No more "cannot read property of undefined" at 2am because the LLM passed a number where you expected a string.
import {
ToolOrchestrator,
ToolRegistry,
ToolExecutor,
AnthropicAdapter
} from '@mapbox/agents-tools';
import { AnthropicProvider } from '@mapbox/agents-llm-providers';
import { ConversationManager, InMemoryStateStore } from '@mapbox/agents-conversation-manager';
const llm = new AnthropicProvider({
apiKey: process.env.ANTHROPIC_API_KEY,
model: 'claude-sonnet-4-6'
});
const registry = new ToolRegistry();
registry.register(new BookHotelTool());
registry.register(new SearchHotelsTool());
registry.register(new FindAttractionsTool());
const cm = new ConversationManager(new InMemoryStateStore());
const adapter = new AnthropicAdapter();
const executor = new ToolExecutor(registry);
const orchestrator = new ToolOrchestrator(llm, registry, executor, adapter, cm);
// Start a conversation
const result = await orchestrator.run(
'conv-123',
'Find me a hotel on the Strip for this weekend',
{ maxIterations: 5, maxToolCalls: 10 }
);
console.log(result.response);That is it. The orchestrator handles the full multi-turn loop: send message, get tool calls back, execute tools, send results, get next response, repeat until the LLM signals it is done or you hit the iteration cap.
The Mapbox MCP server is the fastest way to get location tools into an agent. You point your agent at the server path and it auto-discovers everything.
import { MapboxMCPClient, createMCPToolWrappers } from './mcp';
const mcpClient = new MapboxMCPClient({
serverPath: process.env.MCP_SERVER_PATH,
env: { MAPBOX_ACCESS_TOKEN: process.env.MAPBOX_ACCESS_TOKEN }
});
await mcpClient.connect();
const tools = await createMCPToolWrappers(mcpClient);
tools.forEach(tool => registry.register(tool));Your agent now has access to directions, geocoding, isochrones, POI search, static maps, and more. Each tool is already described in a way that LLMs understand. We did that work so you do not have to.
The MCP server works with any MCP-compatible runtime too. If you are using Claude Desktop, Cursor, or any other agent that supports MCP, you can drop our server in and get all 30+ tools without touching a line of JavaScript.
This is the thing I get most excited about. Once you understand it, you start seeing every complex agent problem differently.
The core idea is AgentTool. It wraps any ToolOrchestrator as a Tool. From the parent agent's perspective, calling a specialist agent looks exactly like calling any other tool. The LLM generates { "message": "find Italian restaurants near the Bellagio" } and the specialist agent takes it from there.
import { AgentTool } from '@mapbox/agents-tools';
// Build the search specialist
const searchRegistry = new ToolRegistry();
searchRegistry.register(new SearchAndGeocodeTool());
searchRegistry.register(new CategorySearchTool());
searchRegistry.register(new PlaceDetailsTool());
const searchOrchestrator = new ToolOrchestrator(llm, searchRegistry, new ToolExecutor(searchRegistry), adapter, cm);
// Wrap it as a tool for the planner
const searchSpecialist = new AgentTool({
name: 'search_specialist',
description: 'Find restaurants, hotels, and points of interest. Use for any search or discovery request.',
orchestrator: searchOrchestrator,
systemPrompt: 'You are a search specialist. Search for what the user needs and return clear, structured results.',
config: { maxIterations: 3, temperature: 0.3 }
});
// Build the planner
const plannerRegistry = new ToolRegistry();
plannerRegistry.register(searchSpecialist);
plannerRegistry.register(navigationSpecialist);
plannerRegistry.register(bookingSpecialist);
const planner = new ToolOrchestrator(llm, plannerRegistry, new ToolExecutor(plannerRegistry), adapter, cm);Now the planner can run specialists in parallel:
const result = await planner.run(conversationId, userMessage, {
parallelToolExecution: true,
maxIterations: 4
});When a user asks something that spans multiple domains, the planner fires all relevant specialists at the same time. Sub-conversations are fully isolated so parallel calls cannot interfere with each other. And because each specialist has a focused tool set and a tight system prompt, it is more reliable than one generalist agent with 30 tools.
The Tokyo Food Concierge is a working example of this pattern in the repo: a planner, a search specialist for finding restaurants, and a navigation specialist for routing and scooter booking. They share one ConversationManager and one AnthropicProvider. The planner coordinates; the specialists execute.
We have you covered there too. There is a Python version of the framework available at github.com/mapbox/mapbox-agents-for-python-private. The same patterns apply: tool definitions, orchestration, multi-agent composition, conversation state. And the Mapbox MCP server is language-agnostic by design, so Python agents connect to the same 30+ tools without any translation layer. Whether your team lives in TypeScript or Python, you are working with the same architecture and the same mental model.
The monorepo is at github.com/mapbox/mapbox-agents-for-js-private (private for now, reach out if you want access). The examples directory has four working agents you can run:
- Vegas Concierge: hotel search, flight search via Duffel, restaurant recommendations, weather, and human-in-the-loop booking confirmation
- Tokyo Food Concierge: multi-agent with a planner and specialists, Luup scooter integration, intent matching with Qdrant
- Real Estate Agent: property search, school proximity, walkability scoring, semantic search over listings
- Map App Starter: minimal starting point if you want to build from scratch
Each example comes with a .env.local.example that tells you exactly which keys you need. Most of them work with just an Anthropic API key and a Mapbox token.
git clone https://github.com/mapbox/mapbox-agents-for-js-private
cd mapbox-agents-for-js
pnpm install
cd examples/vegas-concierge
cp .env.local.example .env.local
# fill in your keys
pnpm devAgents are the new interface to location data. When a user asks an AI assistant where to eat, what route to take, or what neighborhood to live in, that request is spatial. The AI needs to understand geography, proximity, routing, and place. That is the Mapbox problem space and it is where we have been investing for a decade.
We built this framework because we needed it ourselves. We are sharing it because the problems we solved are the same problems every team building a spatial agent is running into. And because every agent built on this framework is one that calls Mapbox APIs, which means the better we make this, the better the ecosystem gets for everyone.
If you build something with it, I would genuinely love to see it. Open an issue, start a discussion, or just tag me. The best part of shipping something like this is seeing what people build that you never imagined.
The framework is available in both JavaScript/TypeScript and Python. The Mapbox MCP server works with any MCP-compatible agent runtime, no matter which language your team uses.