Skip to content

Latest commit

 

History

History
623 lines (419 loc) · 97.3 KB

CHANGELOG.md

File metadata and controls

623 lines (419 loc) · 97.3 KB

Unreleased

Features

  • 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 uses POST /user-feedback/v1/cli-feedback, the undocumented Feedback API endpoint added for the Mapbox CLI's mapbox 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_request or other) and an optional tool name. The endpoint requires a location, which feedback about the server doesn't have, so it sends 0, 0 as the CLI does.
    • Where it came from: the server appends — mapbox-mcp-server <version> via <client> <version>, about <tool> to the text, so the team can tell these from the CLI's reports. The client comes from the server handling the call, which the hosted server needs, since one process serves many clients.
    • Confirmation: like feedback_tool, it asks through elicitation when the client supports it and sends nothing if the user declines or the prompt fails. It sends its own id, so a retried POST doesn't make a second report.

0.17.1-dev.1 - 2026-11-09

Bug Fixes

  • 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_tool leaves the payload out of structuredContent and only sends a mapbox://temp/ ref, and ChatGPT only lets the widget read resources in its own ui:// scope, so the map couldn't load it. render_map_tool now 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. A ResizeObserver on the chip now re-places the buttons whenever its size changes. Browsers without ResizeObserver keep the old behavior.

