Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
## Unreleased

### 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 input schema. The Places API's Details endpoint takes only a `mapbox_id` and has no equivalent parameters.
- **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. `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.
- We are not supporting both APIs going forward, since the server isn't at 1.0 yet.

### 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.
Expand Down
18 changes: 0 additions & 18 deletions src/tools/place-details-tool/PlaceDetailsTool.input.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,6 @@ export const PlaceDetailsInputSchema = z.object({
.string()
.describe(
'The Mapbox ID of the place to retrieve details for. Obtained from search results returned by search_and_geocode_tool, category_search_tool, or reverse_geocode_tool (the mapbox_id field in properties).'
),
attribute_sets: z
.array(z.enum(['basic', 'photos', 'visit', 'venue']))
.optional()
.describe(
'Which attribute sets to include in the response. Options: "basic" (name/address/coordinates — always requested from the API regardless of whether you list it here, since it\'s required for a valid response), "photos" (place photo URLs), "visit" (opening hours, rating, price level, popularity), "venue" (phone number, website URL, social media links). When not specified, only basic attributes are returned.'
),
language: z
.string()
.optional()
.describe(
'BCP 47 language tag for localized results (e.g. "en", "fr", "de", "ja"). Affects place names and address formatting.'
),
worldview: z
.enum(['ar', 'cn', 'in', 'jp', 'ma', 'ru', 'tr', 'us'])
.optional()
.describe(
'Worldview for geopolitically sensitive content such as disputed borders. Options: "ar" (Argentina), "cn" (China), "in" (India), "jp" (Japan), "ma" (Morocco), "ru" (Russia), "tr" (Turkey), "us" (United States, default).'
)
});

Expand Down
96 changes: 62 additions & 34 deletions src/tools/place-details-tool/PlaceDetailsTool.output.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,77 @@ import { z } from 'zod';
/**
* Output schema for PlaceDetailsTool
*
* Models the GeoJSON Feature returned by the Mapbox Details API.
* Uses passthrough() to allow additional fields from optional attribute_sets
* (photos, visit, venue) to pass through without validation errors.
* Models the flat Place record returned by the Mapbox Places API's Details
* endpoint (`places/v1/details/retrieve`), currently in Public Preview.
* Nearly every field beyond `mapbox_id`/`name` is optional and every nested
* object uses .passthrough() — live responses observed during development
* varied in which fields were present (e.g. `building` and `opening_hours`
* only appear on some places), and a Public Preview API's contract can add
* fields without notice, so this schema is deliberately permissive rather
* than risk an output-validation failure like the one fixed for the
* previous Details API (see CHANGELOG).
*/
export const PlaceDetailsOutputSchema = z
.object({
type: z.literal('Feature'),
geometry: z
mapbox_id: z.string(),
name: z.string(),
full_address: z.string().optional(),
brand: z.string().nullable().optional(),
primary_category: z.string().optional(),
categories: z.array(z.string()).optional(),
// OSM opening_hours syntax, e.g. "Mo 09:00-23:45; Tu 09:00-23:45; ...".
opening_hours: z.string().optional(),
permanently_closed: z.boolean().nullable().optional(),
phone: z.string().optional(),
website: z.string().optional(),
status: z.string().optional(),
created_at: z.string().optional(),
updated_at: z.string().optional(),
score: z
.object({
type: z.literal('Point'),
coordinates: z.tuple([z.number(), z.number()])
closed: z.number().nullable().optional(),
reality: z.number().nullable().optional(),
popularity: z.number().nullable().optional()
})
.passthrough(),
properties: z
.passthrough()
.optional(),
coordinates: z
.object({
name: z.string(),
mapbox_id: z.string(),
feature_type: z.string(),
address: z.string().optional(),
full_address: z.string().optional(),
place_formatted: z.string().optional(),
context: z.object({}).passthrough().optional(),
coordinates: z
latitude: z.number(),
longitude: z.number(),
source: z.string().optional(),
routable_points: z
.array(
z
.object({
name: z.string().optional(),
latitude: z.number(),
longitude: z.number()
})
.passthrough()
)
.optional()
})
.passthrough()
.optional(),
address: z.object({}).passthrough().optional(),
attributes: z
.record(z.string(), z.union([z.string(), z.boolean(), z.number()]))
.optional(),
building: z.object({}).passthrough().optional(),
photos: z
.array(
z
.object({
longitude: z.number(),
latitude: z.number()
url: z.string(),
width: z.number().nullable().optional(),
height: z.number().nullable().optional(),
source: z.string().optional()
})
.passthrough()
.optional(),
bbox: z
.tuple([z.number(), z.number(), z.number(), z.number()])
.optional(),
language: z.string().optional(),
maki: z.string().optional(),
poi_category: z.array(z.string()).optional(),
poi_category_ids: z.array(z.string()).optional(),
brand: z.array(z.string()).optional(),
brand_id: z.array(z.string()).optional(),
external_ids: z.record(z.string(), z.string()).optional(),
// metadata contains attribute_set fields (photos, visit, venue)
metadata: z.object({}).passthrough().optional()
})
.passthrough()
)
.optional(),
telemetry: z.object({}).passthrough().optional()
})
.passthrough();

Expand Down
Loading
Loading