Skip to content
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

- **`optimization_tool` now renders as a live Mapbox GL JS map** following the same dual-spec pattern as `directions_tool`/`isochrone_tool`: MCP Apps via `_meta.ui.resourceUri` → `OptimizationAppUIResource`, plus an inline MCP-UI rawHtml block (gated by `ENABLE_MCP_UI`). One shared `renderOptimizationAppHtml` template renders the trip line with numbered markers (1, 2, 3, …) at each stop in the visit order.
- **`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.
Expand Down
2 changes: 2 additions & 0 deletions src/resources/resourceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 { OptimizationAppUIResource } from './ui-apps/OptimizationAppUIResource.js';
import { VersionResource } from './version/VersionResource.js';
import { httpRequest } from '../utils/httpPipeline.js';

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

Expand Down
79 changes: 79 additions & 0 deletions src/resources/ui-apps/OptimizationAppUIResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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 { renderOptimizationAppHtml } from './optimizationAppHtml.js';

export class OptimizationAppUIResource extends BaseResource {
readonly name = 'Optimization App UI';
readonly uri = 'ui://mapbox/optimization-app/index.html';
readonly description =
'Interactive UI for visualizing an optimized multi-stop trip 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 = renderOptimizationAppHtml({
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