Maintenance

  • OpenTelemetry JS 3.x (#325). @opentelemetry/sdk-trace-base is deprecated in every 2.x release and removed in 3.0, and the ox-security scan flagged it on every PR touching package.json (#319). Moves the OpenTelemetry set to the 3.0 line: sdk-trace 3.0.0 in place of sdk-trace-base, resources 3.0.0, sdk-node, instrumentation and exporter-trace-otlp-http 0.300.0, auto-instrumentations-node 0.82.0 and api 1.10.0. 3.1.0 came out the day before, so this stays on 3.0.0 for now.
    • Code: ConsoleSpanExporter, ReadableSpan and SpanExporter now come from sdk-trace. In the test, BasicTracerProvider is now TracerProvider, and SimpleSpanProcessor takes { exporter }.
    • Node: engines is now >=22.15.0, the 3.x minimum (it was >=22).
    • Checked: the built server, both ESM and CommonJS, sends the same config.load_env and tool.distance_tool spans and the same metrics to a local OTLP endpoint as 0.17.0 does.

0.17.0 - 2026-11-06

0.17.0-dev.3 - 2026-11-05

Bug Fixes

  • 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 returned pk. 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 proximity or bbox the 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_tool and category_search_tool now pin the ref to the first result in that case, flagged proximityFromResults so 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 the error text. Search refs whose proximity or bbox isn't the shape the map sends (a hand-written "lon,lat" string, say) are now rejected when resolved, so render_map_tool tells 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 numeric longitude and latitude to resolve, the same check the map makes, so a damaged ref is caught at render_map_tool instead.

0.17.0-dev.2 - 2026-11-05

New Features

  • 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 and aria-pressed.
    • Traffic: the Mapbox Traffic v1 tileset (mapbox://mapbox.mapbox-traffic-v1) is drawn as lines colored by congestion (light, moderate, heavy, severe) in the middle slot, 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 from api.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 new render_map_tool input, and a line in the tool description, so the model knows it can show traffic and can open the map with it on. false turns it off. Left out, the map keeps whatever the user picked, the same way baseStyle works. A showTraffic-only call is renderable on its own, like baseStyle.
    • 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. A baseStyle switch 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, showTraffic on, 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 (setStyle clears them), and mounts controls through onAdd. Checked a live San Francisco tile: the source layer is traffic, with congestion, class and structure. Checked in Claude Desktop on both styles.
  • 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 (Places attributes.price_level as $ 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 and score.popularity isn'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 a label (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_hours string 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 (Places telemetry.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 on host-context-changed), or the system's prefers-color-scheme when the host sends none. Colors come from the host's style variables (--color-background-primary, --color-text-primary and 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.
  • static_map_image_tool shows 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.ui at ui://mapbox/static-map/index.html, which draws the image from the tool result at full width as a data: 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.

Bug Fixes

  • The dev HTTP server (npm run dev:http) sends the server instructions (#314). It built its server without them, unlike src/index.ts and 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 calling list_public_tokens_tool.
  • The render_map_tool map view ignores postMessage events 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_tool reads mapbox:// 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 native resources/read, such as Claude.ai and Claude Desktop.
  • static_map_image_tool falls 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 a mapbox://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 a mapbox://response/static-map?data=... ref instead of a mapbox://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 large directions_tool and isochrone_tool responses 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 to buildStaticMapRequestUrl.ts, shared by the tool and ToolResponseResource. 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 own MAPBOX_PUBLIC_TOKEN is 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 only mapbox://categories was registered, so clients that read resources directly got "Resource not found"; only the resource_reader_tool fallback could read it. Resources can now declare a uriTemplate they also answer, and the categories resource registers mapbox://categories/{language}. Anything after mapbox://categories/ that isn't a language code (like %20ja or ja?x=1) now returns "Resource not found" instead of the English list; a well-formed code the API doesn't support (like xx) 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 without baseMapConfig no 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-pass showTraffic along 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 in structuredContent, 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. A mapbox://selffetch/ ref now says to pass it to render_map_tool instead 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's data= and checks no access token is in it. docs/mcp-ui.md now describes the viewer and the public-token link.
    • Tool-selection eval: token_embedded is listed in both hillclimb _state.json metrics, so reports show it next to ref_ok and no_token_ask.

Other Features

  • Tool-selection eval: site cases check list_public_tokens_tool (#311). The site-building cases expected export_map_html_tool, which #279 reverted, so they could never pass on main. They now expect the agent to get a public token from list_public_tokens_tool and embed it in the page it writes, checked by a new token_embedded metric against a realistic-looking stub token. Adds a site-chatgpt-app case (a ChatGPT Apps SDK widget with a Mapbox map) and an EVAL_CASES=<ids> filter for running a few cases cheaply. On main's tool text, Claude Opus 5.5 (18 rows) and OpenAI chat-latest (15 rows) embedded the listed token every time.

0.17.0-dev.1 - 2026-10-30

Breaking Changes

  • MCP TypeScript SDK v2 (#296, part of #245). The server and the @mapbox/mcp-server/tools, /resources and /utils exports now use the v2 SDK packages (@modelcontextprotocol/server and @modelcontextprotocol/core, 2.3.1) instead of @modelcontextprotocol/sdk 1.x, and @modelcontextprotocol/ext-apps moves 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 v2 McpServer. A v1 server from @modelcontextprotocol/sdk won'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 at ctx.http.authInfo.token instead of extra.authInfo.token.
    • Clients: nothing changes. The server still answers the initialize handshake and negotiates 2025-11-25 with current clients, and the MCP Apps wire protocol is the same in ext-apps 2.x, so render_map_tool renders 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 structuredContent that 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, so lenientOutputSchema wraps each tool's Zod schema: tools/list still publishes the real JSON Schema, and validation only warns. A tool with an output schema that returns only text gets structuredContent: {} 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. toJsonSchema2020 is 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-string setRequestHandler, RequestHandlerExtra to ServerContext, extra.authInfo to ctx.http?.authInfo). @modelcontextprotocol/client and @modelcontextprotocol/node are 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 real tools/call through McpServer for a mismatched, a missing and a matching structuredContent, 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.

New Features

  • 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_url and 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 pk tokens and drops anything else, so secret and temporary tokens never come back. It can't create tokens (that needs tokens:write; DevKit's create_token_tool does it).
    • No new scope: it needs tokens:read, which the in-chat map already uses to find the default public token. With a pk. access token, or when listing fails on the server's own MAPBOX_ACCESS_TOKEN and MAPBOX_PUBLIC_TOKEN is set, it returns that token. A caller with its own bearer token never gets the server's MAPBOX_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.
  • 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_turn and 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 id itself 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 (with duplicate: 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: true skips 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:write to send, and user-feedback:read for 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_tool leaves it out.
    • Annotated readOnlyHint: false, destructiveHint: false, idempotentHint: false. The location goes as top-level lat/lon; the optional GeoJSON feature and screenshot fields aren't sent yet, since the docs give feature coordinates as [latitude, longitude] and that needs checking first.

Bug Fixes

  • directions_tool: geometries="geojson" now always returns coordinates directly, regardless of trip length (#255). Previously, every geojson request used overview=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 a mapbox://temp/... resource URI (a mapbox://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 overview input ("full" | "simplified") now defaults to "simplified" whenever geometries="geojson" (and to "full", unchanged, whenever geometries="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 congestion annotation unless overview=full (422: Overview option must be full for congestion, confirmed live) — distance/speed annotations have no such restriction and stay accurate at any overview level. So congestion_information (the per-segment traffic breakdown) is only present when the effective overview is "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 what directions_tool's own response contains.
    • Matches the overview parameter optimization_tool already exposes (also defaulting to "simplified"), which never needed a large-response fallback in the first place.
    • directionsRequestFromRef (#276) now always asks for overview=full, so apps that draw a route from the ref still get every vertex when the original call took the new simplified default.
  • directions_tool: turn-by-turn instructions now actually populate (#256). The tool has always requested steps=true and documented its response as including "turn-by-turn instructions," but the extraction code read announcements from step.voiceInstructions, which the Directions API only returns when the separate voice_instructions=true parameter is set — never sent by this tool. So instructions was silently empty for every call, regardless of route length. Switched to step.maneuver.instruction (e.g. "Bear right onto Great Portland Street/A4201. Continue on A4201."), a human-readable instruction already present on every step whenever steps=true is 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.instruction is 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_tool and place photos: URLs with several trailing dots are now checked like the plain hostname. The URL check stripped only one trailing root-label dot, so https://localhost../x.png was compared as localhost. and passed, as did app.localhost.., dotless hosts such as intranet.., and IP literals such as 127.0.0.1.. and 169.254.169.254... It now strips them all. This covers custom-marker URLs in static_map_image_tool and photo URLs in PlacePhotoResource. Public hosts with a trailing dot (example.com.) are still accepted.

Maintenance

  • MCP TypeScript SDK 2.4.0 (#303). Bumps @modelcontextprotocol/server, /core and /client from 2.3.1 to 2.4.0 and /node to 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 own skills/list and skills/get handlers. Its patch changes don't affect our tools: none of our schemas use $id, a $ref root or x-mcp-header.
  • Removed build output committed under src/ (#297). src/utils/versionUtils-cjs.cjs, .d.cts and their source maps were compiled output from a tsc run that wrote next to the sources, committed back in #96. Only versionUtils-cjs.cts is source: tshy swaps it in for versionUtils.ts in the CommonJS build. .gitignore now keeps .js, .cjs, .mjs, .d.*ts and .map files out of src/ and test/.
  • Tests are typechecked in CI (#298). tsconfig.test.json covers src/ and test/, but nothing ran it, so test type errors piled up: about 200 on main, plus about 250 more from SDK v2 typing structuredContent as an open object (#296). They're all fixed, and npm run typecheck now runs in CI before the tests. tsconfig.test.json also gets noEmit, since running it without that writes .js files 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. textOf and resourceTextOf read text content and throw on the wrong content type.
    • Tests that were wrong: the tuple check in input-schema-validation.test.ts never looked inside arrays under zod 4 (ZodArray._def.type is 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.ts imported a file that never existed. cleanResponseData.test.ts used an old directions input shape and now builds it with DirectionsInputSchema.parse. A || Infinity fallback in a SimplifyTool test meant one comparison could never fail.

0.16.1-dev - 2026-10-16

0.16.0 - 2026-10-15

New Features

  • render_map_tool: a results panel for search results, with popularity from Place Details (#262). When a map shows places from search_and_geocode_tool, category_search_tool or ground_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 batched POST to 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 markers rather than payload_refs, so markers[] now takes optional id (the POI's mapbox_id), name, category and distanceMeters. A marker with id gets the same row and enrichment. Markers with id and no label are 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 any https: 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 a data: 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 without resources/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 A stays 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 request init through, so the POST body can be checked.
  • directionsRequestFromRef in @mapbox/mcp-server/utils, for apps that keep long routes out of the model's context (#276). Every directions_tool result carries a short ref in structuredContent.mapboxRender.ref that holds only the call's inputs. directionsRequestFromRef(ref, { accessToken }) turns it back into the Directions API URL for the same route with geometries=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 way render_map_tool does, with no server-side state. Also exports resolveSelfFetchRef, isSelfFetchRef, buildDirectionsRequestUrl (the tool's own URL builder, so an app's request stays in sync with it) and the MapAppPayload/MapAppSelfFetch/DirectionsRequestInput types. See docs/importing-tools.md.

Bug Fixes

  • directions_tool and isochrone_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 a mapbox://temp/ ref. The hosted deployment runs several stateless tasks with no session stickiness, so a follow-up resources/read that landed on another task got "Resource not found or expired" (most visible on long routes such as London to Edinburgh with geometries="geojson"). They now return a mapbox://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 (new ToolResponseResource) 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 uses mapbox://temp/.

  • Tool descriptions no longer steer Claude away from other tools and sources, per the Claude connector directory review (#273). render_map_tool dropped "PREFERRED MAP TOOL", the instruction never to use built-in map tools (which named places_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_tool dropped "FIRST and ONLY" and "do NOT … search the web for places"; it now says it already includes reverse geocoding, so a separate reverse_geocode_tool call 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 the limit parameter 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 to render_map_tool.

Other Features

  • Tool-selection eval for catching routing regressions from tool descriptions (#274). npm run eval:tools runs 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_tool only for image files, ground_location_tool vs reverse_geocode_tool, brand vs category search, and whether web search stays available for place questions. The tool list and server instructions come from a real McpServer built 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 every render_map_tool call 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 a write_file host tool and check that it neither asks the user for a Mapbox token nor leaves a placeholder token in the page; they expect export_map_html_tool (#275) and fail on servers without it. EVAL_SERVER_ROOT points the harness at another checkout, so two branches can be compared, and npm run eval:tools:report builds 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 of npm 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, openai and tsx as dev dependencies. See evals/tool-selection/README.md.

0.15.1 - 2026-10-09

New Features

  • @mapbox/mcp-server/utils now exports the server setup helpers, so servers built from the subpath exports get the #268 and #269 fixes (#271). publishJsonSchema2020, toJsonSchema2020, and buildServerInstructions were only used by this package's own entry point (src/index.ts). Anything that builds its own McpServer from @mapbox/mcp-server/tools and installs the tools itself, which is how the hosted server at mcp.mapbox.com works, still published draft-07 schemas (rejected by Claude Desktop) and sent no server instructions (so models that load tools lazily keep picking static_map_image_tool over render_map_tool). Upgrading to 0.15.0 alone fixed neither. Such a server can now call publishJsonSchema2020(server) before installing tools and pass instructions: buildServerInstructions(<enabled tool names>) when creating the server. The custom-server example in docs/importing-tools.md called .definition and .execute(), which the tools don't have; it now uses getAllTools()/getAllResources(), installTo(), and both helpers. Added export tests, including one that builds an external McpServer from 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.

0.15.0 - 2026-10-08

Security

  • 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_tool payload refs) by comparing the resource's recorded owner to getUserNameFromToken(callerToken). That helper base64-decodes the JWT payload and returns the u field 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 into extra.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 through resources/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.owner value 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). MapAppUIResource and render_map_tool declared connectDomains: ['https://*.mapbox.com', 'https://events.mapbox.com'], a wildcard over every Mapbox subdomain (which also made the events.mapbox.com entry 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 one mapAppCsp(apiEndpoint) helper, so the resource and the tool can't drift apart: api.mapbox.com and events.mapbox.com for connections, plus the origin of MAPBOX_API_ENDPOINT when it points somewhere else (the page's Directions, Isochrone, Search Box and other calls go there); api.mapbox.com for resources; blob: workers. BaseTool's meta.ui.csp type gains workerDomains so the tool can declare the same policy as the resource.

    Checked in headless Chrome with the new policy enforced as a Content-Security-Policy header: the map loaded on standard, rendered a payload with a line layer, a circle layer, markers and lightPreset: "dusk", then switched to standard-satellite. Every request went to api.mapbox.com (103 requests, including tiles, 3D models and imagery) or events.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.

Fixed

  • Map display now goes to render_map_tool even in hosts that load tools lazily (#269). In Claude Desktop, after a successful isochrone_tool call, the model loaded static_map_image_tool to "show a map" and never loaded render_map_tool. Claude Desktop lists only tool names until the model loads a tool, so render_map_tool's description ("ALWAYS use this tool to display maps") was never in view when it chose. The server now returns MCP instructions from initialize, which hosts show up front: use render_map_tool to show anything on a map, pass mapboxRender.ref values unchanged and never write one by hand, and use static_map_image_tool only when the user explicitly asks for an image file. The instructions name only enabled tools, and are omitted when render_map_tool is disabled. static_map_image_tool's description now carries the same redirect. The hint appended to tool results with a mapboxRender.ref now names render_map_tool over static_map_image_tool and 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, without static_map_image_tool, and without render_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's inputSchema and outputSchema as draft-07, with no option to change it, while MCP specifies 2020-12 as the default dialect. Changing only the $schema label would not have been enough: 30 schema nodes are tuples (bbox, coordinate pairs), which draft-07 writes as items: [...], a form 2020-12 does not allow.

    publishJsonSchema2020() (src/utils/jsonSchema2020.ts) wraps setRequestHandler on our own server instance before any tool is registered, so the SDK's tools/list handler is installed wrapped. Its schemas are converted on the way out: the label becomes 2020-12, tuple items becomes prefixItems (with additionalItems becoming items), and definitions becomes $defs with refs rewritten to match. The conversion walks only real subschema positions, so a property named items or definitions is 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_tool result 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_tool always writes contours_minutes/contours_meters into its mapbox://selffetch/isochrone ref (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_tool then 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 numeric contours_minutes or contours_meters array. 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 its mapboxRender.ref verbatim. The iframe's isSafeIsochroneParams does 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 returned isError: false with "Rendered map with 0 layers" and a parenthetical note. That would have drawn an empty globe under the summary. render_map_tool now 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 set baseMapConfig or baseStyle are unaffected. Two existing tests used an isochrone fixture with coordinates as an array, a shape isochrone_tool never 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. baseStyle was only ever applied at map-construction time in mapAppHtml.ts's initMap(), seeded from a server-embedded #initial-data script tag — but MapAppUIResource never actually passes initialData when serving that HTML, so the tag is always empty and the map always constructs with the default "standard" style. The real payload (with baseStyle) arrives afterward via the ui/notifications/tool-result → render(payload) path, which only ever handled baseMapConfig (color/theme properties via setConfigProperty), never baseStyle itself. 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 a baseStyle change from what the map is currently on and switches via map.setStyle(...), deferring the rest of the render until style.load fires (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.

New Features

  • ground_location_tool now returns mapbox_id and external_ids on 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 local CategorySearchFeature interface didn't declare the others either — even though CategorySearchTool.output.schema.ts has always modelled mapbox_id (required) and external_ids (optional) on the same API response. Two consequences, both for the model reading the tool result: there was no way to follow up with place_details_tool (which takes a mapbox_id) to get ratings, popularity, opening hours, or a phone number for a place ground_location_tool had 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 to nearby_pois, with .describe() text pointing at the place_details_tool follow-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 returned mapbox_id is not guaranteed to work against the Places /details/retrieve endpoint: some ids decode to the OpenStreetMap-sourced urn: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 /forward endpoint (category + place + attribute phrasing, e.g. "quiet coffee shops with wifi near downtown"), verified working well in our own testing. The tool's description and q field 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.

Fixed

  • 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. New baseStyle: "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/top slots, most baseMapConfig properties) rendered over global satellite imagery instead of a vector basemap. Requested via Slack. Applied at map-construction time, same as baseStyle's sibling baseMapConfig, 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. See docs/render-map-tool.md for the property-compatibility notes and an example.

  • The server now identifies which MCP client connected to it. server.server.getClientVersion() (populated from the clientInfo sent in the client's initialize request) 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 a server.server.oninitialized callback rather than right after server.connect(), since getClientVersion()/getClientCapabilities() are only populated once the client's initialize request has actually been processed, which is not guaranteed by the time connect()'s promise resolves (it only waits for the transport to start). Confirmed live that reading capabilities immediately after connect() reliably returned undefined even for a client that declared them; moving both reads into oninitialized fixed 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 as mcp.client.name/mcp.client.version on 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 via baseMapConfig, and place custom layers with slot. Previously MapAppPayload/RenderMapInputSchema only supported adding new GeoJSON layers on top of a fixed base map. baseMapConfig exposes 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 the config option, when present in the initial payload) to avoid a flash of default colors, and via setConfigProperty on 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 when slot is omitted). Both verified live in a real browser against the Mapbox API. See docs/render-map-tool.md for the full property list and an example. Filed as #249 after a user asked whether render_map_tool could turn the water red on the fly.

Breaking Changes

  • place_details_tool now calls the Mapbox Places API instead of the older Details API. The tool previously called search/details/v1/retrieve (docs.mapbox.com/api/search/details/); it now calls places/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, and worldview are removed from the primary input schema — the Places API's Details endpoint takes only a mapbox_id and 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_hours as a plain OSM-format string, coordinates: { latitude, longitude }, score: { popularity, reality, closed }, address, attributes, photos, building) — see PlaceDetailsTool.output.schema.ts. Callers reading properties.* or geometry.coordinates from 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.reality are 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 a 422 Invalid mapbox_id format error (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_tool now retries against the legacy Details API on that specific 422 and normalizes its response onto the same flat output shape (adding feature_type, bbox, context, and metadata fields, populated only via this fallback). attribute_sets/language/worldview are forwarded to the fallback request when provided. POI lookups are unaffected — they resolve via the Places API in a single request, same as before.

Changed

  • Tool calls are now bound to the MCP server that received them. BaseTool tracked its target server in a single mutable instance field set by installTo(), 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/tools are module-level singletons, an application that installed one instance into more than one McpServer would have the later installTo() 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, via AsyncLocalStorage, 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. Calling run() directly, outside a registered callback, still falls back to the most recently installed server.

    BaseResource carried the same pattern and got the same treatment. Its only reader was log(), so the practical effect there was misdirected log messages rather than misdirected client interaction, but the resource instances exported from @mapbox/mcp-server/resources are module-level singletons for the same reason and the shared field was the same hazard.

  • render_map_tool's base map style switched from streets-v12 to Mapbox Standard. streets-v12 was never a deliberate choice — it was inherited from StaticMapImageTool'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 a slot, which (per the Style Spec) places them above all imported layers, matching their prior position under streets-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.

Dependencies

  • Bumped @modelcontextprotocol/sdk to 1.30.0. Not adopting the 2026-07-28 spec 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, since directions_tool/search_and_geocode_tool's elicitation-based selection and ground_location_tool's sampling-based strategy detection all depend on the mechanisms being replaced. Regenerated patches/@modelcontextprotocol+sdk+1.30.0.patch (previously pinned to 1.29.0) — same patch content, applies cleanly to the new version, verified live against the built server.

0.14.0 - 2026-07-30

New Features

  • directions_tool — route selection elicitation. When the Directions API returns two or more route alternatives, the tool now asks the user to pick one via server.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-stage directions_tool elicitation 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 (a try/catch around each elicitInput call, with no capability pre-check).

Fixed

  • place_details_tool: fixed output validation failure when attribute_sets omitted "basic". The tool's output schema requires properties.name and properties.feature_type, both of which are only populated by the Details API's basic attribute set — so a call like attribute_sets: ["visit"] was accepted at the input stage but then failed with an opaque Output validation error once the API's response came back without those fields. "basic" is now always merged into the outgoing attribute_sets request regardless of what the caller passes, matching what the tool's own description already claimed ("always included").

Changed

  • Removed dead MCP-UI code. --disable-mcp-ui no 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 orphaned StaticMapUIResource (ui://mapbox/static-map/index.html) — it was registered but no tool had referenced it since static_map_image_tool stopped 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.

Documentation

  • README and render_map_tool docs refreshed. The README's "Rich Map Previews" section and docs/mcp-ui.md described MCP-UI support (@mcp-ui/server, ENABLE_MCP_UI, StaticMapUIResource wired to static_map_image_tool) that was fully removed when render_map_tool shipped — @mcp-ui/server is no longer a dependency and nothing in src/ reads ENABLE_MCP_UI anymore. docs/mcp-ui.md now explains what changed and points to the new docs/render-map-tool.md, a comprehensive guide covering the full payload schema and, in particular, how to call render_map_tool standalone 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 removed point_in_polygon_tool with its actual replacement, points_within_polygon_tool.

0.13.0 - 2026-07-30

Security

  • directions_tool: fixed query parameter injection via the exclude parameter. A point(<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 through URLSearchParams. Together these let a caller-supplied exclude value add or override arbitrary query parameters on the authenticated Directions API request. Fixed by (1) requiring a point(...) entry's interior to be exactly two numbers and nothing else, and (2) building the exclude parameter through URLSearchParams like 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.

New Features

  • render_map_tool — single visualization primitive for Mapbox MCP. Takes a MapAppPayload and 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 _mapApp payload on their structuredContent; the LLM passes it to render_map_tool to 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.
  • MapAppPayload schema (src/utils/mapAppPayload.ts) — the wire format between data tools and render_map_tool. Thin pass-through over Mapbox Style spec paint/layout objects 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 any MapAppPayload. 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 / decodePolylineWithFallback so the iframe only ever receives GeoJSON.

Fixed

  • 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 configured OTEL_EXPORTER_OTLP_ENDPOINT had tokens copied verbatim into their telemetry backend. The trace exporter is now wrapped in a RedactingSpanExporter that strips the token signature from all string span attributes before export. Redaction keeps the token prefix and account name — pk.eyJ1...xyz.signature becomes pk.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 to access_token=***.

  • urlSafety: isSafeExternalUrl() (used to validate static_map_image_tool custom-marker overlay URLs) now uses ipaddr.js to 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 clear isError text result instead of crashing with MCP error -32602: Output validation error — the API omits tracepoints/matchings in this case, which previously violated the tool's output schema and was returned as structuredContent anyway, triggering the MCP SDK's output validation. The same schema-violating structuredContent could also be returned for a code: "Ok" response that otherwise failed schema validation (e.g. a confidence out of range); that fallback now also returns a graceful isError result instead of the raw invalid payload. tracepoints and matchings are 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 — including render_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/overview choices) to have geometry to draw. For ground_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_ref can'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.

Testing

  • 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_tool do no network I/O) in ~5s; skips itself with a clear message if dist/esm/index.js hasn't been built yet (CI already builds before testing).

0.12.7 - 2026-07-20

Fixed

  • 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 default geometries="none" was used because no route data was returned to draw. The map now works regardless of the geometries value, 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_tool calls 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.

Documentation

  • 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).

0.12.6 - 2026-07-13

Fixed

  • Public token resolution: resolveMapboxPublicToken now also resolves a public token for tk.* (OAuth-issued temporary) bearers, not just sk.* bearers. Previously, granting the tokens:read scope to an OAuth client had no effect because the Tokens API lookup was skipped for tk.* tokens, causing GL JS preview tools (e.g. Directions) to fail with "No Mapbox public token available" even when tokens:read was 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.

0.12.5 - 2026-06-15

Changed

  • Docker: Remove libgnutls30 from the runtime image via dpkg --remove --force-depends. The package is only depended on by apt, which is not needed at runtime. libgnutls30 is not called by Node.js (which uses OpenSSL for TLS) and was present solely as a transitive system dependency of the Debian slim base.

0.12.3 - 2026-06-11

Changed

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

Dependencies

  • 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 running npm run lint.

0.12.2-dev - 2026-06-10

Security

  • 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's meta.ui.resourceUri declaration 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 overrides for brace-expansion to ^2.0.3 — eliminates vulnerable 1.1.14 installs nested under @eslint/config-array, @eslint/eslintrc, and eslint via minimatch@3.1.5
  • CVE-2026-33750 (Docker): Upgrade npm to 11.16.0 in Dockerfile — node:22-slim ships with npm 10.9.8 which bundles brace-expansion 2.0.2 internally; upgrading npm replaces it with 5.0.6 (patched)

Breaking Changes

  • Remove point_in_polygon_tool — points_within_polygon_tool fully covers the single-point case; pass a one-element points array instead. Updated points_within_polygon_tool description to make clear it handles single points as well as batches.

Dependencies

  • Upgrade tshy to ^4.1.1, vitest/@vitest/coverage-istanbul to ^4.1.4, typescript to ^6.0.2 — removed deprecated baseUrl from tsconfig.base.json (TS6), updated paths entry to use relative ./ prefix
  • Upgrade OpenTelemetry to 2.x — upgraded @opentelemetry/resources and @opentelemetry/sdk-trace-base from ^1.30.1 to ^2.6.1; upgraded experimental packages (sdk-node, instrumentation, exporter-trace-otlp-http) from ^0.56.0 to ^0.214.0; upgraded auto-instrumentations-node to ^0.72.0 and semantic-conventions to ^1.40.0; migrated new Resource() to resourceFromAttributes() following the 2.x API change
  • Upgrade zod from ^3.25.42 to ^4.3.6 — migrated z.record() calls to require explicit key schema (z.string()), updated .shape access (no longer a function in v4), and fixed denoise default handling in IsochroneTool input schema

New Features

  • directions_tool now 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.resourceUri pointing to a new DirectionsAppUIResource (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=geojson is requested and the response carries a renderable LineString, an inline rawHtml UIResource is added to the tool's content[] (gated by the existing ENABLE_MCP_UI/--disable-mcp-ui flag, like static_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 (requires tokens:read on the sk.* token) with MAPBOX_PUBLIC_TOKEN env 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 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)
    • category argument on find-places-nearby — 482 Mapbox Search API categories
    • mode argument on get-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 to neighborhood when the client doesn't support sampling.

Security

  • 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)

Fixes

  • CLI metadata flags: Handle --help and --version before server startup so users can inspect usage and version information without requiring Mapbox environment configuration.
  • Prompt descriptions: Add missing driving-traffic transport mode to get-directions, search-along-route, and show-reachable-areas prompt descriptions
  • ground_location_tool: Use mapbox/ prefix for isochrone profiles and add driving-traffic support
  • 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_tool for location context queries

0.11.0 - 2026-04-01

Security

  • CVE-2026-4926: Upgraded @modelcontextprotocol/sdk to ^1.29.0, resolving path-to-regexp to 8.4.1 and fixing the ReDoS vulnerability GHSA-j3q9-mxjg-w52f; regenerated output-validation patch for the new version
  • static_map_image_tool: Validate style parameter against username/style-id format 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

New Features

  • 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 N point_in_polygon_tool calls 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 zones
    • nearest_point_tool: Find the nearest point in a collection to a target — replaces calling distance_tool for each candidate
    • intersect_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 distance
    • length_tool: Measure the total length of a line/route without a routing API call
    • nearest_point_on_line_tool: Snap a point to the nearest position on a line or route
    • convex_tool: Compute the convex hull of a set of points

Exports

  • Added getAllTools to @mapbox/mcp-server/tools subpath export for batch access to all registered tools
  • Added getVersionInfo and VersionInfo type to @mapbox/mcp-server/utils subpath export

Removed

  • version_tool: Removed from the tool list — version info is now available as a resource at mapbox://version with zero token overhead

New Features

  • mapbox://version resource: Server version, git SHA, tag, and branch accessible via readResource('mapbox://version')

Bug Fixes

  • 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 via resources/read with a 30-minute TTL
    • TemporaryResourceManager now enforces a 50MB byte cap with oldest-first eviction to prevent unbounded memory growth
    • TemporaryDataResource now serves image mime types as blob content

0.10.0 - 2026-03-04

New Features

  • 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

Bug Fixes

  • 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 matching readResource calls (#133)
    • server.registerResource() with a plain string registers an exact-match static resource; template URIs like mapbox://temp/{id} require a ResourceTemplate object
    • BaseResource.installTo() now detects { in the URI and wraps it with ResourceTemplate automatically

Dependencies

  • Upgrade @mcp-ui/server from ^5.13.1 to ^6.1.0 (security advisory on older versions)
  • Upgrade @modelcontextprotocol/sdk from ^1.26.0 to ^1.27.1 (security advisory on older versions); regenerated output-validation patch for new version

Documentation

  • Updated README: Goose added to MCP Apps supported clients; MCP-UI noted as legacy protocol

0.9.0 - 2026-02-24

New Features

  • place_details_tool: New tool to retrieve detailed information about a specific place by Mapbox ID

    • Accepts a mapbox_id from search results (search_and_geocode_tool, category_search_tool, reverse_geocode_tool)
    • Optional attribute_sets parameter: basic, photos, visit (hours, rating, price), venue (phone, website, social media)
    • Optional language and worldview parameters
    • 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
  • 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-route prompt to use geometries="none" and linear interpolation for route sampling instead of extracting coordinates from geometry

Bug Fixes

  • search_and_geocode_tool, category_search_tool: Include mapbox_id in formatted text output so models can chain directly to place_details_tool without 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, and region_code_full optional to match real API responses
    • Fixed BaseTool to pass the full Zod schema (not just .shape) to the MCP SDK so .passthrough() settings are preserved during structured-content validation

Registry

  • Added hosted MCP endpoint (https://mcp.mapbox.com/mcp) to server.json remotes for registry discoverability

Dependencies

  • Upgrade @modelcontextprotocol/ext-apps from ^1.1.0 to ^1.1.1
  • Upgrade @modelcontextprotocol/sdk from 1.25.3 to 1.26.0
  • Regenerated SDK patch for version 1.26.0

Documentation

  • 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

Developer Experience

  • 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

0.8.3

Features Added

  • MCP Apps Support for StaticMapImageTool (#109)
    • Added interactive map preview in compatible MCP clients (VS Code, Claude Code, Goose)
    • Implemented StaticMapUIResource serving interactive HTML with inline MCP Apps SDK
    • Added @modelcontextprotocol/ext-apps@^1.0.1 dependency
    • Enhanced BaseTool with meta property for MCP Apps metadata
    • Configured CSP for api.mapbox.com domains
    • Sends ui/notifications/size-changed to 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)

Security

  • CVE-2026-0621: Updated @modelcontextprotocol/sdk to 1.25.3 to fix ReDoS vulnerability in UriTemplate regex patterns
  • Regenerated SDK patch for version 1.25.3

Dependencies

  • Added @modelcontextprotocol/ext-apps@^1.0.1
  • Updated @modelcontextprotocol/sdk from 1.17.5 to 1.25.3

0.8.2

Bug Fixes

  • 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

0.8.0

Bug Fixes

  • Removed an invalid option in input schema of search_and_geocode_tool. The navigation_profile used to have invalid driving-traffic option.

0.7.0

Features Added

  • MCP Resources Support: Added native MCP resource API support

    • Introduced CategoryListResource exposing category lists as mapbox://categories resource
    • Supports localized category lists via URI pattern mapbox://categories/{language} (e.g., mapbox://categories/ja for Japanese)
    • Created base resource infrastructure (BaseResource, MapboxApiBasedResource) for future resource implementations
    • Added ResourceReaderTool as fallback for clients without native resource support
    • Enables more efficient access to static reference data without tool calls
  • MCP-UI Support: Added rich UI embedding for compatible MCP clients

    • StaticMapImageTool now 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=false env var or --disable-mcp-ui flag
    • Added @mcp-ui/server@^5.13.1 dependency
    • Configuration helper functions in toolConfig.ts

Deprecations

  • CategoryListTool: Marked as deprecated in favor of the new mapbox://categories resource
    • Tool remains functional for backward compatibility
    • Users are encouraged to migrate to either the native resource API or resource_reader_tool

0.6.1

Other

  • Update to MCP registry schema version 2025-10-17

0.6.0 (Unreleased)

Features Added

  • Support for structuredContent for all applicable tools
  • Registers output schemas with the MCP server and validates schemas
  • Adds OpenTelemetry Instrumentation for all HTTP calls

Bug Fixes

  • Fixed the version tool to properly emit the git version and branch

Other Features

  • Refactored fetchClient to be generic httpRequest.

0.5.5

  • Add server.json for MCP registry

0.5.0

  • Introduce new tool: SearchAndGeocodeTool
  • Remove former tools: ForwardGeocodeTool, PoiSearchTool; their capabilities are combined in the new tool

0.4.1

  • Minor changes to tool descriptions for clarity

0.4.0 (Unreleased)

Features Added

  • New fetch pipeline with automatic retry behavior

Bug Fixes

  • Dual emits ESM and CommonJS bundles with types per target

Other Features

  • Migrated from Jest to vitest

v0.2.0 (2025-06-25)

  • Format Options: Add format parameter to all geocoding and search tools
    • CategorySearchTool, ForwardGeocodeTool, PoiSearchTool, and ReverseGeocodeTool now support both json_string and formatted_text output formats
    • json_string returns raw GeoJSON data as parseable JSON string
    • formatted_text returns human-readable text with place names, addresses, and coordinates
    • Default to formatted_text for backward compatibility
    • Comprehensive test coverage for both output formats

v0.1.0 (2025-06-12)

  • Support-NPM-Package: Introduce the NPM package of this mcp-server

v0.0.1 (2025-06-11)

  • First tag release