Skip to content
Draft
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
## Unreleased

### Features

- **ChatGPT: a map that stays open next to the chat (#333).** In ChatGPT, `render_map_tool` opens its map as a tab beside the conversation instead of inline (`preferredModelDisplayMode: "fullscreen"` on the tool, `preferredDisplayMode` on the UI resource), and the map works with the chat while it is open. Hosts without the OpenAI MCP Extensions ignore the `openai/ui` metadata and `document.modelContext`.
- **Open it yourself:** `render_map_tool` is a thread entrypoint (`_meta["openai/ui"].entrypoints: [{ type: "thread" }]`), listed as **Mapbox Map** under "+" → "More tools". Called with `{}`, as an entrypoint is, it opens an empty map. `{}` used to be an error; a call whose refs all fail to resolve still is. The tool is now titled **Mapbox Map** (was "Render Map"), since the title is what the tab and the entrypoint show.
- **Map to chat:** the open map shares its view (center, zoom, bounds) as background context (`ui/update-model-context` `structuredContent`), so "what's around here?" works without naming the place. A place the user selects is a titled item in the composer. A place is a pin, a search result, or a label of the Standard base map: points of interest, place names, landmark icons and indoor labels, the label featuresets Standard offers. A selected label is highlighted (POIs, which have only a `hide` state, are hidden under a selected pin), a second click deselects it, and a click on another moves the selection. ChatGPT clears the context both when a message is sent and when the user removes the item, so the selection stays on the map, and the place isn't attached again until the user selects one. An inline map shares nothing.
- **Chat to map (WebMCP):** the map registers tools through `document.modelContext` that act on the open map, so "show coffee shops on it" no longer opens a second map: `redraw_map` (takes `render_map_tool`'s arguments; the map calls `render_map_tool` itself and redraws in place), `search_map`, `add_places`, `move_map` and `read_map`. ChatGPT offers them in agent mode (Codex) only, not in an Instant chat, and not when several maps are open.
- **Pointing the model at the open map:** the server instructions, the `render_map_tool` description and result, the render hint data tools append, and the map's shared context point the model at these tools. A new map asks the model to call `read_map` once, since the model only started using the map's tools after calling one; in ChatGPT it also says firmly to update this map rather than open another.
- **Tileset explorer: `tileset_explorer_tool` (#335).** Previews a tileset's tiles on an interactive map (`ui://mapbox/tileset-explorer/index.html`). Takes a `tileset_id` (`username.id`; several vector tilesets joined with commas), reads its TileJSON and returns the source layers, their fields and the zoom range.
- **The map:** every source layer is drawn in its own color (polygons, lines and points) over a blank background, framed to the tileset's bounds and never below its minzoom. The panel switches layers, lists their fields, shows tile boundaries and adds a base map. A click shows a feature's layer, geometry, ID and properties. A raster tileset is one raster layer.
- **In ChatGPT:** opens as a tab next to the chat, and is a thread entrypoint (**Mapbox Tileset Explorer**) that starts on `mapbox.mapbox-streets-v8`. An open explorer shares the tileset, its hidden layers and the view as background context, and the selected feature as a titled item the user can remove. It registers two WebMCP tools: `load_tileset` (calls `tileset_explorer_tool` and shows the result in the open explorer) and `read_tileset_view` (features in view per source layer, with up to three samples each).
- **Tokens:** the TileJSON `tiles` URLs carry the caller's token, so only metadata is returned. The page loads tiles through `mapbox://` with the account's public token; without one it still lists layers and fields.

## 0.18.0 - 2026-11-11

### Features
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ Every geospatial tool in this server (directions, isochrone, search, and more) c

If you need a guaranteed static image in clients without MCP Apps support, use `static_map_image_tool` instead — it returns a base64-encoded PNG/JPEG that every client can display.

#### A map next to the chat in ChatGPT

In ChatGPT, `render_map_tool` maps open as a tab next to the chat and stay open. You can also open an empty map from "+" → "More tools" → **Mapbox Map**. The map shares its view and the selected place (a pin, a search result, or a base map label such as a shop or a neighborhood) with the conversation, and in agent mode it registers WebMCP tools (`redraw_map`, `search_map`, `add_places`, `move_map`, `read_map`) so the model draws on the open map instead of opening another. Hosts without the [OpenAI MCP Extensions](https://developers.openai.com/plugins/build/extensions) ignore all of this.

#### CLIENT_NEEDS_RESOURCE_FALLBACK

**Resource Fallback Tools (Opt-In for Non-Compliant Clients)**
Expand All @@ -187,6 +191,14 @@ export CLIENT_NEEDS_RESOURCE_FALLBACK=true

## Tools

### Tileset explorer (`tileset_explorer_tool`)

Previews a Mapbox tileset's tiles on an interactive map. Pass a `tileset_id` (`username.id`, for example `mapbox.mapbox-streets-v8`, `mapbox.satellite` or one of your own; join several vector tilesets with commas). The tool returns the tileset's source layers, their fields and its zoom range, and the map draws every source layer in its own color over a blank background. The panel switches layers on and off, lists each layer's fields, shows tile boundaries, and puts a light or dark base map underneath. Click a feature to see its properties. Raster tilesets are drawn as they are.

In ChatGPT the explorer opens as a tab next to the chat, and also from "+" → "More tools" → **Mapbox Tileset Explorer**, where it starts on Mapbox Streets. While open it shares the tileset, the view and the selected feature with the conversation, and in agent mode it registers two WebMCP tools, `load_tileset` and `read_tileset_view`, so the model switches tilesets and reads the features in view in the open explorer instead of opening another.

The server reads the TileJSON with the caller's token and passes on only its metadata, never the tile URLs, which carry that token. The map loads tiles with the account's public token, so a private tileset shows only for its own account.

### Utility Tools

#### Resource Reader Tool
Expand Down
3 changes: 3 additions & 0 deletions src/resources/resourceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { InlinePayloadResource } from './inline-payload/InlinePayloadResource.js
import { ToolResponseResource } from './tool-response/ToolResponseResource.js';
import { MapAppUIResource } from './ui-apps/MapAppUIResource.js';
import { StaticMapViewerUIResource } from './ui-apps/StaticMapViewerUIResource.js';
import { TilesetExplorerUIResource } from './ui-apps/TilesetExplorerUIResource.js';
import { VersionResource } from './version/VersionResource.js';
import { PlacePhotoResource } from './place-photo/PlacePhotoResource.js';
import { httpRequest } from '../utils/httpPipeline.js';
Expand All @@ -25,6 +26,8 @@ export const ALL_RESOURCES = [
new MapAppUIResource({ httpRequest }),
// Full-width viewer for static_map_image_tool's image.
new StaticMapViewerUIResource(),
// Tileset preview, targeted by tileset_explorer_tool.
new TilesetExplorerUIResource({ httpRequest }),
// Photos for the map app's results carousel, which its CSP can't load directly.
new PlacePhotoResource({ httpRequest }),
new VersionResource()
Expand Down
6 changes: 6 additions & 0 deletions src/resources/ui-apps/MapAppUIResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export class MapAppUIResource extends BaseResource {
ui: {
csp: mapAppCsp(this.apiEndpoint()),
preferredSize: { width: 1000, height: 600 }
},
// ChatGPT: the map opens as a tab next to the chat, where it
// stays open across turns, rather than inline.
'openai/ui': {
preferredDisplayMode: 'fullscreen',
availableDisplayModes: ['inline', 'fullscreen']
}
}
}
Expand Down
73 changes: 73 additions & 0 deletions src/resources/ui-apps/TilesetExplorerUIResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Mapbox, Inc.
// Licensed under the MIT License.

import type {
ReadResourceResult,
ServerContext
} from '@modelcontextprotocol/server';
import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
import { BaseResource } from '../BaseResource.js';
import type { HttpRequest } from '../../utils/types.js';
import { resolveMapboxPublicToken } from '../../utils/mapboxPublicToken.js';
import { renderTilesetExplorerHtml } from './tilesetExplorerHtml.js';
import { mapAppCsp } from './mapAppCsp.js';

export const TILESET_EXPLORER_URI = 'ui://mapbox/tileset-explorer/index.html';

/** UI for `tileset_explorer_tool`. */
export class TilesetExplorerUIResource extends BaseResource {
readonly name = 'Mapbox Tileset Explorer UI';
readonly uri = TILESET_EXPLORER_URI;
readonly description =
"Interactive preview of a Mapbox tileset's tiles, source layers and features (MCP Apps)";
readonly mimeType = RESOURCE_MIME_TYPE;

private readonly httpRequest: HttpRequest;
private readonly apiEndpoint: () => string;

constructor(params: {
httpRequest: HttpRequest;
apiEndpoint?: () => string;
}) {
super();
this.httpRequest = params.httpRequest;
this.apiEndpoint =
params.apiEndpoint ??
(() => process.env.MAPBOX_API_ENDPOINT || 'https://api.mapbox.com/');
}

async read(_uri: string, extra?: ServerContext): Promise<ReadResourceResult> {
const accessToken =
(extra?.http?.authInfo?.token as string | undefined) ||
process.env.MAPBOX_ACCESS_TOKEN ||
'';

const publicToken = accessToken
? await resolveMapboxPublicToken({
accessToken,
apiEndpoint: this.apiEndpoint(),
httpRequest: this.httpRequest
})
: undefined;

return {
contents: [
{
uri: this.uri,
mimeType: RESOURCE_MIME_TYPE,
text: renderTilesetExplorerHtml({ publicToken: publicToken ?? '' }),
_meta: {
ui: {
csp: mapAppCsp(this.apiEndpoint()),
preferredSize: { width: 1000, height: 600 }
},
'openai/ui': {
preferredDisplayMode: 'fullscreen',
availableDisplayModes: ['inline', 'fullscreen']
}
}
}
]
};
}
}
Loading