diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b77d74b..3fb06a06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## 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`. Only ChatGPT Desktop offers them to the model, and not when several maps are open; on the web, a follow-up opens another map. + - **Pointing the model at the open map:** the server instructions, the `render_map_tool` description and the render hint data tools append name these tools, each saying it applies in ChatGPT Desktop, since ChatGPT on the web registers the tools but doesn't offer them to the model. In ChatGPT, the result of a call that opens a map names them too. The map's shared context doesn't name them. Other clients get the same instructions as before plus that one line. + - **Layout in the host:** the map keeps its controls, labels and cards inside the area the host leaves uncovered (`hostContext.safeAreaInsets`), such as below ChatGPT's title bar on a phone, while the map itself runs underneath. Inline, it asks for no more height than the host allows (`containerDimensions.maxHeight`). `render_map_tool` has a square Mapbox mark icon for the entrypoint, since the wide server logo is unreadable in ChatGPT's "+" menu; `BaseTool` now passes `icons` through to `registerTool`. + ## 0.18.0 - 2026-11-11 ### Features diff --git a/README.md b/README.md index ed3b51cc..c9dca3ed 100644 --- a/README.md +++ b/README.md @@ -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 ChatGPT Desktop 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. On the web, a follow-up opens another map. 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)** diff --git a/src/resources/ui-apps/MapAppUIResource.ts b/src/resources/ui-apps/MapAppUIResource.ts index cc20d8fb..906a938e 100644 --- a/src/resources/ui-apps/MapAppUIResource.ts +++ b/src/resources/ui-apps/MapAppUIResource.ts @@ -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'] } } } diff --git a/src/resources/ui-apps/mapAppHtml.ts b/src/resources/ui-apps/mapAppHtml.ts index 527f7858..0c5c0e7f 100644 --- a/src/resources/ui-apps/mapAppHtml.ts +++ b/src/resources/ui-apps/mapAppHtml.ts @@ -2,6 +2,9 @@ // Licensed under the MIT License. import type { MapAppPayload } from '../../utils/mapAppPayload.js'; +import { MAP_APP_SELECTION_JS } from './mapAppSelectionJs.js'; +import { MAP_APP_MODEL_CONTEXT_JS } from './mapAppModelContextJs.js'; +import { MAP_APP_TOOLS_JS } from './mapAppToolsJs.js'; /** * Render the generic Mapbox MCP App HTML — served by the MCP Apps resource @@ -74,6 +77,20 @@ export function renderMapAppHtml(params: { html, body { width: 100%; height: 100%; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #map { position: absolute; inset: 0; } + /* The map runs under the host's own controls, such as ChatGPT's title bar + on a phone; what sits on it stays inside the area the host leaves + uncovered (hostContext.safeAreaInsets). */ + #frame { + position: absolute; pointer-events: none; + top: var(--safe-top, 0px); right: var(--safe-right, 0px); + bottom: var(--safe-bottom, 0px); left: var(--safe-left, 0px); + } + #frame > * { pointer-events: auto; } + #frame > #loading { pointer-events: none; } + #map .mapboxgl-ctrl-top-left { top: var(--safe-top, 0px); left: var(--safe-left, 0px); } + #map .mapboxgl-ctrl-top-right { top: var(--safe-top, 0px); right: var(--safe-right, 0px); } + #map .mapboxgl-ctrl-bottom-left { bottom: var(--safe-bottom, 0px); left: var(--safe-left, 0px); } + #map .mapboxgl-ctrl-bottom-right { bottom: var(--safe-bottom, 0px); right: var(--safe-right, 0px); } #summary { position: absolute; top: 12px; left: 12px; z-index: 10; background: var(--ui-surface); color: var(--ui-text); @@ -210,6 +227,7 @@ export function renderMapAppHtml(params: {
+
@@ -218,6 +236,7 @@ export function renderMapAppHtml(params: {
Loading…
+
${initialDataScript}