report_tool: send feedback about the MCP server to Mapbox (#330). The model can pass on a bug, a wrong or confusing tool result, a missing capability or a suggestion to the team that builds the server. It usesPOST /user-feedback/v1/cli-feedback, the undocumented Feedback API endpoint added for the Mapbox CLI'smapbox report(see its spec), which files the report under Mapbox's own account with any valid token and no extra scope. It isn't for wrong map data: routing those reports to the data teams needs their agreement and a triage pipeline first.- Fields: the text, a category (
bug,wrong_result,feature_requestorother) 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.
- Fields: the text, a category (
- Big maps failed in ChatGPT with "cannot read resource outside its widget scope" (#327). Isochrones for every store of a coffee chain, across three travel modes, made a map payload over 50 KB. Above that size
render_map_toolleaves the payload out ofstructuredContentand only sends amapbox://temp/ref, and ChatGPT only lets the widget read resources in its ownui://scope, so the map couldn't load it.render_map_toolnow also puts a payload that big in the result's_meta.ui.payload, which hosts pass to the map but not to the model, and which the map already checks before the ref. render_map_tool: the summary chip no longer covers the zoom buttons after the map frame narrows (#326, fixes #307). #278 moved the zoom and layer buttons below the chip when it appears, but measured the chip only then. When a host narrowed the frame or switched display mode afterwards, the chip wrapped to more lines and covered the buttons again: at 200 px wide, a click on + landed on the chip. AResizeObserveron the chip now re-places the buttons whenever its size changes. Browsers withoutResizeObserverkeep the old behavior.
- OpenTelemetry JS 3.x (#325).
@opentelemetry/sdk-trace-baseis deprecated in every 2.x release and removed in 3.0, and the ox-security scan flagged it on every PR touchingpackage.json(#319). Moves the OpenTelemetry set to the 3.0 line:sdk-trace3.0.0 in place ofsdk-trace-base,resources3.0.0,sdk-node,instrumentationandexporter-trace-otlp-http0.300.0,auto-instrumentations-node0.82.0 andapi1.10.0. 3.1.0 came out the day before, so this stays on 3.0.0 for now.- Code:
ConsoleSpanExporter,ReadableSpanandSpanExporternow come fromsdk-trace. In the test,BasicTracerProvideris nowTracerProvider, andSimpleSpanProcessortakes{ exporter }. - Node:
enginesis now>=22.15.0, the 3.x minimum (it was>=22). - Checked: the built server, both ESM and CommonJS, sends the same
config.load_envandtool.distance_toolspans and the same metrics to a local OTLP endpoint as 0.17.0 does.
- Code:
- Agents asked for a Mapbox token instead of calling
list_public_tokens_tool(#321). Building a single-page map app on staging, Codex quoted our instruction back and still asked the user to paste a token. The server instructions and the tool description now say to put the returnedpk.token in the page, since public tokens are made to ship in browser code, and not to leave a placeholder. - Maps from searches showed the wrong place, or failed with two searches (#320). Found on staging.
- Wrong city: "Show coffee shops near Union Square, San Francisco" drew cafés in southern New Jersey, where the user was. The map runs a search ref's search again from the browser, and with no
proximityorbboxthe Search Box API biases toward the caller's IP, so on a hosted server the map and the reply came from different places.search_and_geocode_toolandcategory_search_toolnow pin the ref to the first result in that case, flaggedproximityFromResultsso the map draws no "Search center" pin there. - Distances: the cards and popups showed distances measured from that IP (4059202 m from SFO for a user in New Jersey). They now show only when the call passed a
proximity. - Two searches on one map: "Driving route from SFO to Palo Alto" passed a search ref for each end, and the second failed with
There is already a source with ID "selffetch-search-results". Self-fetched layers that would reuse an id now get a free one (-2,-3, ...), which also covers two routes, isochrones or trips on one map. - "[object Object]" errors: Search Box errors carry the reason in an object (
{"message":{"error":"Invalid proximity provided..."}}), and both the map and the tools printed the object. Both now show theerrortext. Search refs whoseproximityorbboxisn't the shape the map sends (a hand-written"lon,lat"string, say) are now rejected when resolved, sorender_map_tooltells the model to re-run the search instead of the map failing. - Retyped route refs: Codex passed a directions ref whose destination key read
longittude, and the map failed with "Could not fetch route". Directions, map matching and optimization refs now need at least two coordinates with numericlongitudeandlatitudeto resolve, the same check the map makes, so a damaged ref is caught atrender_map_toolinstead.
- Wrong city: "Show coffee shops near Union Square, San Francisco" drew cafés in southern New Jersey, where the user was. The map runs a search ref's search again from the browser, and with no
render_map_tool: live traffic, and Satellite/Traffic buttons on the map (#278). Asked to show traffic downtown, ChatGPT told a user the map couldn't do it, which was true: the map only drew GeoJSON, and traffic showed up only in route summaries. Switching to satellite also took another tool call. Fixes #277.- Buttons: two icon buttons sit under the zoom control, the same size as zoom and compass: a filled globe for satellite view and a filled car for live traffic, per the design feedback, each labeled with a tooltip and
aria-label. One that's on gets a blue icon on a light blue background andaria-pressed. - Traffic: the Mapbox Traffic v1 tileset (
mapbox://mapbox.mapbox-traffic-v1) is drawn as lines colored by congestion (light, moderate, heavy, severe) in themiddleslot, so road labels and our own layers stay on top. Light traffic shows only on main roads (motorway through tertiary, plus ramps), because on every side street it turned the whole map green; moderate and worse show on every road. Each direction is offset to its own side of the road, and lines run from 1 px at zoom 12 to 4 px at zoom 18. While it's on, a white "Fast [four colors] Slow" pill sits in the bottom left, below any payload legend, using the design's colors (#00c94e,#ff9900,#ff2b3a,#8b0a18) for both the pill and the lines. The tiles come fromapi.mapbox.com, which the CSP already allows. The map doesn't refetch them on a timer, so a map left open goes stale until it's moved or reloaded. showTraffic: a newrender_map_toolinput, and a line in the tool description, so the model knows it can show traffic and can open the map with it on.falseturns it off. Left out, the map keeps whatever the user picked, the same waybaseStyleworks. AshowTraffic-only call is renderable on its own, likebaseStyle.- Style switches keep what's drawn:
setStyle()drops every source and layer the page added. The page now keeps a copy of each GeoJSON layer it draws (including self-fetched routes and isochrones, and deferred data once it arrives), and after a switch from the Satellite button it re-adds them, the base map config and traffic without moving the camera. AbaseStyleswitch from the model keeps traffic on too. - Summary chip: it sat on top of the zoom buttons in the top-left corner. While it shows, the corner's controls now sit below it.
- Logo and attribution: the legend covered the Mapbox logo, and an open results panel covered the attribution, both of which the Mapbox terms require to stay visible. The legend now sits above the logo and the panel stops above the attribution, and the map's fit leaves room for the legend's new spot.
- Testing: added tests for both buttons, the traffic layer and legend, restoring layers and base map config after a switch,
showTrafficon, off and left out, and the controls moving below the summary. The test sandbox's fake map now tracks sources and layers like a style does (setStyleclears them), and mounts controls throughonAdd. Checked a live San Francisco tile: the source layer istraffic, withcongestion,classandstructure. Checked in Claude Desktop on both styles.
- Buttons: two icon buttons sit under the zoom control, the same size as zoom and compass: a filled globe for satellite view and a filled car for live traffic, per the design feedback, each labeled with a tooltip and
render_map_tool: results carousel at the bottom of the map instead of the side list (#292). Search results and id-bearing markers now show as a row of swipeable cards along the bottom, like the place results in ChatGPT, instead of the 280 px list on the right, which covered much of an inline map or started collapsed below 560 px wide. Each card has the place's name, category, price level (Placesattributes.price_levelas$to$$$$) and distance, its photo once loaded, and a second line with its hours. There's no star rating, since the Places API doesn't have one andscore.popularityisn't one.- Place pins instead of orange numbers: places are white pins with a Maki icon for their category (the Maki name Search Box gives each result, or one matched from the category, falling back to a plain marker), like ChatGPT's place results. About 20 common icons are inlined (about 12 KB). A caller's color tints the icon. Numbered badges stay for markers whose number means something: a caller's
style: "numbered"with alabel(a visit order), optimized trip stops, and route start/end. - Card and pin stay in sync: the selected card gets a border, and its pin turns black (a numbered badge turns white), and comes to the front. Every carousel pin shows its name under it with a white halo, like a map label. The first card starts selected. Swiping selects whichever card ends up in the middle, clicking a card selects it, and clicking a pin scrolls to its card. Picking a card or a pin moves the map only when the pin or its name label is off screen or behind the cards, and then only far enough to show them.
- No popups on carousel pins: a pin with a card no longer opens a popup, and the phone number waits for a place detail card. Pins without an id (route ends, waypoints) keep their popups.
- Hours: parsed from the OSM-style
opening_hoursstring for simple rules (Mo-Fr 07:00-19:00; Sa,Su 08:00-17:00,Su off,24/7, several ranges a day, ranges past midnight). Anything more complex (holidays, months) shows no hours rather than wrong ones. With the place's time zone (Placestelemetry.tags.timezone) the card says "Open until 8:30 PM" in green or "Closed · opens 7:30 AM" in red. Without one it shows today's hours on the viewer's day ("Open 7:30–15:00"). Times use the viewer's locale. - Layout: the cards sit 36 px up so the Mapbox logo and attribution stay visible, the legend moves up above them, and the map fits its data above both. The collapse toggle and the right-hand fit padding are gone. The zoom buttons move down below the summary chip ("6 results for …") while it shows, instead of sitting under it (same fix as #278).
- OpenAI's UI guidelines: every card has a picture (the place's icon on a muted tile until a photo loads, or when there is none), labeled with the place's name for screen readers. Numbered badges pick dark or white numbers by contrast with the badge color (WCAG). The tool description asks the model not to list the places again after rendering, since the cards already show them.
- Light and dark: the cards, summary chip, legend and messages follow the host's theme (MCP Apps
hostContext.theme, at start and onhost-context-changed), or the system'sprefers-color-schemewhen the host sends none. Colors come from the host's style variables (--color-background-primary,--color-text-primaryand so on) when it sends them, so the cards match the chat around them. The map, its pins and their name labels look the same in both. - Tool and resource descriptions now say "carousel" instead of "panel". Dedupe across merged payloads, the batched Place Details lookup and photo loading are unchanged.
- Place pins instead of orange numbers: places are white pins with a Maki icon for their category (the Maki name Search Box gives each result, or one matched from the category, falling back to a plain marker), like ChatGPT's place results. About 20 common icons are inlined (about 12 KB). A caller's color tints the icon. Numbered badges stay for markers whose number means something: a caller's
static_map_image_toolshows its map full size in an MCP App viewer (#315). MCP Apps hosts showed the image result as a small thumbnail, and the model's reply couldn't show it. The tool now points_meta.uiatui://mapbox/static-map/index.html, which draws the image from the tool result at full width as adata:URL (no token, no request), falling back to #312's public-token URL when the image went to a temporary resource. Clients that declare MCP Apps support get text saying the map is already shown, so the reply doesn't repeat it. The result itself is unchanged, so hosts without MCP Apps keep the image and the link.
- The dev HTTP server (
npm run dev:http) sends the server instructions (#314). It built its server without them, unlikesrc/index.tsand hosted, so local plugin QA pointed at it could route tools differently from production. In Codex, an agent building a page asked the user for a Mapbox token instead of callinglist_public_tokens_tool. - The
render_map_toolmap view ignorespostMessageevents that don't come from the host window (#308). The check was lost when the directions preview was folded into the shared map app, so another frame on the page could send a fake tool result. resource_reader_toolreadsmapbox://refs again (#309). Resource lookup only matched URIs declared with*, so every templated ref (mapbox://temp/,response/,inline/,compute/,place-photo/) came back "Resource not found". That broke ref reads for clients without nativeresources/read, such as Claude.ai and Claude Desktop.static_map_image_toolfalls back to 1x when a 2x image is too large to inline, and links with the caller's public token (#312). A high-density image over the 700 KB inline limit came back as amapbox://temp/resource with no image, so hosts like Codex showed nothing. The tool now requests the same map at 1x first (a 1000x800 streets map is 877 KB at 2x but 295 KB at 1x) When that's still too large, it returns amapbox://response/static-map?data=...ref instead of amapbox://temp/one (folded in from #258): the temp ref lived in one server process's memory, so on the hosted deployment a follow-up read that landed on another task found nothing. Like the largedirections_toolandisochrone_toolresponses from #257, the ref carries the request's inputs (with the 1x density when that's the image it stands for), and reading it renders the same image again with the reader's token, so any process can answer it. The Static Images URL building moved tobuildStaticMapRequestUrl.ts, shared by the tool andToolResponseResource. Its link now works: models hand the user the URL as the map (Codex linked it, then embedded it as a broken image), but it had no token and returned 401. The URL now carries the caller's default public token, looked up the same way the interactive map does, and the text asks the model to give it as a link rather than embed it: neither Claude's UI nor Codex renders images from other domains in a reply (both showed a broken image), while a link opens in the browser everywhere, terminals included. The secret or OAuth token used for the request still never appears (#201). The server's ownMAPBOX_PUBLIC_TOKENis used only on a local install, never for a hosted caller; with no public token, the text says not to link the URL.- Localized category lists are readable through MCP
resources/read(#313).mapbox://categories/{language}(e.g.mapbox://categories/ja) was documented but onlymapbox://categorieswas registered, so clients that read resources directly got "Resource not found"; only theresource_reader_toolfallback could read it. Resources can now declare auriTemplatethey also answer, and the categories resource registersmapbox://categories/{language}. Anything aftermapbox://categories/that isn't a language code (like%20jaorja?x=1) now returns "Resource not found" instead of the English list; a well-formed code the API doesn't support (likexx) still gets its English list, as the API returns. - Review follow-ups for 0.17 (#317). Fixes from review of #278, #309, #311 and #312.
render_map_tool: a render withoutbaseMapConfigno longer brings back the previous one (say, night mode) when the Satellite button switches style. The description now says traffic and satellite carry over between calls, and to re-passshowTrafficalong with the content, not on its own. It also no longer says the carousel cards show the phone (they don't since #292), and asks the model to add the phone or address itself.resource_reader_tool: an image ref (like a large static map) came back as base64 text plus a second copy instructuredContent, which goes over Claude Desktop's 1 MB tool-result cap. An image under 700 KB now comes back once as an image the host shows, and anything bigger as a short note. Amapbox://selffetch/ref now says to pass it torender_map_toolinstead of "Resource not found".static_map_image_tool: when the 1x fallback replaces a too-large @2x image, the result now says so, so the model can tell the user and suggest a smaller size for @2x. A new test decodes the large-image ref'sdata=and checks no access token is in it.docs/mcp-ui.mdnow describes the viewer and the public-token link.- Tool-selection eval:
token_embeddedis listed in both hillclimb_state.jsonmetrics, so reports show it next toref_okandno_token_ask.
- Tool-selection eval: site cases check
list_public_tokens_tool(#311). The site-building cases expectedexport_map_html_tool, which #279 reverted, so they could never pass on main. They now expect the agent to get a public token fromlist_public_tokens_tooland embed it in the page it writes, checked by a newtoken_embeddedmetric against a realistic-looking stub token. Adds asite-chatgpt-appcase (a ChatGPT Apps SDK widget with a Mapbox map) and anEVAL_CASES=<ids>filter for running a few cases cheaply. On main's tool text, Claude Opus 5.5 (18 rows) and OpenAIchat-latest(15 rows) embedded the listed token every time.
- MCP TypeScript SDK v2 (#296, part of #245). The server and the
@mapbox/mcp-server/tools,/resourcesand/utilsexports now use the v2 SDK packages (@modelcontextprotocol/serverand@modelcontextprotocol/core, 2.3.1) instead of@modelcontextprotocol/sdk1.x, and@modelcontextprotocol/ext-appsmoves from 1.x to 2.0.3. v2 is the line that gets new MCP features, such as the SDK's Skills extension support, and ext-apps 2.x requires it.- For code that imports our tools:
installTo(server)needs a v2McpServer. A v1 server from@modelcontextprotocol/sdkwon't work, so a deployment that builds its own server (the hosted server, for one) has to move to v2 in the same release. Tool and resource handlers now receive the v2 context: the access token is atctx.http.authInfo.tokeninstead ofextra.authInfo.token. - Clients: nothing changes. The server still answers the
initializehandshake and negotiates 2025-11-25 with current clients, and the MCP Apps wire protocol is the same in ext-apps 2.x, sorender_map_toolrenders as before. Adopting the 2026-07-28 protocol revision (MRTR in place of elicitation, the Sampling replacement) is separate work under #245. - No more SDK patch: the v1 SDK was patched so a tool's
structuredContentthat didn't match its output schema, or was missing, logged a warning instead of failing the call. v2 takes any Standard Schema as an output schema, solenientOutputSchemawraps each tool's Zod schema:tools/liststill publishes the real JSON Schema, and validation only warns. A tool with an output schema that returns only text getsstructuredContent: {}instead of an error, since v2 checks for it before validating.patches/is now empty. - JSON Schema 2020-12: v2 publishes tool schemas as 2020-12 itself, so
publishJsonSchema2020(server)is now a deprecated no-op, kept so existing callers don't break.toJsonSchema2020is unchanged. - Removed:
OptimizationV2Tool, which was never registered and was built on the SDK's experimental tasks API, which v2 removes. - Codemod: most of the change is
npx @modelcontextprotocol/codemod v1-to-v2 .(import paths, method-stringsetRequestHandler,RequestHandlerExtratoServerContext,extra.authInfotoctx.http?.authInfo).@modelcontextprotocol/clientand@modelcontextprotocol/nodeare dev dependencies, used only by tests and scripts. - Testing: the old patch tests called
tool.run()directly and never went through the SDK; they're replaced by tests that make a realtools/callthroughMcpServerfor a mismatched, a missing and a matchingstructuredContent, and they fail without the wrapper. Tests that built a request context by hand used the v1{ authInfo }shape, so the token never reached the code under test; they now use{ http: { authInfo } }, which also makes the map app's secret-token checks test what they say. A stdio smoke test against the built server covers the handshake, all 30 tools with 2020-12 schemas, prompts, completions, resources, the MCP Apps UI resource, logging and a tool call.
- For code that imports our tools:
list_public_tokens_tool: a public token for pages built outside the chat (#304, part of #281). An agent building a site or app with a Mapbox map had to ask the user for a token. The tool lists the account's public (pk.) tokens with their names, default flag, URL restrictions and scopes, and recommends one.- Safer token first: pass
site_urland it recommends a token restricted to that URL when one exists. Otherwise it recommends the default public token and says a published site should use a URL-restricted one, with the link to create it. - Public tokens only: it asks the Tokens API for
pktokens and drops anything else, so secret and temporary tokens never come back. It can't create tokens (that needstokens:write; DevKit'screate_token_tooldoes it). - No new scope: it needs
tokens:read, which the in-chat map already uses to find the default public token. With apk.access token, or when listing fails on the server's ownMAPBOX_ACCESS_TOKENandMAPBOX_PUBLIC_TOKENis set, it returns that token. A caller with its own bearer token never gets the server'sMAPBOX_PUBLIC_TOKEN, and a token restricted to other URLs is never recommended. - Server instructions now tell the model to use it instead of asking the user for a token when it builds a page outside the chat.
- Safer token first: pass
feedback_tool: report wrong map, place or routing data to Mapbox (#291). When someone says a place has closed, a road is closed, or a route took an illegal turn, the agent can send it to the Feedback API (POST /user-feedback/v1/feedback, Public Preview) with the text, a category and the location, usually taken from an earlier search or directions result. It returns the report's id and status (received).- Confirm first: the report goes to Mapbox under the user's account, so the description tells the model to show the user what it will send and get a yes. Clients that support elicitation also get a confirmation prompt from the tool itself. Declining sends nothing, and so does a failed prompt.
- Categories: an enum of the Feedback Agent's default categories that make sense from chat (
poi_closed,poi_details,road_closure_issue,illegal_turnand so on), leaving out the navigation-app ones such as voice guidance and rerouting. - No duplicates on retry: the HTTP pipeline retries 5xx and 429 responses, POSTs included, so the tool picks the item
iditself and a retry resends the same item. If the report goes through but the response body is unexpected, the tool still reports success with the id it sent, so the model doesn't send it again. - No paying twice for one report: once the API leaves Public Preview, each report costs $0.02, billed to the account that sends it. Before sending, the tool lists the account's own reports from the last 30 days in the same category that are still
received, and if one is within 50 m it returns that one (withduplicate: true) instead of sending. The list endpoint has no location filter, so distances are compared in the tool. The API only lists the account's own reports, so this catches the same user reporting the same thing again, not reports from other accounts.send_anyway: trueskips the check when the user says theirs is a different problem. The check runs before the confirmation prompt, so nobody is asked to confirm a report that won't be sent. - Token scope: it needs
user-feedback:writeto send, anduser-feedback:readfor the duplicate check. On a 401 or 403 when sending, the error says so. Without the read scope, or if the list call fails, the report is sent unchecked and the result says why.--disable-tools feedback_toolleaves it out. - Annotated
readOnlyHint: false,destructiveHint: false,idempotentHint: false. The location goes as top-levellat/lon; the optional GeoJSONfeatureandscreenshotfields aren't sent yet, since the docs givefeaturecoordinates as[latitude, longitude]and that needs checking first.
directions_tool:geometries="geojson"now always returns coordinates directly, regardless of trip length (#255). Previously, every geojson request usedoverview=full(maximum-precision geometry) — fine for a short trip, but for a long one (reported: London to a point in Scotland) the response exceeded the 50KB context limit and the tool silently swapped to a completely different contract: geometry and legs stripped out, replaced by amapbox://temp/...resource URI (amapbox://response/...ref since #257) that has to be read through the MCP resources API rather than being usable directly — not even a fetchable HTTP URL. An agent that asked for coordinates because it needed to draw the route itself (e.g. in a generated app) had no way to know in advance which contract it would get, since the split was driven by trip length, not by anything the caller controlled.- New optional
overviewinput ("full" | "simplified") now defaults to"simplified"whenevergeometries="geojson"(and to"full", unchanged, whenevergeometries="none", since no geometry is returned there either way). A simplified route is still a fully accurate, drawable line — confirmed live against the real Directions API, a real London→Edinburgh route (~420 miles) drops from 10,057 coordinate pairs / ~700KB down to 46 coordinate pairs / ~4.7KB, comfortably inside the response limit.overview="full"is still available for callers who explicitly need maximum-precision geometry, at the cost of the same large-response behavior as before (now a deliberate, documented opt-in rather than a surprise past a size cliff). - Tradeoff: the Mapbox Directions API rejects the
congestionannotation unlessoverview=full(422: Overview option must be full for congestion, confirmed live) —distance/speedannotations have no such restriction and stay accurate at any overview level. Socongestion_information(the per-segment traffic breakdown) is only present when the effectiveoverviewis"full"; it's correctly omitted (not reported as a misleading all-zero breakdown, which is what the code did previously whenever congestion happened to be unavailable) when it wasn't requested.average_speed_kph,distance,duration, and turn-by-turn summaries are unaffected at any overview level. render_map_tool's live map preview is unaffected either way — it already always fetches its own full-detail geometry directly from the Directions API client-side (mapbox://selffetch/directions...), independent of whatdirections_tool's own response contains.- Matches the
overviewparameteroptimization_toolalready exposes (also defaulting to"simplified"), which never needed a large-response fallback in the first place. directionsRequestFromRef(#276) now always asks foroverview=full, so apps that draw a route from the ref still get every vertex when the original call took the new simplified default.
- New optional
directions_tool: turn-by-turninstructionsnow actually populate (#256). The tool has always requestedsteps=trueand documented its response as including "turn-by-turn instructions," but the extraction code read announcements fromstep.voiceInstructions, which the Directions API only returns when the separatevoice_instructions=trueparameter is set — never sent by this tool. Soinstructionswas silently empty for every call, regardless of route length. Switched tostep.maneuver.instruction(e.g."Bear right onto Great Portland Street/A4201. Continue on A4201."), a human-readable instruction already present on every step wheneversteps=trueis set, no extra parameter required. Also raised the output cap from 10 to 200: the old cap was sized for the old (broken) source, which could emit several announcements per turn;maneuver.instructionis one entry per real turn, so a normal route — even a long one with dozens of turns — stays well under the new cap. Confirmed live against the real Directions API.static_map_image_tooland place photos: URLs with several trailing dots are now checked like the plain hostname. The URL check stripped only one trailing root-label dot, sohttps://localhost../x.pngwas compared aslocalhost.and passed, as didapp.localhost.., dotless hosts such asintranet.., and IP literals such as127.0.0.1..and169.254.169.254... It now strips them all. This covers custom-marker URLs instatic_map_image_tooland photo URLs inPlacePhotoResource. Public hosts with a trailing dot (example.com.) are still accepted.
- MCP TypeScript SDK 2.4.0 (#303). Bumps
@modelcontextprotocol/server,/coreand/clientfrom 2.3.1 to 2.4.0 and/nodeto 2.1.2. 2.4.0 adds the SDK's Skills extension (@modelcontextprotocol/server/ext/skills, SEP-2640), which #295 can use instead of its ownskills/listandskills/gethandlers. Its patch changes don't affect our tools: none of our schemas use$id, a$refroot orx-mcp-header. - Removed build output committed under
src/(#297).src/utils/versionUtils-cjs.cjs,.d.ctsand their source maps were compiled output from atscrun that wrote next to the sources, committed back in #96. OnlyversionUtils-cjs.ctsis source: tshy swaps it in forversionUtils.tsin the CommonJS build..gitignorenow keeps.js,.cjs,.mjs,.d.*tsand.mapfiles out ofsrc/andtest/. - Tests are typechecked in CI (#298).
tsconfig.test.jsoncoverssrc/andtest/, but nothing ran it, so test type errors piled up: about 200 on main, plus about 250 more from SDK v2 typingstructuredContentas an open object (#296). They're all fixed, andnpm run typechecknow runs in CI before the tests.tsconfig.test.jsonalso getsnoEmit, since running it without that writes.jsfiles next to the sources (see #297).- New helpers in
test/utils/resultTestUtils.ts:structuredOf<ToolOutput>(result)reads structured content as the tool's own output type, so a test that checks a field the schema doesn't have fails to compile.textOfandresourceTextOfread text content and throw on the wrong content type. - Tests that were wrong: the tuple check in
input-schema-validation.test.tsnever looked inside arrays under zod 4 (ZodArray._def.typeis now the string'array'), so a tuple nested in an array would have passed. It uses zod 4's accessors now and covers that case.structured-content.test.tsimported a file that never existed.cleanResponseData.test.tsused an old directions input shape and now builds it withDirectionsInputSchema.parse. A|| Infinityfallback in a SimplifyTool test meant one comparison could never fail.
- New helpers in
-
render_map_tool: a results panel for search results, with popularity from Place Details (#262). When a map shows places fromsearch_and_geocode_tool,category_search_toolorground_location_tool, a list docked at the top right shows one row per pin (name, category, distance), numbered to match the pins. Clicking a row flies to that pin. The page then makes one batchedPOSTto the Places API's/details/retrieve(up to 100 ids) and adds each place's popularity score to its row and its phone number to its popup. It runs client-side on the public token, like the other self-fetched layers, and if the call fails, the rows stay as they were.- Inline markers: Claude usually builds the map from its own
markersrather thanpayload_refs, somarkers[]now takes optionalid(the POI'smapbox_id),name,categoryanddistanceMeters. A marker withidgets the same row and enrichment. Markers withidand nolabelare numbered to match their row. - Ids the API rejects: the batch endpoint rejects the whole request if any id isn't a native Places id (
urn:mbxpoi:<uuid>). Search Box also returns OSM-sourced ids (urn:mbxpoi-osm:) and, near Union Square in San Francisco,urn:mbxpoi:mapbox-n<id>. The page sends only UUID-form ids, and if the API still rejects one it names, drops it and retries once, so one bad id doesn't stop the rest from being enriched. - Photos through the server: Place Details photos are hosted wherever each business's site hosts them (e.g.
dyn.mktgcdn.com,cdn.prod.website-files.com), so the map's CSP can't list their hosts, and allowing anyhttps:image host would also allow scripts from anywhere. A new resource,mapbox://place-photo/{mapbox_id}, looks the place up on the server and returns its first photo as base64, and the page shows it as adata:URL. The resource only takes an id, never a URL, and only fetches what Place Details returns for it: https on a public hostname that resolves only to public addresses, no redirects, JPEG/PNG/WebP/GIF only, up to 1 MB, with a 5 s timeout. Results are cached by id for 30 minutes, misses included. The cache is shared by everyone on the server process, so it is capped at 20 MB as well as 200 entries. Each photo costs one more single-place Place Details lookup on the server, only for rows whose place has a photo. A row gets a thumbnail only when a photo loads, for up to 10 rows per map. A page with no MCP host or CSP loads the photo URL directly. Hosts withoutresources/read, such as ChatGPT, show no photos; the page stops waiting after 15 s. - One panel per render: a render can draw inline markers and then merge in fetched search results. Both join one numbered list, a place in both is listed once, and each place is looked up once. The first set keeps the caller's own numbers when they're unique (a visit order, say), and later sets continue from there. A letter label such as
Astays on its pin and shows in its row. A Place Details response from an earlier render is ignored, so it can't write into a newer panel. - Layout: while the panel is open, the map fits its data into the space beside it. Clicking the panel's heading collapses it to a single line and refits, and on a map narrower than 560 px it starts collapsed.
- Testing: added tests for the panel and enrichment, including id filtering, numbering, inline markers, and photos both through the server and loaded directly on a standalone page. The photo resource has tests for its URL and content checks, size cap, caching and failures. The test sandbox's fake DOM gained
appendChild/removeChild/addEventListener, and its fetch mock now passes the requestinitthrough, so thePOSTbody can be checked.
- Inline markers: Claude usually builds the map from its own
-
directionsRequestFromRefin@mapbox/mcp-server/utils, for apps that keep long routes out of the model's context (#276). Everydirections_toolresult carries a short ref instructuredContent.mapboxRender.refthat holds only the call's inputs.directionsRequestFromRef(ref, { accessToken })turns it back into the Directions API URL for the same route withgeometries=geojson, plus the index of the route the user picked among alternatives. An app can give the model the ref instead of the route and fetch the geometry itself, the wayrender_map_tooldoes, with no server-side state. Also exportsresolveSelfFetchRef,isSelfFetchRef,buildDirectionsRequestUrl(the tool's own URL builder, so an app's request stays in sync with it) and theMapAppPayload/MapAppSelfFetch/DirectionsRequestInputtypes. Seedocs/importing-tools.md.
-
directions_toolandisochrone_tool: large responses return a short ref that works on the hosted server (#257). Above 50 KB, both tools stored the full response in process memory and returned amapbox://temp/ref. The hosted deployment runs several stateless tasks with no session stickiness, so a follow-upresources/readthat landed on another task got "Resource not found or expired" (most visible on long routes such as London to Edinburgh withgeometries="geojson"). They now return amapbox://response/{tool}?data=...ref that carries the call's inputs, not its response, so it stays a few hundred bytes and the summary the model sees stays small. Reading it (newToolResponseResource) validates the inputs against the tool's schema and runs the request again with the reader's token: Directions responses are cleaned the same way and keep the route the user picked among alternatives. Any process can answer it, so there is no store, TTL or owner check. The tradeoff is a second API request on read, which with live traffic can differ slightly from the original. A two-process integration test makes the call in one spawned server, kills it, and reads the ref from another.static_map_image_tool's large-image fallback still usesmapbox://temp/. -
Tool descriptions no longer steer Claude away from other tools and sources, per the Claude connector directory review (#273).
render_map_tooldropped "PREFERRED MAP TOOL", the instruction never to use built-in map tools (which namedplaces_map_display_v0), and "Invoke this as the FINAL step", which could produce maps nobody asked for. It now says it is for when someone wants to see a location, route, or dataset on a map.ground_location_tooldropped "FIRST and ONLY" and "do NOT … search the web for places"; it now says it already includes reverse geocoding, so a separatereverse_geocode_toolcall is not needed for the same coordinate.category_list_tool(deprecated) no longer tells Claude to page through every category before doing anything else, in either the tool or thelimitparameter description; it now just says it supports limit/offset pagination. The map hint that data tools append to their results now reads "If the user wants to see this on a map, pass this ref unchanged to render_map_tool" instead of telling Claude to show every result on a map. Tool names, schemas, and behavior are unchanged. The server instructions still route map requests torender_map_tool.
- Tool-selection eval for catching routing regressions from tool descriptions (#274).
npm run eval:toolsruns 19 prompts against Claude (Opus 5.5 by default) and grades which tools it calls against the expected ones: map only when asked,static_map_image_toolonly for image files,ground_location_toolvsreverse_geocode_tool, brand vs category search, and whether web search stays available for place questions. The tool list and server instructions come from a realMcpServerbuilt the way the hosted deployment builds it and read back over MCP. Each prompt runs with all descriptions loaded and again with the tools behind the API's tool search, which approximates hosts like Claude Desktop that load tools on demand. Tool results are canned per case, so Mapbox APIs are never called. A grader check also confirms that everyrender_map_toolcall passes the map ref unchanged, with a distinct ref per tool result so an invented or hand-built map is caught. Two site-building cases give the agent awrite_filehost tool and check that it neither asks the user for a Mapbox token nor leaves a placeholder token in the page; they expectexport_map_html_tool(#275) and fail on servers without it.EVAL_SERVER_ROOTpoints the harness at another checkout, so two branches can be compared, andnpm run eval:tools:reportbuilds an HTML report with per-case transcripts. It calls the Claude API and costs about $8 per 114-row run, so it is not part ofnpm test.EVAL_PROVIDER=openai(npm run eval:tools:openai -- --model <id>) runs the same cases against an OpenAI model through the Responses API, including OpenAI's server-side tool search for the deferred mode, so description changes can be checked for ChatGPT-style regressions too. Adds@anthropic-ai/sdk,openaiandtsxas dev dependencies. Seeevals/tool-selection/README.md.
@mapbox/mcp-server/utilsnow exports the server setup helpers, so servers built from the subpath exports get the #268 and #269 fixes (#271).publishJsonSchema2020,toJsonSchema2020, andbuildServerInstructionswere only used by this package's own entry point (src/index.ts). Anything that builds its ownMcpServerfrom@mapbox/mcp-server/toolsand installs the tools itself, which is how the hosted server atmcp.mapbox.comworks, still published draft-07 schemas (rejected by Claude Desktop) and sent no server instructions (so models that load tools lazily keep pickingstatic_map_image_tooloverrender_map_tool). Upgrading to 0.15.0 alone fixed neither. Such a server can now callpublishJsonSchema2020(server)before installing tools and passinstructions: buildServerInstructions(<enabled tool names>)when creating the server. The custom-server example indocs/importing-tools.mdcalled.definitionand.execute(), which the tools don't have; it now usesgetAllTools()/getAllResources(),installTo(), and both helpers. Added export tests, including one that builds an externalMcpServerfrom the subpath exports and checks that it lists 2020-12 schemas and sends the instructions. Checked that the built package exposes all three helpers under both ESM and CommonJS.
-
Temporary resource ownership is now keyed on the caller's token, not on the unverified username claim inside it.
TemporaryDataResource.read()gated access to cached large tool responses (route geometry, isochrone GeoJSON, static-map images,render_map_toolpayload refs) by comparing the resource's recorded owner togetUserNameFromToken(callerToken). That helper base64-decodes the JWT payload and returns theufield with no signature verification — and none is possible here, since Mapbox signs its tokens with a secret this server does not hold. The entire authorization decision therefore rested on a claim the caller controls: anyone who knew a resource URI and the owner's Mapbox username could fabricate a token asserting that username and read the cached data. Unlike a tool invocation, which hands the token to a live Mapbox API call that rejects a fake, the resource read never made that call, so nothing downstream caught it.Ownership is now a SHA-256 fingerprint of the token bytes (
getOwnerKeyFromToken), compared in constant time (ownerKeyMatches). Forging the check now requires already holding the victim's real token — at which point the attacker can call the Mapbox API directly and gains nothing from this path. This deliberately avoids the two obvious alternatives: local signature verification is impossible, and token introspection against Mapbox's auth backend would add a network round-trip to every resource read, which is the exact latency the temporary-resource cache exists to avoid, plus a new failure mode when that endpoint is unavailable. The check also no longer assumes anything about the transport — a deployer wiring a caller-supplied bearer token intoextra.authInfo.token, the pattern the MCP TypeScript SDK documents (where signature verification is the deployer's opt-in responsibility), is protected regardless.The same fix is applied to
resolveMapPayloadRef(), which carries its own copy of the ownership check because it reads the resource manager directly rather than going throughresources/read.Behavior change: ownership is now per-token rather than per-account. A caller that rotates to a different access token mid-session can no longer read temporary resources it created under the previous token; those reads fail closed and surface as the normal "not found or expired" response. With a 30-minute TTL this window is narrow, and failing closed is the correct direction for an authorization check. Callers persisting a
TemporaryResource.ownervalue across versions should treat it as an opaque key — it is no longer a username, and a username must never be stored or compared there.The account scoping this replaces was added in #205; its regression tests built every token — including the legitimate owner's — with the same hardcoded placeholder signature, so the suite could not distinguish a real token from a forged one and never exercised same-username forgery. Added regression tests covering forged-username reads on the JSON path, the image-blob path, and the map-payload-ref path, plus assertions that two tokens claiming the same username yield different ownership keys and that a username is never stored as an ownership key.
-
Map app CSP now names exact hosts instead of allowing all of
*.mapbox.com(#265).MapAppUIResourceandrender_map_tooldeclaredconnectDomains: ['https://*.mapbox.com', 'https://events.mapbox.com'], a wildcard over every Mapbox subdomain (which also made theevents.mapbox.comentry redundant). MCP Apps hosts compare the declared CSP against what the UI actually loads, and OpenAI's app guidelines ask for "the exact domains" and allowlists "as narrow as possible". Both now come from onemapAppCsp(apiEndpoint)helper, so the resource and the tool can't drift apart:api.mapbox.comandevents.mapbox.comfor connections, plus the origin ofMAPBOX_API_ENDPOINTwhen it points somewhere else (the page's Directions, Isochrone, Search Box and other calls go there);api.mapbox.comfor resources;blob:workers.BaseTool'smeta.ui.csptype gainsworkerDomainsso the tool can declare the same policy as the resource.Checked in headless Chrome with the new policy enforced as a
Content-Security-Policyheader: the map loaded onstandard, rendered a payload with a line layer, a circle layer, markers andlightPreset: "dusk", then switched tostandard-satellite. Every request went toapi.mapbox.com(103 requests, including tiles, 3D models and imagery) orevents.mapbox.com, with no CSP violations or page errors. Added tests for the helper, the resource's and the tool's declared CSP, and a custom endpoint.
-
Map display now goes to
render_map_tooleven in hosts that load tools lazily (#269). In Claude Desktop, after a successfulisochrone_toolcall, the model loadedstatic_map_image_toolto "show a map" and never loadedrender_map_tool. Claude Desktop lists only tool names until the model loads a tool, sorender_map_tool's description ("ALWAYS use this tool to display maps") was never in view when it chose. The server now returns MCPinstructionsfrominitialize, which hosts show up front: userender_map_toolto show anything on a map, passmapboxRender.refvalues unchanged and never write one by hand, and usestatic_map_image_toolonly when the user explicitly asks for an image file. The instructions name only enabled tools, and are omitted whenrender_map_toolis disabled.static_map_image_tool's description now carries the same redirect. The hint appended to tool results with amapboxRender.refnow namesrender_map_tooloverstatic_map_image_tooland says to pass the ref unchanged. That also addresses the hand-written ref behind the empty-globe isochrone in ChatGPT (#267). Checked the instructions over stdio against the built server, and added tests for the instruction text with all tools, withoutstatic_map_image_tool, and withoutrender_map_tool. -
Tool schemas are now published as JSON Schema 2020-12, so Claude Desktop can call the tools again (#268). Claude Desktop rejected calls to every tool that declares an output schema with "Tool 'search_and_geocode_tool' has an invalid outputSchema: JSON Schema declares an unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#\"). The default validator supports JSON Schema 2020-12 only". The MCP SDK's
McpServer(1.30.0 here, and still in 1.32.1, the latest on npm) generates every tool'sinputSchemaandoutputSchemaas draft-07, with no option to change it, while MCP specifies 2020-12 as the default dialect. Changing only the$schemalabel would not have been enough: 30 schema nodes are tuples (bbox, coordinate pairs), which draft-07 writes asitems: [...], a form 2020-12 does not allow.publishJsonSchema2020()(src/utils/jsonSchema2020.ts) wrapssetRequestHandleron our own server instance before any tool is registered, so the SDK'stools/listhandler is installed wrapped. Its schemas are converted on the way out: the label becomes 2020-12, tupleitemsbecomesprefixItems(withadditionalItemsbecomingitems), anddefinitionsbecomes$defswith refs rewritten to match. The conversion walks only real subschema positions, so a property nameditemsordefinitionsis left alone. It isn't a global patch, and the SDK's own validation of tool input and output still runs against the zod schemas as before.Checked against the built server over stdio: all 29 tools list with the 2020-12 label, all 57 schemas compile under Ajv's 2020-12 validator, and a live
search_and_geocode_toolresult validates against its converted output schema. In Ajv's strict mode, which isn't the default, converted tuples still raise length warnings; zod's own 2020-12 output does the same. Added tests for the converter, for every registered tool's listed schemas, for the SDK's unwrapped draft-07 output (so the wrapper can be removed once the SDK emits 2020-12 itself), and for a tool call whose structured output includes a tuple. -
render_map_tool: a malformed isochrone ref no longer "renders" as an empty globe that the model reports as a working map (#267). Caught live in the ChatGPT app: asking for a 15-minute isochrone around an address drew a zoomed-out globe with the error "Could not fetch isochrone: You must supply one of contours_meters or contours_minutes", while the model told the user "the map above shows the 15-minute driving isochrone".isochrone_toolalways writescontours_minutes/contours_metersinto itsmapbox://selffetch/isochroneref (checked against the hosted server), so the ref the map got was most likely one the model wrote by hand.resolveSelfFetchRef()only checked that the ref decoded to JSON, so a ref with no contours, a scalar (contours_minutes: 15), or the wrong key name still resolved.render_map_toolthen reported success, and the iframe's Isochrone request quietly left out the bad field and got a 422 back. Isochrone refs now resolve only when they have numeric coordinates and a non-empty numericcontours_minutesorcontours_metersarray. A ref that fails goes down the existing unresolved-refs path, which now says a ref may be malformed and tells the model to re-run the source tool and pass itsmapboxRender.refverbatim. The iframe'sisSafeIsochroneParamsdoes the same check, so a bad ref fails before any network call.Running the built server over stdio showed that rejecting the ref alone wasn't enough. The screenshot's call also passed a
summary, and a summary by itself counted as renderable, so a call whose only ref failed still returnedisError: falsewith "Rendered map with 0 layers" and a parenthetical note. That would have drawn an empty globe under the summary.render_map_toolnow returns an error whenever a ref failed to resolve and no layers, markers, or self-fetched layers are left to draw. Calls with no refs that only setbaseMapConfigorbaseStyleare unaffected. Two existing tests used an isochrone fixture withcoordinatesas an array, a shapeisochrone_toolnever produces; they now use the real{longitude, latitude}shape. Added regression tests that fail without the fix. -
render_map_tool:baseStyle(e.g."standard-satellite") was silently ignored on every real call.baseStylewas only ever applied at map-construction time inmapAppHtml.ts'sinitMap(), seeded from a server-embedded#initial-datascript tag — butMapAppUIResourcenever actually passesinitialDatawhen serving that HTML, so the tag is always empty and the map always constructs with the default"standard"style. The real payload (withbaseStyle) arrives afterward via theui/notifications/tool-result→render(payload)path, which only ever handledbaseMapConfig(color/theme properties viasetConfigProperty), neverbaseStyleitself. Caught live: asking for a"standard-satellite"render produced a map correctly labeled "satellite view" that was actually still the standard vector style.render()now detects abaseStylechange from what the map is currently on and switches viamap.setStyle(...), deferring the rest of the render untilstyle.loadfires (matching how a style is normally swapped in GL JS, since it replaces sources/layers). Added regression tests reproducing the original silent-ignore bug (confirmed to fail without the fix) and covering the no-op case when a render repeats the already-active style.
-
ground_location_toolnow returnsmapbox_idandexternal_idson each nearby POI. The tool runs a category search internally, but its POI mapper hand-picked six properties (name,address,longitude,latitude,category,distance_meters) and dropped the rest, and its localCategorySearchFeatureinterface didn't declare the others either — even thoughCategorySearchTool.output.schema.tshas always modelledmapbox_id(required) andexternal_ids(optional) on the same API response. Two consequences, both for the model reading the tool result: there was no way to follow up withplace_details_tool(which takes amapbox_id) to get ratings, popularity, opening hours, or a phone number for a placeground_location_toolhad just surfaced — the documented enrichment path was a dead end from this tool's output — and no way to attribute a record to the upstream supplier behind it (dataplor,tripadvisor). Both fields now thread through tonearby_pois, with.describe()text pointing at theplace_details_toolfollow-up so the model knows what the id is for.Both stay optional in
PoiSchema, since a feature can come back without either, and neither is added to the human-readable text output — they are identifiers, not prose. Note that a returnedmapbox_idis not guaranteed to work against the Places/details/retrieveendpoint: some ids decode to the OpenStreetMap-sourcedurn:mbxpoi-osm:scheme, which that endpoint rejects. -
search_and_geocode_tool: description now mentions Search Box API's natural-language query support. Mapbox is rolling out NL understanding on the underlying/forwardendpoint (category + place + attribute phrasing, e.g. "quiet coffee shops with wifi near downtown"), verified working well in our own testing. The tool's description andqfield now mention this so callers know richer free-text phrasing is worth trying, with a caveat that big-box retail brand + address queries (Costco, Walmart, Target) are currently unreliable and return unrelated results — verified via a 202-query test set against the live API, after first ruling out a CDN-caching artifact in our own test methodology that had produced misleading results.
-
search_and_geocode_tool:q's max length corrected from 256 to 200 characters. The Search Box API docs state a 256-character limit, but the live API actually rejects anything over 200 with{"message":"Query exceeded character limit of 200"}(confirmed directly against the API: 200 chars succeeds, 201 fails). Our schema previously allowed up to 256, so a caller sending 201-256 characters would pass our validation and then fail with an opaque 400 from the API.q's.max()and description updated to match actual API behavior. -
render_map_tool: support Mapbox Standard Satellite as a base style. NewbaseStyle: "standard" | "standard-satellite"field (defaults to"standard", preserving prior behavior) switches the base map to Mapbox Standard Satellite — the same Standard style family (dynamic labels,bottom/middle/topslots, mostbaseMapConfigproperties) rendered over global satellite imagery instead of a vector basemap. Requested via Slack. Applied at map-construction time, same asbaseStyle's siblingbaseMapConfig, and validated against an allowlist client-side so an unrecognized value can never reach the style URL. Verified live in a real browser against the Mapbox API. Seedocs/render-map-tool.mdfor the property-compatibility notes and an example. -
The server now identifies which MCP client connected to it.
server.server.getClientVersion()(populated from theclientInfosent in the client'sinitializerequest) is now logged on connect, e.g.Client identified as: claude-ai v1.0.0— useful for support/debugging when behavior differs across Claude Desktop, Cursor, VS Code, etc. Reading it required moving the read to aserver.server.oninitializedcallback rather than right afterserver.connect(), sincegetClientVersion()/getClientCapabilities()are only populated once the client'sinitializerequest has actually been processed, which is not guaranteed by the timeconnect()'s promise resolves (it only waits for the transport to start). Confirmed live that reading capabilities immediately afterconnect()reliably returnedundefinedeven for a client that declared them; moving both reads intooninitializedfixed the same-shaped bug in the existing capability-gated tool registration (currently dormant, since no tool is registered through that path yet, but was silently broken for whenever one is added). The client name/version is also recorded asmcp.client.name/mcp.client.versionon every subsequent tool-execution trace span, so OTel-backed traces can be filtered or grouped by client. -
render_map_tool: restyle the base map itself viabaseMapConfig, and place custom layers withslot. PreviouslyMapAppPayload/RenderMapInputSchemaonly supported adding new GeoJSON layers on top of a fixed base map.baseMapConfigexposes Mapbox Standard's config-property system (map.setConfigProperty('basemap', ...)) — e.g.{ "colorWater": "#ff0000" }turns the water red, or{ "lightPreset": "night" }switches to night lighting — covering colors,theme,lightPreset, and label/3D-object visibility toggles. Applied at map-construction time (via theconfigoption, when present in the initial payload) to avoid a flash of default colors, and viasetConfigPropertyon later re-renders.layers[].slot("bottom" | "middle" | "top") places a custom layer relative to Standard's own layers instead of always rendering above everything, including labels (still the default whenslotis omitted). Both verified live in a real browser against the Mapbox API. Seedocs/render-map-tool.mdfor the full property list and an example. Filed as #249 after a user asked whetherrender_map_toolcould turn the water red on the fly.
place_details_toolnow calls the Mapbox Places API instead of the older Details API. The tool previously calledsearch/details/v1/retrieve(docs.mapbox.com/api/search/details/); it now callsplaces/v1/details/retrieve(docs.mapbox.com/api/search/places/), a separate, newer product built around a larger POI dataset. This is not a compatible upgrade:- Input:
attribute_sets,language, andworldvieware removed from the primary input schema — the Places API's Details endpoint takes only amapbox_idand has no equivalent parameters. They're added back as fallback-only parameters (see below), so existing callers still validate, but they now only take effect for boundary/administrative lookups. - Output: the response is no longer a GeoJSON
Feature. It's a flat object (name,full_address,phone,website,categories,opening_hoursas a plain OSM-format string,coordinates: { latitude, longitude },score: { popularity, reality, closed },address,attributes,photos,building) — seePlaceDetailsTool.output.schema.ts. Callers readingproperties.*orgeometry.coordinatesfrom the old shape need to update to the new field names. - Lost fields: the old API's
rating/review_count/price(user rating and review count) have no equivalent in the new API for POI lookups.score.popularity/score.realityare data-quality/confidence signals, not user ratings, and are preserved in the formatted text output as "Popularity: N%". - The Places API is Public Preview: its default quota is 1,000 records/month per account and 100 records/sec, and its response contract may change without notice. The output schema is deliberately permissive (
.passthrough()throughout, most fields optional) to avoid the class of output-validation failure fixed in 0.14.0 if the API adds or omits fields. The tool's description now surfaces the 1,000/month quota so callers know to contact Mapbox for higher volume. - Boundary/administrative lookups fall back to the legacy Details API. The Places API only covers points of interest and rejects
mapbox_ids for neighborhoods, cities, or other administrative boundaries with a422 Invalid mapbox_id formaterror (confirmed live against the API). Per Places team feedback, that's a real capability gap — the legacy API remains the only way to resolve those IDs, and the only source of enhanced Japan data — so rather than losing it,place_details_toolnow retries against the legacy Details API on that specific 422 and normalizes its response onto the same flat output shape (addingfeature_type,bbox,context, andmetadatafields, populated only via this fallback).attribute_sets/language/worldvieware forwarded to the fallback request when provided. POI lookups are unaffected — they resolve via the Places API in a single request, same as before.
- Input:
-
Tool calls are now bound to the MCP server that received them.
BaseTooltracked its target server in a single mutable instance field set byinstallTo(), and the callback it registered read that field at call time rather than capturing the server it was registered on. Because the pre-configured instances exported from@mapbox/mcp-server/toolsare module-level singletons, an application that installed one instance into more than oneMcpServerwould have the laterinstallTo()silently redirect the earlier server's callbacks: logging, sampling (ground_location_tool), and elicitations (search_and_geocode_tool,directions_tool) would all be sent to whichever server was installed last. Each invocation now resolves the server that registered its callback, viaAsyncLocalStorage, so concurrent calls arriving through different servers stay on their own. Single-server applications — including the server shipped by this package — behave exactly as before. Callingrun()directly, outside a registered callback, still falls back to the most recently installed server.BaseResourcecarried the same pattern and got the same treatment. Its only reader waslog(), so the practical effect there was misdirected log messages rather than misdirected client interaction, but the resource instances exported from@mapbox/mcp-server/resourcesare module-level singletons for the same reason and the shared field was the same hazard. -
render_map_tool's base map style switched fromstreets-v12to Mapbox Standard.streets-v12was never a deliberate choice — it was inherited fromStaticMapImageTool's pre-existing default when the interactive GL JS map app was built, and never revisited. Mapbox's own docs now default new GL JS maps to Standard ("a versatile and visually appealing map style suitable for many applications"), and this org's own agent-skills guidance has moved the same direction for new integrations.MapAppPayload's custom layers/markers are unaffected: they're added without aslot, which (per the Style Spec) places them above all imported layers, matching their prior position understreets-v12. Verified visually against a route line, a filled polygon, and markers rendered together on the new style.StaticMapImageTool(the separate Static Images API tool) is untouched — that API can't render Standard.
- Bumped
@modelcontextprotocol/sdkto1.30.0. Not adopting the2026-07-28spec revision this release covers (stateless request/response model, elicitation replaced by Multi Round-Trip Requests, Sampling deprecated) — that's a separate migration tracked in #245, sincedirections_tool/search_and_geocode_tool's elicitation-based selection andground_location_tool's sampling-based strategy detection all depend on the mechanisms being replaced. Regeneratedpatches/@modelcontextprotocol+sdk+1.30.0.patch(previously pinned to1.29.0) — same patch content, applies cleanly to the new version, verified live against the built server.
directions_tool— route selection elicitation. When the Directions API returns two or more route alternatives, the tool now asks the user to pick one viaserver.elicitInput(...)(MCP elicitations), presenting each option's duration, distance, primary roads, traffic conditions, and incident count. Only the selected route is returned, and the choice is threaded through to the map preview's self-fetch so re-rendering shows the same route rather than defaulting back to the first one. If the client doesn't support elicitations, the user declines, or the call errors, the tool falls back to returning all route alternatives, matching prior behavior.- Corrected
docs/elicitations.md, which had described a two-stagedirections_toolelicitation flow (routing preferences before the API call, plus automatic client-capability detection) that was never implemented. The doc now accurately describes the single-stage route-selection elicitation and the actual fallback mechanism (atry/catcharound eachelicitInputcall, with no capability pre-check).
place_details_tool: fixed output validation failure whenattribute_setsomitted"basic". The tool's output schema requiresproperties.nameandproperties.feature_type, both of which are only populated by the Details API'sbasicattribute set — so a call likeattribute_sets: ["visit"]was accepted at the input stage but then failed with an opaqueOutput validation erroronce the API's response came back without those fields."basic"is now always merged into the outgoingattribute_setsrequest regardless of what the caller passes, matching what the tool's own description already claimed ("always included").
- Removed dead MCP-UI code.
--disable-mcp-uino longer appears in--help(it stopped doing anything once MCP-UI support was removed) but is still silently accepted so an existing launch config that passes it doesn't hard-fail on "Unknown option". Deleted the orphanedStaticMapUIResource(ui://mapbox/static-map/index.html) — it was registered but no tool had referenced it sincestatic_map_image_toolstopped declaring an MCP Apps UI resource. No behavior change: MCP-UI support was already fully gone from the codebase; this just removes the code that referenced it.
- README and
render_map_tooldocs refreshed. The README's "Rich Map Previews" section anddocs/mcp-ui.mddescribed MCP-UI support (@mcp-ui/server,ENABLE_MCP_UI,StaticMapUIResourcewired tostatic_map_image_tool) that was fully removed whenrender_map_toolshipped —@mcp-ui/serveris no longer a dependency and nothing insrc/readsENABLE_MCP_UIanymore.docs/mcp-ui.mdnow explains what changed and points to the newdocs/render-map-tool.md, a comprehensive guide covering the full payload schema and, in particular, how to callrender_map_toolstandalone with your own GeoJSON — no other Mapbox tool required. Also added the ~12 tools missing from the README's tool inventory (render_map_tool,ground_location_tool,place_details_tool,destination_tool,union_tool/intersect_tool/difference_tool,convex_tool,nearest_point_tool/nearest_point_on_line_tool,length_tool) and replaced the entry for the removedpoint_in_polygon_toolwith its actual replacement,points_within_polygon_tool.
- directions_tool: fixed query parameter injection via the
excludeparameter. Apoint(<lng> <lat>)exclude entry was validated by splitting on spaces and reading only the first two tokens — any extra content after them (e.g.point(0 0 &injected=evil)) was never inspected or rejected. That value then reached the outbound Mapbox Directions API request through a hand-rolled encoder that escaped,/(/)/space but not&/=, concatenated directly onto the query string rather than throughURLSearchParams. Together these let a caller-suppliedexcludevalue add or override arbitrary query parameters on the authenticated Directions API request. Fixed by (1) requiring apoint(...)entry's interior to be exactly two numbers and nothing else, and (2) building theexcludeparameter throughURLSearchParamslike every other parameter, so it's always correctly percent-encoded regardless of content. Applied to both the server-side request builder and its hand-ported client-side twin in the map preview iframe.
render_map_tool— single visualization primitive for Mapbox MCP. Takes aMapAppPayloadand displays a live Mapbox GL JS map. All other geo tools (directions, isochrone, optimization, search, map-matching, ground-location, polygon-ops) return a ready-to-render_mapApppayload on theirstructuredContent; the LLM passes it torender_map_toolto show a map. This is the only tool that declares_meta.ui.resourceUri, so MCP App hosts (which only fully render the iframe for the last tool in a chained sequence) always render successfully — the visualization step is terminal by design.MapAppPayloadschema (src/utils/mapAppPayload.ts) — the wire format between data tools andrender_map_tool. Thin pass-through over Mapbox Style specpaint/layoutobjects so any layer/marker/legend combination expressible in GL JS is expressible in the payload.- Per-tool payload builders —
buildPolygonOpsMapPayload(still used server-side; see below for the tools that moved their payload building to the iframe instead). Each is a pure function over its tool's response: ~20-80 lines, no HTML, no iframe wiring. - Shared
renderMapAppHtml(src/resources/ui-apps/mapAppHtml.ts) — one iframe template that consumes anyMapAppPayload. Used by both the MCP Apps resource (MapAppUIResource) and any client that wants to bake initial data in. - Polyline decoding moves tool-side via
decodePolyline/decodePolylineWithFallbackso the iframe only ever receives GeoJSON.
-
Tracing: Access tokens are no longer included in exported spans. Mapbox APIs take the access token as a URL query parameter, and OpenTelemetry's HTTP/undici auto-instrumentation records the full request URL on client spans (
url.full,url.query), so operators who configuredOTEL_EXPORTER_OTLP_ENDPOINThad tokens copied verbatim into their telemetry backend. The trace exporter is now wrapped in aRedactingSpanExporterthat strips the token signature from all string span attributes before export. Redaction keeps the token prefix and account name —pk.eyJ1...xyz.signaturebecomespk.your-account.redacted— so spans still distinguish public from secret tokens and show which account a request billed to, without carrying a usable credential. Values that do not parse as a Mapbox token fall back toaccess_token=***. -
urlSafety:
isSafeExternalUrl()(used to validatestatic_map_image_toolcustom-marker overlay URLs) now usesipaddr.jsto parse IPv6 literals and correctly identify IPv4 addresses embedded via any standard encoding (IPv4-mapped, IPv4-compatible, 6to4, NAT64, IPv4-translated/SIIT), instead of pattern-matching a subset of string forms. The previous regex-based check missed the bare IPv4-compatible form and the 6to4/NAT64/SIIT forms, letting blocked IPv4 ranges (loopback, private, link-local, etc.) through when expressed as one of those encodings. The embedded address is now detected from the parsed address's raw bytes rather than by enumerating each encoding's string shape. -
map_matching_tool: When the Map Matching API can't match a trace (e.g.
code: "NoMatch"for distant/unmatchable coordinates), the tool now returns a clearisErrortext result instead of crashing withMCP error -32602: Output validation error— the API omitstracepoints/matchingsin this case, which previously violated the tool's output schema and was returned asstructuredContentanyway, triggering the MCP SDK's output validation. The same schema-violatingstructuredContentcould also be returned for acode: "Ok"response that otherwise failed schema validation (e.g. aconfidenceout of range); that fallback now also returns a gracefulisErrorresult instead of the raw invalid payload.tracepointsandmatchingsare also now.optional()in the output schema as a defensive measure. (AGI-1021) -
render_map_tool: map previews no longer break after a server restart or when reopening an old conversation. Rendering previously depended entirely on in-memory server state (a 30-minute-TTL
Map) that doesn't survive a process restart — includingrender_map_tool's own merged-output ref, the one every MCP App host actually re-fetches when a map card is redisplayed. That ref is now self-describing (the whole small payload is encoded directly into the ref) instead of pointing at that ephemeral store, whenever the payload is small enough to inline. -
directions_tool, isochrone_tool, map_matching_tool, optimization_tool (v1), search_and_geocode_tool, category_search_tool, ground_location_tool: map previews now fetch their own data directly from the relevant Mapbox API, client-side in the iframe, using the same public token already used for map tiles, instead of depending on geometry computed and cached server-side. This also means these previews always reflect fresh data on every render (e.g. current traffic for directions) and no longer depend on the caller's own request options (e.g.
geometries/overviewchoices) to have geometry to draw. Forground_location_tool, the sampling-derived grounding strategy (which decides the geocode types and whether a POI lookup is needed) is resolved once server-side and threaded through the ref, since the iframe can't invoke MCP sampling itself. -
union_tool, intersect_tool, difference_tool: map previews are now recomputed from the original input polygons on every render instead of being cached behind a server-side ref that could expire or vanish on a restart.
-
render_map_tool: clearer recovery when a
payload_refcan't be resolved. Instead of silently dropping the data or returning a bare "nothing to render" error, the LLM is now told to re-run the upstream tool to get a fresh ref. The map preview itself now also shows the server's actual explanation (e.g. "expired") instead of a generic "malformed payload" message.
- New integration test: process-restart survival (
test/integration/processRestart.test.ts). Spawns the actual built server (dist/esm/index.js) as a real child process, gets a ref back over real stdio/MCP protocol, kills that process, spawns an independent one, and resolves the same ref against it — the only test in the suite that crosses a real process boundary, which is what the restart-survival fixes above actually need proven. Runs offline (union_tool/render_map_tooldo no network I/O) in ~5s; skips itself with a clear message ifdist/esm/index.jshasn't been built yet (CI already builds before testing).
- directions_tool: The map preview UI (both the MCP Apps resource and the legacy MCP-UI inline UI) now fetches its own route directly from the Directions API using the tool call's input parameters, instead of depending on the tool response carrying
geometries="geojson". Previously, the map showed an error whenever the defaultgeometries="none"was used because no route data was returned to draw. The map now works regardless of thegeometriesvalue, so text/data responses can stay compact by default without ever breaking the preview. - directions_tool map preview: Replaced the one-shot render latch with per-call render cycles, so a host that reuses the same iframe across sequential
directions_toolcalls gets the new route instead of the first one forever, and hardened the iframe's postMessage handling — messages are only accepted from the embedding host (sender pinning), and untrusted input that gets interpolated into the Directions request URL (coordinates,routing_profile,exclude) is validated before the self-fetch runs, mirroring the server-side zod guarantees.
- CONTRIBUTING: Clarify that unsolicited PRs adding third-party directory/discovery listings (e.g. README badges,
beacon.json-style manifests) are out of scope and will be closed without review (#225).
- Public token resolution:
resolveMapboxPublicTokennow also resolves a public token fortk.*(OAuth-issued temporary) bearers, not justsk.*bearers. Previously, granting thetokens:readscope to an OAuth client had no effect because the Tokens API lookup was skipped fortk.*tokens, causing GL JS preview tools (e.g. Directions) to fail with "No Mapbox public token available" even whentokens:readwas granted. - Public token cache is now per-user: the resolved public token cache was previously a single global slot shared by every request. It's now keyed by the token's account, so concurrent requests from different accounts can no longer receive each other's cached public token.
- Docker: Remove
libgnutls30from the runtime image viadpkg --remove --force-depends. The package is only depended on byapt, which is not needed at runtime.libgnutls30is not called by Node.js (which uses OpenSSL for TLS) and was present solely as a transitive system dependency of the Debian slim base.
- Temporary resources (
mapbox://temp/...) are now scoped to the account that created them: a read by a different account returns the standard not-found response. Token resolution mirrors the tools (request auth, then the env token for stdio/single-user), so local reads are unaffected. Adds regression tests.
- Normalize line endings to LF: Added
.gitattributes(* text=auto eol=lf) and"endOfLine": "lf"to the Prettier config so Windows contributors no longer hit CRLF/Prettier failures when runningnpm run lint.
- static_map_image_tool: Stop embedding the Mapbox access token in tool results. Previously the tool returned a
createUIResource({ iframeUrl })whose URL carried the caller's?access_token=query param, leaking the secret token via the MCP-UI resource item. The credentialed URL is now only used server-side to fetch the image, which is returned inline as base64. The tool'smeta.ui.resourceUrideclaration is removed (the iframe path required the credentialed URL to function and cannot be reinstated without leaking). A regression test asserts the access token does not appear in any content item. - chore: upgrade @opentelemetry/* packages to latest (fixes protobufjs GHSA-xq3m-2v4x-88gg critical CVE) (#183)
- CVE-2026-33750: Added
overridesforbrace-expansionto^2.0.3— eliminates vulnerable1.1.14installs nested under@eslint/config-array,@eslint/eslintrc, andeslintviaminimatch@3.1.5 - CVE-2026-33750 (Docker): Upgrade npm to
11.16.0in Dockerfile —node:22-slimships with npm 10.9.8 which bundlesbrace-expansion2.0.2 internally; upgrading npm replaces it with 5.0.6 (patched)
- Remove
point_in_polygon_tool—points_within_polygon_toolfully covers the single-point case; pass a one-elementpointsarray instead. Updatedpoints_within_polygon_tooldescription to make clear it handles single points as well as batches.
- Upgrade
tshyto^4.1.1,vitest/@vitest/coverage-istanbulto^4.1.4,typescriptto^6.0.2— removed deprecatedbaseUrlfromtsconfig.base.json(TS6), updatedpathsentry to use relative./prefix - Upgrade OpenTelemetry to 2.x — upgraded
@opentelemetry/resourcesand@opentelemetry/sdk-trace-basefrom^1.30.1to^2.6.1; upgraded experimental packages (sdk-node,instrumentation,exporter-trace-otlp-http) from^0.56.0to^0.214.0; upgradedauto-instrumentations-nodeto^0.72.0andsemantic-conventionsto^1.40.0; migratednew Resource()toresourceFromAttributes()following the 2.x API change - Upgrade
zodfrom^3.25.42to^4.3.6— migratedz.record()calls to require explicit key schema (z.string()), updated.shapeaccess (no longer a function in v4), and fixeddenoisedefault handling inIsochroneToolinput schema
directions_toolnow renders as a live Mapbox GL JS map for both the MCP Apps spec and legacy MCP-UI clients:- MCP Apps: the tool declares
_meta.ui.resourceUripointing to a newDirectionsAppUIResource(ui://mapbox/directions-app/index.html). MCP App–capable hosts (Claude Desktop, VS Code, Cursor) render the route via postMessage handoff. - MCP-UI: when
geometries=geojsonis requested and the response carries a renderable LineString, an inlinerawHtmlUIResource is added to the tool'scontent[](gated by the existingENABLE_MCP_UI/--disable-mcp-uiflag, likestatic_map_image_tool). - One source of truth: both pathways render the same HTML produced by
renderDirectionsAppHtml— for MCP Apps the resource serves a generic version and the iframe receives the tool result via postMessage; for MCP-UI the tool bakes the route geometry into the HTML before returning. No more "GL JS map for one client, static image for the other." - Public token: resolved server-side via
GET /tokens/v2/{user}?default=true(requirestokens:readon thesk.*token) withMAPBOX_PUBLIC_TOKENenv var fallback. Non-MCP-App hosts that also have MCP-UI disabled ignore both UI hints and consume the existing text/structuredContent payload unchanged. - Graceful degradation: responses without renderable geometry (>50KB temporary-resource path,
geometries=none,geometries=polyline*) skip the inline rawHtml block and show a "no geometry to render" message in the MCP App iframe. - CSP:
_meta.ui.csp.workerDomains: ['blob:']so MCP App hosts grant Mapbox GL JS the iframe sandbox permissions it needs.
- MCP Apps: the tool declares
- MCP Completions capability: Add auto-completion support for prompt arguments per MCP spec (2025-11-25). Clients can now suggest values when users fill in prompt parameters (#176)
categoryargument onfind-places-nearby— 482 Mapbox Search API categoriesmodeargument onget-directions,search-along-route,show-reachable-areas— driving, driving-traffic, walking, cycling
- ground_location_tool MCP sampling: Use MCP sampling to classify the grounding strategy (
routing/neighborhood/poi/region) and shape downstream Geocoding, Search, and Isochrone calls accordingly. Falls back toneighborhoodwhen the client doesn't support sampling.
- static_map_image_tool: Validate custom-marker URLs to reject loopback, private, link-local, and cloud-metadata IP addresses, preventing SSRF via the Mapbox Static Images API (CWE-918)
- CLI metadata flags: Handle
--helpand--versionbefore server startup so users can inspect usage and version information without requiring Mapbox environment configuration. - Prompt descriptions: Add missing
driving-traffictransport mode toget-directions,search-along-route, andshow-reachable-areasprompt descriptions - ground_location_tool: Use
mapbox/prefix for isochrone profiles and adddriving-trafficsupport - ground_location_tool: Reverse geocode now returns neighborhood/locality/place name instead of street address by default
- ground_location_tool: Strengthened tool description to prefer it over
reverse_geocode_toolfor location context queries
- CVE-2026-4926: Upgraded
@modelcontextprotocol/sdkto^1.29.0, resolvingpath-to-regexpto8.4.1and fixing the ReDoS vulnerability GHSA-j3q9-mxjg-w52f; regenerated output-validation patch for the new version - static_map_image_tool: Validate
styleparameter againstusername/style-idformat to prevent path traversal attacks where a crafted style value (e.g.,../../tokens/v2) could escape the/styles/v1/URL path and access arbitrary Mapbox API endpoints using the server operator's token - static_map_image_tool: Remove access token from URL returned in text content — the token is only used internally for the HTTP fetch and the MCP Apps iframe URL, not exposed to the model context
- 9 new offline Turf.js geometry tools (no API key required, instant results):
points_within_polygon_tool: Batch-test multiple points against a polygon in one call — replaces Npoint_in_polygon_toolcalls for delivery zone validation, fleet geofencing, etc.union_tool: Merge two or more polygons into a single unified geometry; useful for combining service areas, isochrones, or delivery zonesnearest_point_tool: Find the nearest point in a collection to a target — replaces callingdistance_toolfor each candidateintersect_tool: Find the intersection geometry of two polygons (area they share in common)difference_tool: Subtract one polygon from another ("what is in zone A but not zone B?")destination_tool: Calculate a destination point given origin, bearing, and distancelength_tool: Measure the total length of a line/route without a routing API callnearest_point_on_line_tool: Snap a point to the nearest position on a line or routeconvex_tool: Compute the convex hull of a set of points
- Added
getAllToolsto@mapbox/mcp-server/toolssubpath export for batch access to all registered tools - Added
getVersionInfoandVersionInfotype to@mapbox/mcp-server/utilssubpath export
- version_tool: Removed from the tool list — version info is now available as a resource at
mapbox://versionwith zero token overhead
- mapbox://version resource: Server version, git SHA, tag, and branch accessible via
readResource('mapbox://version')
- static_map_image_tool: Large images (>700KB raw) are now stored as temporary resources instead of being inlined as base64, preventing the 1MB tool result limit from being exceeded in Claude Desktop
- Image stored at
mapbox://temp/static-map-{id}, retrievable viaresources/readwith a 30-minute TTL TemporaryResourceManagernow enforces a 50MB byte cap with oldest-first eviction to prevent unbounded memory growthTemporaryDataResourcenow serves image mime types as blob content
- Image stored at
- IsochroneTool large-response handling: Isochrone responses exceeding 50KB are now stored as temporary resources, consistent with DirectionsTool (#131)
- Returns a compact summary with contour count and resource URI instead of the full GeoJSON
- Full GeoJSON retrievable via
readResource('mapbox://temp/isochrone-{id}')with 30-minute TTL - Normal-sized responses are unaffected
- static_map_image_tool: Return a proper error when the Mapbox Static Images API returns a non-2xx response instead of silently encoding the error JSON as a fake base64 image (#130)
- BaseResource URI template registration: Fixed
TemporaryDataResource(and any resource with{in its URI) never matchingreadResourcecalls (#133)server.registerResource()with a plain string registers an exact-match static resource; template URIs likemapbox://temp/{id}require aResourceTemplateobjectBaseResource.installTo()now detects{in the URI and wraps it withResourceTemplateautomatically
- Upgrade
@mcp-ui/serverfrom^5.13.1to^6.1.0(security advisory on older versions) - Upgrade
@modelcontextprotocol/sdkfrom^1.26.0to^1.27.1(security advisory on older versions); regenerated output-validation patch for new version
- Updated README: Goose added to MCP Apps supported clients; MCP-UI noted as legacy protocol
-
place_details_tool: New tool to retrieve detailed information about a specific place by Mapbox ID
- Accepts a
mapbox_idfrom search results (search_and_geocode_tool,category_search_tool,reverse_geocode_tool) - Optional
attribute_setsparameter:basic,photos,visit(hours, rating, price),venue(phone, website, social media) - Optional
languageandworldviewparameters - Returns formatted text summary plus structured GeoJSON Feature content
- Opening hours formatted as readable per-day text (e.g. "Monday: 9 AM – 9 PM") rather than raw JSON
- Accepts a
-
Large Response Handling: DirectionsTool now creates temporary resources for responses >50KB
- Prevents context window overflow on long-distance routes
- Returns summary with distance, duration, and resource URI
- Full route geometry available via MCP resource API
- Temporary resources expire after 30 minutes
- Resource URI format:
mapbox://temp/directions-{id} - Updated tool description to guide LLMs: use geometries="none" for planning, geometries="geojson" only for visualization
- Returns lightweight structured content for large responses (summary data without geometry) to satisfy output schema validation
- Updated
search-along-routeprompt to usegeometries="none"and linear interpolation for route sampling instead of extracting coordinates from geometry
- search_and_geocode_tool, category_search_tool: Include
mapbox_idin formatted text output so models can chain directly toplace_details_toolwithout re-fetching results as JSON - category_search_tool: Fix schema validation failures on Japanese and other international place data
- Added
.passthrough()to all context sub-schemas to allow extra fields returned by the API - Made
country_code,country_code_alpha_3,region_code, andregion_code_fulloptional to match real API responses - Fixed
BaseToolto pass the full Zod schema (not just.shape) to the MCP SDK so.passthrough()settings are preserved during structured-content validation
- Added
- Added hosted MCP endpoint (
https://mcp.mapbox.com/mcp) toserver.jsonremotesfor registry discoverability
- Upgrade
@modelcontextprotocol/ext-appsfrom^1.1.0to^1.1.1 - Upgrade
@modelcontextprotocol/sdkfrom 1.25.3 to 1.26.0 - Regenerated SDK patch for version 1.26.0
- PR Guidelines: Added CHANGELOG requirement to CLAUDE.md (#112)
- All pull requests must now update CHANGELOG.md
- Document what changed, why, and any breaking changes
- Add entry under "Unreleased" section with PR number
- Release Process: Added automated CHANGELOG preparation script (#112)
- New
npm run changelog:prepare-release <version>command - Automatically replaces "Unreleased" with version and date
- Adds new empty "Unreleased" section for next changes
- Includes validation for version format and CHANGELOG structure
- New
- MCP Apps Support for StaticMapImageTool (#109)
- Added interactive map preview in compatible MCP clients (VS Code, Claude Code, Goose)
- Implemented
StaticMapUIResourceserving interactive HTML with inline MCP Apps SDK - Added
@modelcontextprotocol/ext-apps@^1.0.1dependency - Enhanced
BaseToolwithmetaproperty for MCP Apps metadata - Configured CSP for
api.mapbox.comdomains - Sends
ui/notifications/size-changedto fit panel to rendered image height - Fullscreen toggle using
ui/request-display-mode - Uses proper
RESOURCE_MIME_TYPE("text/html;profile=mcp-app") per MCP Apps specification - Tool response now includes: URL text (first, for MCP Apps), base64 image (for non-MCP-Apps clients), and optional UIResource (when MCP-UI enabled)
- CVE-2026-0621: Updated
@modelcontextprotocol/sdkto 1.25.3 to fix ReDoS vulnerability in UriTemplate regex patterns - Regenerated SDK patch for version 1.25.3
- Added
@modelcontextprotocol/ext-apps@^1.0.1 - Updated
@modelcontextprotocol/sdkfrom 1.17.5 to 1.25.3
- StaticMapImageTool: Added text content to response for better MCP client compatibility (#103)
- Tool now returns structured content array with text description, image, and optional MCP-UI resource
- Text content includes map metadata (center, zoom, size, style, overlay count)
- Follows MCP specification for tool results with multiple content items
- Removed an invalid option in input schema of
search_and_geocode_tool. Thenavigation_profileused to have invaliddriving-trafficoption.
-
MCP Resources Support: Added native MCP resource API support
- Introduced
CategoryListResourceexposing category lists asmapbox://categoriesresource - Supports localized category lists via URI pattern
mapbox://categories/{language}(e.g.,mapbox://categories/jafor Japanese) - Created base resource infrastructure (
BaseResource,MapboxApiBasedResource) for future resource implementations - Added
ResourceReaderToolas fallback for clients without native resource support - Enables more efficient access to static reference data without tool calls
- Introduced
-
MCP-UI Support: Added rich UI embedding for compatible MCP clients
StaticMapImageToolnow returns both image data and an embeddable iframe URL- Enables inline map visualization in compatible clients (e.g., Goose)
- Fully backwards compatible - clients without MCP-UI support continue working unchanged
- Enabled by default, can be disabled via
ENABLE_MCP_UI=falseenv var or--disable-mcp-uiflag - Added
@mcp-ui/server@^5.13.1dependency - Configuration helper functions in
toolConfig.ts
- CategoryListTool: Marked as deprecated in favor of the new
mapbox://categoriesresource- Tool remains functional for backward compatibility
- Users are encouraged to migrate to either the native resource API or
resource_reader_tool
- Update to MCP registry schema version 2025-10-17
- Support for
structuredContentfor all applicable tools - Registers output schemas with the MCP server and validates schemas
- Adds OpenTelemetry Instrumentation for all HTTP calls
- Fixed the version tool to properly emit the git version and branch
- Refactored
fetchClientto be generichttpRequest.
- Add server.json for MCP registry
- Introduce new tool: SearchAndGeocodeTool
- Remove former tools: ForwardGeocodeTool, PoiSearchTool; their capabilities are combined in the new tool
- Minor changes to tool descriptions for clarity
- New fetch pipeline with automatic retry behavior
- Dual emits ESM and CommonJS bundles with types per target
- Migrated from Jest to vitest
- Format Options: Add
formatparameter to all geocoding and search tools- CategorySearchTool, ForwardGeocodeTool, PoiSearchTool, and ReverseGeocodeTool now support both
json_stringandformatted_textoutput formats json_stringreturns raw GeoJSON data as parseable JSON stringformatted_textreturns human-readable text with place names, addresses, and coordinates- Default to
formatted_textfor backward compatibility - Comprehensive test coverage for both output formats
- CategorySearchTool, ForwardGeocodeTool, PoiSearchTool, and ReverseGeocodeTool now support both
- Support-NPM-Package: Introduce the NPM package of this mcp-server
- First tag release