diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea83e61..86526a4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Unreleased +### 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`](./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 diff --git a/README.md b/README.md index c3d7032f..74b87c0e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The Mapbox MCP Server transforms any AI agent or application into a geospatially - **Route optimization** to find the optimal visiting order for multiple stops (traveling salesman problem) - **Map matching** to snap GPS traces to the road network for clean route visualization - **Isochrone generation** to visualize areas reachable within specific time or distance constraints +- **Live, interactive map rendering** (`render_map_tool`) to display routes, search results, and your own custom GeoJSON on a real Mapbox GL JS map directly inside the chat - **Static map images** to create visual representations of locations, routes, and geographic data - **Offline geospatial calculations** for distance, area, bearing, buffers, and spatial analysis without requiring API calls @@ -52,6 +53,7 @@ For detailed setup instructions for different integrations, refer to the followi - [Cursor AI IDE Setup](./docs/cursor-setup.md) - Setting up a development environment in Cursor AI IDE - [Smolagents Integration](./docs/using-mcp-with-smolagents/README.md) - Example showing how to connect Smolagents AI agents to Mapbox's tools - **[Importing Tools Directly](./docs/importing-tools.md)** - Use Mapbox tools in your own applications without running the MCP server +- **[`render_map_tool` Guide](./docs/render-map-tool.md)** - The map visualization primitive: full payload schema and how to render your own data standalone, without any other Mapbox tool ## Example Prompts @@ -72,9 +74,11 @@ Try these prompts with Claude Desktop or other MCP clients after setup: ### Visualization & Maps +- "Using the Mapbox map render tool, show me directions from the Golden Gate Bridge to Union Square in San Francisco" — renders a live, interactive route on a real Mapbox map - "Create a map image showing the route from Golden Gate Bridge to Fisherman's Wharf with markers at both locations" - "Show me a satellite view of Manhattan with key landmarks marked" - "Generate a map highlighting all Starbucks locations within a mile of downtown Seattle" +- "Show a fill polygon over these coordinates: [...], with a marker labeled 'Warehouse' at [...]" — renders your own GeoJSON directly via `render_map_tool`, no other Mapbox tool needed ### Analysis & Planning @@ -133,24 +137,18 @@ Access the complete list of available category IDs for use with the category sea - **Clients with native MCP resource support**: Use the `resources/read` MCP protocol method - **Clients without resource support**: Use the `resource_reader_tool` with the resource URI -## Rich Map Previews (MCP Apps) +## Rich Map Previews (`render_map_tool`) -The `static_map_image_tool` provides an interactive map preview panel in compatible clients, in addition to the base64 image that all clients receive. - -This server implements the **MCP Apps** protocol (`@modelcontextprotocol/ext-apps`), which renders a self-contained HTML app panel directly inside the chat. Supported clients show an interactive map with a Fullscreen toggle: +Every geospatial tool in this server (directions, isochrone, search, and more) can display its result as a live, interactive Mapbox GL JS map via **`render_map_tool`** — the server's single visualization primitive. It renders through the **MCP Apps** protocol (`@modelcontextprotocol/ext-apps`) as a self-contained HTML panel directly inside the chat, with a Fullscreen toggle, in supported clients: - **Claude Desktop** ✅ - **VS Code with GitHub Copilot** ✅ - **Claude Code** ✅ - **[Goose](https://github.com/block/goose)** ✅ -All clients receive the base64-encoded map image regardless of protocol support — interactive previews are a progressive enhancement on top of the standard image response. - -### Legacy: MCP-UI +**You don't need any of this server's other tools to use it.** `render_map_tool` also accepts hand-composed GeoJSON directly — your own polygons, markers, and routes — with no dependency on `directions_tool`, `isochrone_tool`, or any other Mapbox API call. See **[the full `render_map_tool` guide](./docs/render-map-tool.md)** for the payload schema and a complete standalone example. -This server also retains support for **MCP-UI** (`@mcp-ui/server`), an earlier open specification for embedded iframe previews. MCP Apps is the recommended protocol; MCP-UI support is kept for backwards compatibility. - -MCP-UI is enabled by default. To disable it, pass `--disable-mcp-ui` as a command-line flag or set `ENABLE_MCP_UI=false`. See the [MCP-UI documentation](./docs/mcp-ui.md) for details. +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. #### CLIENT_NEEDS_RESOURCE_FALLBACK @@ -210,17 +208,30 @@ Calculate the distance between two geographic coordinates using the Haversine fo **Example Usage**: "What's the distance between San Francisco (37.7749°N, 122.4194°W) and New York (40.7128°N, 74.0060°W)?" -#### Point in Polygon Tool +#### Points Within Polygon Tool -Test whether a point is inside a polygon or multipolygon. +Test one or more points against a polygon or multipolygon, returning only those inside. Handles a single point or a batch in one call. **Features**: - Works with complex polygons including holes - Supports multipolygons -- Useful for geofencing and service area checks +- Batch-tests any number of points in one call +- Useful for geofencing, delivery zone validation, and customer segmentation + +**Example Usage**: "Which of these delivery addresses are inside our service area?" -**Example Usage**: "Is this delivery address inside our service area?" +#### Destination Tool + +Calculate a destination point given a starting point, bearing, and distance using geodesic (great-circle) offset. + +**Features**: + +- Straight-line offset, not a routed path +- Useful for "find a point 5km north of X" or constructing search offsets +- No API calls required + +**Example Usage**: "What's the point 10km northeast of the Space Needle?" #### Bearing Tool @@ -307,6 +318,61 @@ Reduce the number of vertices in a line or polygon using the Douglas-Peucker alg **Example Usage**: "Simplify this complex boundary to reduce the number of points" +#### Length Tool + +Measure the total length of a line defined by a series of coordinates. + +**Features**: + +- Supports kilometers, miles, meters, and feet +- Useful for measuring a drawn route, path, or boundary without a routing API call + +**Example Usage**: "How long is this hiking trail?" + +#### Convex Tool + +Compute the convex hull of a set of points — the smallest convex polygon containing all of them. + +**Features**: + +- Useful for bounding-area analysis or estimating coverage area +- Works offline without API calls + +**Example Usage**: "What's the smallest polygon that contains all of these store locations?" + +#### Nearest Point Tool + +Find the nearest point in a collection to a given target point. + +**Features**: + +- More efficient than calling `distance_tool` for each candidate and sorting +- Useful for finding the closest store, stop, or landmark to a location + +**Example Usage**: "Which of these stores is closest to my current location?" + +#### Nearest Point on Line Tool + +Snap a point to the nearest position on a line or route, returning that point and the distance to it. + +**Features**: + +- Useful for "which point on this route is closest to my location?" or map-matching without an API call + +**Example Usage**: "Where on this hiking trail am I closest to right now?" + +#### Union, Intersect, and Difference Tools + +Combine or compare two or more polygons — `union_tool` merges them into one geometry, `intersect_tool` finds the area they share, and `difference_tool` subtracts one from another. + +**Features**: + +- Useful for combining service areas, finding coverage overlap, or computing exclusion zones (e.g. "what's covered by zone A but not zone B?") +- Works entirely offline — no API calls required +- Each returns a `render_map_tool` reference so the result can be visualized directly + +**Example Usage**: "Combine these two delivery zones into one coverage area" / "Where do these two isochrones overlap?" / "What part of this service area isn't covered by our 15-minute isochrone?" + ### Mapbox API Tools #### Category List Tool (Deprecated) @@ -355,6 +421,25 @@ Performs reverse geocoding using the [Mapbox geocoding V6 API](https://docs.mapb - Support for multiple languages - Rich location context information +#### Ground location tool + +Answers "what's near this coordinate" questions in a single call — place name, nearby points of interest, and travel-time reachability — sourced from live Mapbox data with citations. Use this instead of chaining `reverse_geocode_tool` with a web search. + +**Features**: + +- Classifies the query (routing, neighborhood context, POI search, or region/reachability) and fetches only the relevant data +- Nearby POI search by category, when requested +- Travel-time reachability summary (isochrone-based) +- Returns a `render_map_tool` reference with the grounded location and nearby POIs plotted + +**Example Usage**: "What neighborhood is this coordinate in, and are there any coffee shops nearby?" + +#### Place details tool + +Retrieves detailed information about a specific place using its Mapbox ID — photos, opening hours, ratings, phone numbers, and website URLs. Use after `search_and_geocode_tool`, `category_search_tool`, or `reverse_geocode_tool` to get more detail on a specific result. + +**Example Usage**: "Tell me more about that first coffee shop result — hours, phone number, and website" + #### Directions tool Fetches routing directions using the [Mapbox Directions API](https://docs.mapbox.com/api/navigation/directions/). Features include: diff --git a/docs/mcp-ui.md b/docs/mcp-ui.md index e4c8f566..5a3ab48d 100644 --- a/docs/mcp-ui.md +++ b/docs/mcp-ui.md @@ -1,342 +1,22 @@ -# Interactive Map Previews (MCP Apps & MCP-UI) +# Interactive Map Previews -This document covers how the Mapbox MCP Server delivers interactive map previews in compatible clients. +> **MCP-UI support has been removed from this server.** Interactive map previews are now handled exclusively through **[`render_map_tool`](./render-map-tool.md)** via the MCP Apps protocol. This page is kept at its existing URL to avoid breaking old links; see [render-map-tool.md](./render-map-tool.md) for the current, comprehensive guide. -## Table of Contents +## What changed -- [Overview](#overview) -- [Supported Tools](#supported-tools) -- [Compatible Clients](#compatible-clients) -- [How It Works](#how-it-works) -- [Configuration](#configuration) -- [Technical Details](#technical-details) -- [Examples](#examples) -- [Troubleshooting](#troubleshooting) +- **Before**: `static_map_image_tool` conditionally attached an MCP-UI `UIResource` (an embedded iframe) alongside its base64 image, and separately declared an MCP Apps resource (`StaticMapUIResource`) for hosts that supported it. Both were controlled by the `ENABLE_MCP_UI` env var / `--disable-mcp-ui` CLI flag. +- **Now**: `static_map_image_tool` returns the Mapbox Static Images API URL (as text) and a base64-encoded image only — a plain, static result with no interactive panel of its own. Live, interactive map rendering is the responsibility of the single dedicated visualization tool, `render_map_tool`, which every geospatial tool in this server (including one you compose yourself from raw GeoJSON) can feed into. +- The `ENABLE_MCP_UI` environment variable and `--disable-mcp-ui` CLI flag still parse without error for backwards compatibility, but no longer have any effect — there is nothing left for them to toggle. -## Overview +## Where to go instead -The Mapbox MCP Server actively invests in **MCP Apps** as its primary interactive preview protocol. MCP Apps serves self-contained HTML app panels directly inside the chat, and is supported by Claude Desktop, VS Code with GitHub Copilot, and Claude Code. +- **[`render_map_tool` guide](./render-map-tool.md)** — the current interactive map preview mechanism, including how to call it standalone with your own data (no other Mapbox tool required) or chained from another tool's result. +- **`static_map_image_tool`** — still the right choice when you need a guaranteed image in _every_ client regardless of MCP Apps support (e.g. a client with no interactive-preview capability at all). -The server also maintains support for **MCP-UI** for backwards compatibility with clients like Goose. MCP-UI is not being removed, but new interactive preview development is focused on MCP Apps. +## Why the change -All clients — regardless of protocol support — receive the base64-encoded map image as standard output. - -### Key Benefits - -- **Rich interactive previews**: MCP Apps clients get a full HTML map panel with a Fullscreen toggle -- **Full backwards compatibility**: Clients without MCP Apps or MCP-UI support still receive the base64 image -- **Progressive enhancement**: Every client gets a usable result; supporting clients get a richer experience - -## Supported Tools - -### Static Map Image Tool (`static_map_image_tool`) - -This tool always returns: - -1. **The Mapbox Static Images API URL** (text, required first for MCP Apps to render) -2. **Base64-encoded map image** (for all clients without interactive preview support) -3. **MCP-UI UIResource** (only when MCP-UI is enabled, for Goose and other MCP-UI clients) - -**MCP Apps clients** (Claude Desktop, VS Code, Claude Code): Render the interactive HTML panel served by `StaticMapUIResource`, with click-to-zoom and a Fullscreen toggle. - -**MCP-UI clients** (Goose): Render the embedded iframe resource. - -**Standard clients**: Display the base64 image inline. - -## Compatible Clients - -| Client | MCP Apps | MCP-UI | -| --------------------------------------- | -------- | ------ | -| Claude Desktop | ✅ | | -| VS Code with GitHub Copilot | ✅ | | -| Claude Code | ✅ | | -| [Goose](https://github.com/block/goose) | ✅ | ✅ | -| Cursor IDE | | | -| Other clients | | | - -> **Note**: Client compatibility may change as adoption of both protocols grows. Check your client's documentation for the latest support status. - -All clients in the table receive the base64 image regardless of protocol support. - -## How It Works - -### Architecture - -``` -┌─────────────────┐ -│ MCP Client │ -│ (e.g., Goose) │ -└────────┬────────┘ - │ - │ MCP Protocol - │ -┌────────▼────────────────────┐ -│ Mapbox MCP Server │ -│ ┌────────────────────────┐ │ -│ │ static_map_image_tool │ │ -│ └────────┬───────────────┘ │ -│ │ │ -│ ├─► Text description│ -│ │ (always) │ -│ ├─► Base64 image │ -│ │ (always) │ -│ └─► UIResource │ -│ (if enabled) │ -└─────────────────────────────┘ -``` - -### Response Format - -The `static_map_image_tool` returns a response with multiple content items, following the progressive enhancement pattern: - -```typescript -{ - content: [ - { - // The Mapbox Static Images API URL — MCP Apps reads this to render the map - type: 'text', - text: 'https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/...' - }, - { - // Base64-encoded image — for all clients without interactive preview support - type: 'image', - data: '', - mimeType: 'image/png' - }, - { - // MCP-UI resource for interactive iframes — only present when MCP-UI is enabled - type: 'resource', - resource: { - uri: 'ui://mapbox/static-map/...', - mimeType: 'text/html', - text: '...', - uiMetadata: { - 'preferred-frame-size': ['800px', '600px'] - } - } - } - ]; -} -``` - -**MCP Apps clients** (Claude Desktop, VS Code, Claude Code) render the interactive HTML panel served via `StaticMapUIResource`. The URL in `content[0]` is used by the panel to fetch and display the map image. - -**MCP-UI clients** (Goose) render the iframe resource for an inline preview. - -**Standard clients** (Cursor, etc.) render the base64 image from `content[1]`. - -## Configuration - -MCP-UI is **enabled by default**. You can disable it using either an environment variable or command-line flag. - -### Disable via Environment Variable - -```bash -export ENABLE_MCP_UI=false -npm run build -``` - -Or in your client configuration: - -**Claude Desktop (`claude_desktop_config.json`):** - -```json -{ - "mcpServers": { - "mapbox": { - "command": "npx", - "args": ["-y", "@mapbox/mcp-server"], - "env": { - "MAPBOX_ACCESS_TOKEN": "your_token_here", - "ENABLE_MCP_UI": "false" - } - } - } -} -``` - -**VS Code (`settings.json`):** - -```json -{ - "mcp": { - "servers": { - "MapboxServer": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@mapbox/mcp-server"], - "env": { - "MAPBOX_ACCESS_TOKEN": "your_token", - "ENABLE_MCP_UI": "false" - } - } - } - } -} -``` - -### Disable via Command-Line Flag - -```bash -npx @mapbox/mcp-server --disable-mcp-ui -``` - -Or in your client configuration: - -**Claude Desktop:** - -```json -{ - "mcpServers": { - "mapbox": { - "command": "npx", - "args": ["-y", "@mapbox/mcp-server", "--disable-mcp-ui"], - "env": { - "MAPBOX_ACCESS_TOKEN": "your_token_here" - } - } - } -} -``` - -### Priority - -If both the environment variable and command-line flag are specified, the **environment variable takes precedence**. - -## Technical Details - -### Implementation - -The Mapbox MCP Server supports two interactive preview protocols: - -- **MCP Apps** (`@modelcontextprotocol/ext-apps`) — serves a self-contained HTML app via `StaticMapUIResource` with `RESOURCE_MIME_TYPE` (`text/html;profile=mcp-app`). Supported by Claude Desktop, VS Code, and Claude Code. -- **MCP-UI** (`@mcp-ui/server`) — creates `UIResource` objects with embedded iframe URLs. Supported by Goose. - -**Key Files:** - -- `src/config/toolConfig.ts` - Configuration logic for MCP-UI (`isMcpUiEnabled()` helper) -- `src/tools/static-map-image-tool/StaticMapImageTool.ts` - Fetch + base64 encoding, URL as first content item, conditional UIResource -- `src/resources/ui-apps/StaticMapUIResource.ts` - MCP Apps HTML panel implementation -- `src/tools/BaseTool.ts` - `meta.ui` property for MCP Apps CSP configuration - -**Code Example:** - -```typescript -import { createUIResource } from '@mcp-ui/server'; -import { isMcpUiEnabled } from '../../config/toolConfig.js'; - -// content[0] MUST be the URL — MCP Apps UI finds it via content.find(c => c.type === 'text') -const content: CallToolResult['content'] = [ - { - type: 'text', - text: url // Mapbox Static Images API URL - }, - { - type: 'image', - data: base64Data, - mimeType - } -]; - -// Conditionally add MCP-UI resource if enabled (for Goose and other MCP-UI clients) -if (isMcpUiEnabled()) { - const uiResource = createUIResource({ - uri: `ui://mapbox/static-map/${input.style}/${lng},${lat},${input.zoom}`, - content: { - type: 'externalUrl', - iframeUrl: url - }, - encoding: 'text', - uiMetadata: { - 'preferred-frame-size': [`${width}px`, `${height}px`] - } - }); - content.push(uiResource); -} - -return { content, isError: false }; -``` - -### UIResource Structure - -The UIResource includes: - -- **uri**: Unique identifier for the resource (e.g., `ui://mapbox/static-map/...`) -- **content.type**: Set to `'externalUrl'` to indicate an iframe -- **content.iframeUrl**: The actual URL to embed (Mapbox Static Images API URL) -- **encoding**: Set to `'text'` -- **uiMetadata**: Optional metadata including preferred dimensions - -## Examples - -### Using Goose with MCP-UI - -1. Install Goose (follow [Goose installation instructions](https://github.com/block/goose)) - -2. Configure the Mapbox MCP Server in Goose's configuration - -3. Use natural language to request a map: - - ``` - "Show me a map of downtown San Francisco centered at 37.7749, -122.4194" - ``` - -4. Goose will display an **inline interactive map** rather than just a static image - -### Example Prompt for Static Map Tool - -``` -Create a map showing the route from Golden Gate Bridge to Fisherman's Wharf -with markers at both locations -``` - -**MCP Apps clients (Claude Desktop, VS Code, Claude Code):** - -- Displays interactive HTML map panel with Fullscreen toggle - -**MCP-UI clients (Goose):** - -- Displays interactive embedded map via iframe - -**Standard clients (Cursor, etc.):** - -- Displays base64-encoded static PNG image - -All clients provide the same map information; the experience differs based on client capabilities. - -## Troubleshooting - -### "I'm not seeing interactive maps, only static images" - -**Solution**: Check that: - -1. Your MCP client supports MCP-UI (see [Compatible Clients](#compatible-clients)) -2. MCP-UI is enabled (check environment variable and command-line flags) -3. Your client is properly configured to render MCP-UI resources - -### "How do I verify MCP-UI is enabled?" - -**Solution**: Check the server logs or test with a known MCP-UI compatible client like Goose. When MCP-UI is enabled, the `static_map_image_tool` will return 3 content items (URL text + image + UIResource) instead of 2 (URL text + image). - -### "Can I use MCP-UI with custom tools?" - -**Solution**: Yes! If you're developing custom tools for this server: - -1. Add dependency: `@mcp-ui/server` -2. Import `createUIResource` and `isMcpUiEnabled()` -3. Conditionally add UIResource to your tool's response -4. Follow the pattern in `StaticMapImageTool.ts` - -### "Does disabling MCP-UI affect functionality?" - -**Solution**: No. Disabling MCP-UI only removes the iframe URLs from responses. All tools continue to function normally, returning their standard output (text, images, JSON, etc.). - -## Resources - -- [MCP-UI Specification](https://github.com/modelcontextprotocol/mcp-ui) - Official MCP-UI documentation -- [Goose Documentation](https://github.com/block/goose) - MCP-UI compatible AI agent -- [Mapbox Static Images API](https://docs.mapbox.com/api/maps/static-images/) - API used for map rendering +MCP Apps and MCP-UI solved the same problem (interactive previews) with two different protocols, which meant every visualization-producing tool needed to carry both integrations, decide when to attach which, and keep them in sync. Consolidating all interactive rendering behind one tool (`render_map_tool`) removed that duplication and fixed a real bug in the process: individual tools' own MCP Apps resources only rendered reliably as the _last_ tool call in a chain in some hosts, which `render_map_tool`'s terminal-tool design sidesteps entirely (see [render-map-tool.md](./render-map-tool.md#overview)). --- -For questions or issues related to MCP-UI support, please [open an issue](https://github.com/mapbox/mcp-server/issues) on GitHub. +For questions or issues, please [open an issue](https://github.com/mapbox/mcp-server/issues) on GitHub. diff --git a/docs/render-map-tool.md b/docs/render-map-tool.md new file mode 100644 index 00000000..2f1abfda --- /dev/null +++ b/docs/render-map-tool.md @@ -0,0 +1,205 @@ +# `render_map_tool`: the Mapbox map visualization primitive + +`render_map_tool` displays a live, interactive Mapbox GL JS map inside any MCP client that supports **MCP Apps**. It is the single visualization primitive for this server — every other Mapbox tool that produces geospatial output hands its result to this one tool to actually draw it, and **you can call it directly with your own data too**, without any other Mapbox tool involved at all. + +## Table of Contents + +- [Overview](#overview) +- [Two ways to use it](#two-ways-to-use-it) +- [Standalone usage: visualize your own data](#standalone-usage-visualize-your-own-data) +- [Chained usage: rendering another tool's result](#chained-usage-rendering-another-tools-result) +- [Payload reference](#payload-reference) +- [Compatible clients](#compatible-clients) +- [Notes for third-party integrators](#notes-for-third-party-integrators) +- [Troubleshooting](#troubleshooting) + +## Overview + +`render_map_tool` takes a small JSON payload describing what to draw — layers, markers, a legend, an optional camera position — and renders it as a live Mapbox GL JS map in a self-contained HTML panel (via the [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) protocol). It's intentionally the _only_ tool in this server that declares an MCP Apps UI resource. Two reasons for that: + +1. **Chain-position limitation**: several MCP App hosts (Claude Desktop among them) only fully render the interactive panel for the _last_ tool call in a sequence. Funneling every visualization through one terminal tool means the map always renders, regardless of how many other tools ran first. +2. **Token efficiency**: geometry (a route polyline, a set of isochrone contours, a polygon boundary) can be tens of thousands of coordinate pairs. Passing it through the model as tool-call arguments is slow and expensive. The other Mapbox tools in this server avoid that by stashing their result behind a short reference string (`payload_refs`) instead of inlining the geometry — but this is an optimization, not a requirement. You're always free to pass geometry inline instead. + +## Two ways to use it + +| Mode | When to use it | What you pass | +| -------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| **Chained** | Visualizing a result from another Mapbox tool in this server (directions, isochrone, search, etc.) | `payload_refs`: one or more reference strings from that tool's `structuredContent.mapboxRender.ref` | +| **Standalone** | Visualizing your own GeoJSON — data from your own database, a third-party API, a file, anything | `layers` / `markers` / `legend` / `camera`, composed directly from your data | + +Both modes use the exact same tool, and can be combined in one call (e.g. inline markers layered on top of a chained route). Nothing about the standalone path is a fallback or a lesser-supported mode — it's the same code path other Mapbox tools use internally. + +## Standalone usage: visualize your own data + +This is the case most third-party integrators care about: you have your own geospatial data (a delivery zone, a set of store locations, a GPS trace, anything expressible as GeoJSON) and want to show it on a live Mapbox map inside an MCP conversation, without calling any other tool in this server first. + +Call `render_map_tool` directly with `layers` and/or `markers`: + +```json +{ + "summary": "Downtown delivery zone", + "layers": [ + { + "id": "delivery-zone", + "type": "fill", + "data": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-122.4194, 37.7749], + [-122.4094, 37.7749], + [-122.4094, 37.7849], + [-122.4194, 37.7849], + [-122.4194, 37.7749] + ] + ] + }, + "properties": {} + }, + "paint": { + "fill-color": "#3b82f6", + "fill-opacity": 0.25, + "fill-outline-color": "#1d4ed8" + } + } + ], + "markers": [ + { + "coordinates": [-122.4144, 37.7799], + "style": "pin", + "color": "#ef4444", + "popup": "Warehouse" + } + ], + "legend": [{ "label": "Delivery zone", "color": "#3b82f6", "opacity": 0.25 }] +} +``` + +No `payload_refs`, no dependency on `directions_tool`/`isochrone_tool`/etc. — the map renders exactly this polygon and marker. This is the whole request; there's nothing else to configure. + +**As a natural-language prompt**, this looks like: _"Using the Mapbox map render tool, show a fill polygon over these four coordinates: [...], with a red pin at [...] labeled 'Warehouse'."_ An LLM with access to this tool can compose the JSON payload itself from a plain-language description of your data — you don't need to hand it pre-built GeoJSON if the model already has (or can derive) the coordinates. + +Multiple layers and markers in one call are merged onto the same map and the camera auto-fits to the union of everything drawn, unless you provide an explicit `camera`. + +## Chained usage: rendering another tool's result + +When another tool in this server returns geospatial data, its `structuredContent` includes a `mapboxRender.ref` field: + +```json +{ + "structuredContent": { + "routes": [ + /* ... */ + ], + "mapboxRender": { "ref": "mapbox://selffetch/directions?data=..." } + } +} +``` + +Pass that ref straight through: + +```json +{ "payload_refs": ["mapbox://selffetch/directions?data=..."] } +``` + +Pass multiple refs to merge several tool results onto one map — for example, an isochrone plus a route: + +```json +{ + "payload_refs": [ + "mapbox://selffetch/isochrone?data=...", + "mapbox://selffetch/directions?data=..." + ] +} +``` + +An LLM using this server is instructed (via each tool's own output) to call `render_map_tool` as the final step whenever a `mapboxRender` field is present — you generally don't need to prompt for this explicitly. + +## Payload reference + +All fields are optional; provide whichever combination fits what you're drawing. + +| Field | Type | Description | +| -------------- | ---------- | -------------------------------------------------------------------------------------------- | +| `payload_refs` | `string[]` | Reference strings from other tools' `mapboxRender.ref`. Merges with any inline fields below. | +| `summary` | `string` | Short header chip shown top-left on the map (e.g. `"Route: 12.4 mi, 23 min"`). | +| `layers` | array | Inline GL JS layers — see below. | +| `markers` | array | Inline point markers — see below. | +| `legend` | array | Inline legend rows — see below. | +| `camera` | object | Initial camera position. If omitted, the map auto-fits to everything drawn. | + +**`layers[]`** — one entry per Mapbox GL JS source+layer pair: + +| Field | Type | Description | +| -------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | `string` | Unique id within the payload (used as both source id and layer id). | +| `type` | `"fill" \| "line" \| "circle" \| "symbol"` | Mapbox GL layer type. | +| `data` | GeoJSON `Feature` or `FeatureCollection` | Geometry must be `Point`, `LineString`, `Polygon`, or `MultiPolygon`. Coordinates are `[longitude, latitude]`. | +| `paint` | object | [Mapbox Style Spec](https://docs.mapbox.com/style-spec/reference/layers/) paint object, passed through to `addLayer` as-is (e.g. `{ "line-color": "#3b82f6", "line-width": 5 }`). | +| `layout` | object | Style Spec layout object (e.g. `{ "line-join": "round", "line-cap": "round" }`). | + +**`markers[]`** — one entry per point marker: + +| Field | Type | Description | +| ------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `coordinates` | `[number, number]` | `[longitude, latitude]`. | +| `style` | `"pin" \| "numbered" \| "start" \| "end"` | `pin` is the default Mapbox marker. `numbered` is a circular badge containing `label` (e.g. visit order). `start`/`end` are green/red badges for route endpoints. | +| `label` | `string` | Required when `style` is `"numbered"`. | +| `color` | `string` | CSS color override; defaults are style-derived. | +| `popup` | `string` | Text shown when the marker is clicked. | + +**`legend[]`** — one entry per legend row: + +| Field | Type | Description | +| --------- | -------------- | ----------------- | +| `label` | `string` | Row label. | +| `color` | `string` | Swatch CSS color. | +| `opacity` | `number` (0-1) | Swatch opacity. | + +**`camera`**: + +| Field | Type | Description | +| -------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `center` | `[number, number]` | `[longitude, latitude]`. | +| `zoom` | `number` | Zoom level. | +| `bounds` | `[[number, number], [number, number]]` | `[[minLng, minLat], [maxLng, maxLat]]`. Takes precedence over `center`/`zoom` and over auto-fit if set. | + +The payload format is intentionally a thin pass-through to the Mapbox Style Spec rather than its own DSL — anything expressible as a GL JS `paint`/`layout` object is expressible here, so you're not limited to a fixed set of pre-baked styles. + +## Compatible clients + +`render_map_tool` renders via **MCP Apps** (`@modelcontextprotocol/ext-apps`). Hosts that support it show a live, interactive map with a Fullscreen toggle: + +- **Claude Desktop** ✅ +- **VS Code with GitHub Copilot** ✅ +- **Claude Code** ✅ +- **[Goose](https://github.com/block/goose)** ✅ + +In a client without MCP Apps support, `render_map_tool`'s text/JSON output (the resolved payload) is still returned as the tool result — you lose the live interactive panel, but the call doesn't fail. If you need a guaranteed visual image regardless of client capability, use `static_map_image_tool` instead, which returns a base64-encoded PNG/JPEG that every client can display. + +## Notes for third-party integrators + +- **You don't need any other Mapbox tool.** `render_map_tool` accepts arbitrary standalone GeoJSON through `layers`/`markers`. Nothing about `payload_refs` is required. +- **You don't need a Mapbox access token to reason about the payload shape** — token handling for rendering (fetching map tiles) happens entirely inside the host's iframe, using this server's own public-token resolution. Your `layers`/`markers` are plain GeoJSON/CSS values with no credentials embedded. +- **Geometry size**: very large inline payloads (tens of thousands of coordinates) are still passed through the model as tool arguments in standalone mode, unlike the internal `payload_refs` optimization other tools use. For most use cases (a handful of markers, a modest polygon or route) this is a non-issue; if you're regularly rendering very large geometries, consider simplifying them first (see `simplify_tool`) before passing them to `render_map_tool`. +- **Multiple calls**: each call to `render_map_tool` performs a fresh render — it does not accumulate state across calls. Merge everything you want on one map into a single call's `layers`/`markers`/`payload_refs`. + +## Troubleshooting + +### "I'm not seeing an interactive map, just text/JSON" + +Check that your client supports MCP Apps (see [Compatible clients](#compatible-clients)). Clients without support still receive the resolved payload as a tool result, just not the rendered panel. + +### "My inline layer isn't drawing anything" + +Confirm `data` is a valid GeoJSON `Feature` or `FeatureCollection` with `Point`, `LineString`, `Polygon`, or `MultiPolygon` geometry, and that `type` (`fill`/`line`/`circle`/`symbol`) matches the geometry — for example, a `Polygon` needs `type: "fill"` or `"line"`, not `"symbol"`. + +### "I want a guaranteed image even in clients without MCP Apps support" + +Use `static_map_image_tool` — it always returns a base64-encoded PNG/JPEG, with no dependency on MCP Apps support, so every client gets a usable result. + +--- + +For questions or issues, please [open an issue](https://github.com/mapbox/mcp-server/issues) on GitHub. diff --git a/src/cli.ts b/src/cli.ts index 9e98c3c4..c540935d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,11 +17,14 @@ const OPTIONS = [ '--help, -h Show this help message', '--version, -v Show the server version', '--enable-tools Enable only the comma-separated tools', - '--disable-tools Disable the comma-separated tools', - '--disable-mcp-ui Disable MCP-UI resources' + '--disable-tools Disable the comma-separated tools' ]; const OPTIONS_WITH_VALUES = new Set(['--enable-tools', '--disable-tools']); +// --disable-mcp-ui is no longer a real option (MCP-UI support was removed +// entirely — see docs/mcp-ui.md) but stays recognized-and-ignored here, not +// in OPTIONS, so an existing launch config that still passes it keeps +// starting instead of hard-failing on "Unknown option". const KNOWN_FLAGS = new Set([ '--help', '-h', diff --git a/src/resources/resourceRegistry.ts b/src/resources/resourceRegistry.ts index 9c3f4f71..9ac8dc77 100644 --- a/src/resources/resourceRegistry.ts +++ b/src/resources/resourceRegistry.ts @@ -6,7 +6,6 @@ import { CategoryListResource } from './category-list/CategoryListResource.js'; import { TemporaryDataResource } from './temporary/TemporaryDataResource.js'; import { ComputeResource } from './compute/ComputeResource.js'; import { InlinePayloadResource } from './inline-payload/InlinePayloadResource.js'; -import { StaticMapUIResource } from './ui-apps/StaticMapUIResource.js'; import { MapAppUIResource } from './ui-apps/MapAppUIResource.js'; import { VersionResource } from './version/VersionResource.js'; import { httpRequest } from '../utils/httpPipeline.js'; @@ -18,7 +17,6 @@ export const ALL_RESOURCES = [ new TemporaryDataResource(), new ComputeResource(), new InlinePayloadResource(), - new StaticMapUIResource(), // Single shared map renderer, targeted exclusively by render_map_tool. new MapAppUIResource({ httpRequest }), new VersionResource() diff --git a/src/resources/ui-apps/StaticMapUIResource.ts b/src/resources/ui-apps/StaticMapUIResource.ts deleted file mode 100644 index 43252513..00000000 --- a/src/resources/ui-apps/StaticMapUIResource.ts +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; -import type { - ReadResourceResult, - ServerNotification, - ServerRequest -} from '@modelcontextprotocol/sdk/types.js'; -import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; -import { BaseResource } from '../BaseResource.js'; - -/** - * Serves UI App HTML for Static Map Preview - * Implements MCP Apps pattern with ui:// scheme - */ -export class StaticMapUIResource extends BaseResource { - readonly name = 'Static Map Preview UI'; - readonly uri = 'ui://mapbox/static-map/index.html'; - readonly description = - 'Interactive UI for previewing static map images (MCP Apps)'; - readonly mimeType = RESOURCE_MIME_TYPE; - - async read( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _uri: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _extra?: RequestHandlerExtra - ): Promise { - // Generate HTML for static map visualization with MCP Apps protocol support - const html = ` - - - - - Static Map Preview - - - -
- -
-
-
Loading static map preview...
- Static Map Preview - -
- - - -`; - - return { - contents: [ - { - uri: this.uri, - mimeType: RESOURCE_MIME_TYPE, - text: html, - _meta: { - ui: { - csp: { - connectDomains: ['https://api.mapbox.com'], - resourceDomains: ['https://api.mapbox.com'] - } - } - } - } - ] - }; - } -} diff --git a/src/resources/ui-apps/mapAppHtml.ts b/src/resources/ui-apps/mapAppHtml.ts index 9357ce0a..0ca2f8e2 100644 --- a/src/resources/ui-apps/mapAppHtml.ts +++ b/src/resources/ui-apps/mapAppHtml.ts @@ -4,9 +4,9 @@ import type { MapAppPayload } from '../../utils/mapAppPayload.js'; /** - * Render the generic Mapbox MCP App HTML — used by both the MCP Apps - * resource (postMessage delivery) and any tool's inline MCP-UI rawHtml - * block (initial-data baked in). + * Render the generic Mapbox MCP App HTML — served by the MCP Apps resource + * (`MapAppUIResource`) that `render_map_tool` targets exclusively via + * postMessage delivery. * * The iframe is a thin renderer over Mapbox GL JS. Tools produce a * `MapAppPayload` (see src/utils/mapAppPayload.ts) and the iframe diff --git a/src/tools/static-map-image-tool/StaticMapImageTool.ts b/src/tools/static-map-image-tool/StaticMapImageTool.ts index bddbd759..60901af8 100644 --- a/src/tools/static-map-image-tool/StaticMapImageTool.ts +++ b/src/tools/static-map-image-tool/StaticMapImageTool.ts @@ -140,7 +140,6 @@ export class StaticMapImageTool extends MapboxApiBasedTool< const isRasterStyle = input.style.includes('satellite'); const mimeType = isRasterStyle ? 'image/jpeg' : 'image/png'; - // content[0] MUST be the URL text — MCP Apps UI finds it via content.find(c => c.type === 'text') // Use public URL (without credentials) to avoid leaking the access token const content: CallToolResult['content'] = [ { type: 'text', text: publicUrl } diff --git a/test/tools/static-map-image-tool/StaticMapImageTool.test.ts b/test/tools/static-map-image-tool/StaticMapImageTool.test.ts index 79431ad3..6e9a7e37 100644 --- a/test/tools/static-map-image-tool/StaticMapImageTool.test.ts +++ b/test/tools/static-map-image-tool/StaticMapImageTool.test.ts @@ -32,112 +32,76 @@ describe('StaticMapImageTool', () => { }); it('returns URL as text content', async () => { - // Disable MCP-UI for this test to focus on URL only - const originalEnv = process.env.ENABLE_MCP_UI; - process.env.ENABLE_MCP_UI = 'false'; - - try { - const { httpRequest } = setupHttpRequest(); + const { httpRequest } = setupHttpRequest(); - const result = await new StaticMapImageTool({ httpRequest }).run({ - center: { longitude: -74.006, latitude: 40.7128 }, - zoom: 10, - size: { width: 800, height: 600 }, - style: 'mapbox/satellite-v9' - }); + const result = await new StaticMapImageTool({ httpRequest }).run({ + center: { longitude: -74.006, latitude: 40.7128 }, + zoom: 10, + size: { width: 800, height: 600 }, + style: 'mapbox/satellite-v9' + }); - expect(result.isError).toBe(false); - expect(result.content).toHaveLength(2); // URL text + image - expect(result.content[0].type).toBe('text'); - const textContent = result.content[0] as { type: 'text'; text: string }; - expect(textContent.text).toContain( - 'api.mapbox.com/styles/v1/mapbox/satellite-v9/static/' - ); - expect(textContent.text).toContain('-74.006,40.7128,10'); - expect(textContent.text).toContain('800x600'); - expect(textContent.text).not.toContain('access_token='); - } finally { - // Restore environment variable - if (originalEnv !== undefined) { - process.env.ENABLE_MCP_UI = originalEnv; - } else { - delete process.env.ENABLE_MCP_UI; - } - } + expect(result.isError).toBe(false); + expect(result.content).toHaveLength(2); // URL text + image + expect(result.content[0].type).toBe('text'); + const textContent = result.content[0] as { type: 'text'; text: string }; + expect(textContent.text).toContain( + 'api.mapbox.com/styles/v1/mapbox/satellite-v9/static/' + ); + expect(textContent.text).toContain('-74.006,40.7128,10'); + expect(textContent.text).toContain('800x600'); + expect(textContent.text).not.toContain('access_token='); }); it('returns URL containing map details', async () => { - // Disable MCP-UI for this test to focus on URL only - const originalEnv = process.env.ENABLE_MCP_UI; - process.env.ENABLE_MCP_UI = 'false'; - - try { - const { httpRequest } = setupHttpRequest(); + const { httpRequest } = setupHttpRequest(); - const result = await new StaticMapImageTool({ httpRequest }).run({ - center: { longitude: -74.006, latitude: 40.7128 }, - zoom: 12, - size: { width: 600, height: 400 }, - style: 'mapbox/streets-v12' - }); + const result = await new StaticMapImageTool({ httpRequest }).run({ + center: { longitude: -74.006, latitude: 40.7128 }, + zoom: 12, + size: { width: 600, height: 400 }, + style: 'mapbox/streets-v12' + }); - expect(result.isError).toBe(false); - expect(result.content[0].type).toBe('text'); + expect(result.isError).toBe(false); + expect(result.content[0].type).toBe('text'); - const textContent = result.content[0] as { type: 'text'; text: string }; - expect(textContent.text).toContain('mapbox/streets-v12/static/'); - expect(textContent.text).toContain('-74.006,40.7128,12'); - expect(textContent.text).toContain('600x400'); - } finally { - if (originalEnv !== undefined) { - process.env.ENABLE_MCP_UI = originalEnv; - } else { - delete process.env.ENABLE_MCP_UI; - } - } + const textContent = result.content[0] as { type: 'text'; text: string }; + expect(textContent.text).toContain('mapbox/streets-v12/static/'); + expect(textContent.text).toContain('-74.006,40.7128,12'); + expect(textContent.text).toContain('600x400'); }); it('URL includes overlay markers when overlays present', async () => { - const originalEnv = process.env.ENABLE_MCP_UI; - process.env.ENABLE_MCP_UI = 'false'; - - try { - const { httpRequest } = setupHttpRequest(); + const { httpRequest } = setupHttpRequest(); - const result = await new StaticMapImageTool({ httpRequest }).run({ - center: { longitude: -74.006, latitude: 40.7128 }, - zoom: 12, - size: { width: 600, height: 400 }, - style: 'mapbox/streets-v12', - overlays: [ - { - type: 'marker', - longitude: -74.006, - latitude: 40.7128, - size: 'large', - color: 'ff0000' - }, - { - type: 'marker', - longitude: -74.01, - latitude: 40.71, - size: 'small', - color: '00ff00' - } - ] - }); + const result = await new StaticMapImageTool({ httpRequest }).run({ + center: { longitude: -74.006, latitude: 40.7128 }, + zoom: 12, + size: { width: 600, height: 400 }, + style: 'mapbox/streets-v12', + overlays: [ + { + type: 'marker', + longitude: -74.006, + latitude: 40.7128, + size: 'large', + color: 'ff0000' + }, + { + type: 'marker', + longitude: -74.01, + latitude: 40.71, + size: 'small', + color: '00ff00' + } + ] + }); - expect(result.isError).toBe(false); - const textContent = result.content[0] as { type: 'text'; text: string }; - expect(textContent.text).toContain('pin-l+ff0000(-74.006,40.7128)'); - expect(textContent.text).toContain('pin-s+00ff00(-74.01,40.71)'); - } finally { - if (originalEnv !== undefined) { - process.env.ENABLE_MCP_UI = originalEnv; - } else { - delete process.env.ENABLE_MCP_UI; - } - } + expect(result.isError).toBe(false); + const textContent = result.content[0] as { type: 'text'; text: string }; + expect(textContent.text).toContain('pin-l+ff0000(-74.006,40.7128)'); + expect(textContent.text).toContain('pin-s+00ff00(-74.01,40.71)'); }); it('constructs correct Mapbox Static API URL', async () => { @@ -824,7 +788,7 @@ describe('StaticMapImageTool', () => { }); describe('content shape', () => { - it('returns URL text + base64 image (no MCP-UI fallback)', async () => { + it('returns exactly URL text + base64 image, nothing else', async () => { const { httpRequest } = setupHttpRequest(); const result = await new StaticMapImageTool({ httpRequest }).run({