Skip to content
Closed
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

### New Features

- **`isochrone_tool` now renders as a live Mapbox GL JS map** following the same dual-spec pattern as `directions_tool`: MCP Apps via `_meta.ui.resourceUri` → `IsochroneAppUIResource`, plus an inline MCP-UI rawHtml block (gated by `ENABLE_MCP_UI`). One shared `renderIsochroneAppHtml` template renders the contours as translucent fill + outline layers with the origin marked.
- **`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`).
Expand Down
2 changes: 2 additions & 0 deletions src/resources/resourceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CategoryListResource } from './category-list/CategoryListResource.js';
import { TemporaryDataResource } from './temporary/TemporaryDataResource.js';
import { StaticMapUIResource } from './ui-apps/StaticMapUIResource.js';
import { DirectionsAppUIResource } from './ui-apps/DirectionsAppUIResource.js';
import { IsochroneAppUIResource } from './ui-apps/IsochroneAppUIResource.js';
import { VersionResource } from './version/VersionResource.js';
import { httpRequest } from '../utils/httpPipeline.js';

Expand All @@ -16,6 +17,7 @@ export const ALL_RESOURCES = [
new TemporaryDataResource(),
new StaticMapUIResource(),
new DirectionsAppUIResource({ httpRequest }),
new IsochroneAppUIResource({ httpRequest }),
new VersionResource()
] as const;

Expand Down
83 changes: 83 additions & 0 deletions src/resources/ui-apps/IsochroneAppUIResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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';
import type { HttpRequest } from '../../utils/types.js';
import { resolveMapboxPublicToken } from '../../utils/mapboxPublicToken.js';
import { renderIsochroneAppHtml } from './isochroneAppHtml.js';

/**
* MCP Apps resource for `isochrone_tool` — serves the HTML at
* `ui://mapbox/isochrone-app/index.html`. The iframe receives the tool's
* FeatureCollection via the `ui/notifications/tool-result` postMessage event
* and renders each contour as a translucent fill + outline layer.
*/
export class IsochroneAppUIResource extends BaseResource {
readonly name = 'Isochrone App UI';
readonly uri = 'ui://mapbox/isochrone-app/index.html';
readonly description =
'Interactive UI for visualizing Mapbox isochrone contours with Mapbox GL JS (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?: RequestHandlerExtra<ServerRequest, ServerNotification>
): Promise<ReadResourceResult> {
const accessToken =
(extra?.authInfo?.token as string | undefined) ||
process.env.MAPBOX_ACCESS_TOKEN ||
'';

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

const html = renderIsochroneAppHtml({ publicToken: publicToken ?? '' });

return {
contents: [
{
uri: this.uri,
mimeType: RESOURCE_MIME_TYPE,
text: html,
_meta: {
ui: {
csp: {
connectDomains: [
'https://*.mapbox.com',
'https://events.mapbox.com'
],
resourceDomains: ['https://api.mapbox.com'],
workerDomains: ['blob:']
},
preferredSize: { width: 1000, height: 600 }
}
}
}
]
};
}
}
Loading