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

- **`map_matching_tool` now renders as a live Mapbox GL JS map** showing the raw GPS trace as a dashed orange line and the snapped matched route as a solid blue line on top. Same dual-spec dispatch (MCP Apps + inline MCP-UI) and shared `renderMapMatchingAppHtml` template.
- **`search_and_geocode_tool` and `category_search_tool` now render as a live Mapbox GL JS map** following the same dual-spec pattern as `directions_tool`/`isochrone_tool`/`optimization_tool`. Both tools share a single `SearchAppUIResource` (`ui://mapbox/search-app/index.html`) and one `renderSearchAppHtml` template that drops numbered pins on each result with name/category/address popups.
- **`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.
Expand Down
2 changes: 2 additions & 0 deletions src/resources/resourceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DirectionsAppUIResource } from './ui-apps/DirectionsAppUIResource.js';
import { IsochroneAppUIResource } from './ui-apps/IsochroneAppUIResource.js';
import { OptimizationAppUIResource } from './ui-apps/OptimizationAppUIResource.js';
import { SearchAppUIResource } from './ui-apps/SearchAppUIResource.js';
import { MapMatchingAppUIResource } from './ui-apps/MapMatchingAppUIResource.js';
import { VersionResource } from './version/VersionResource.js';
import { httpRequest } from '../utils/httpPipeline.js';

Expand All @@ -22,6 +23,7 @@ export const ALL_RESOURCES = [
new IsochroneAppUIResource({ httpRequest }),
new OptimizationAppUIResource({ httpRequest }),
new SearchAppUIResource({ httpRequest }),
new MapMatchingAppUIResource({ httpRequest }),
new VersionResource()
] as const;

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

export class MapMatchingAppUIResource extends BaseResource {
readonly name = 'Map Matching App UI';
readonly uri = 'ui://mapbox/map-matching-app/index.html';
readonly description =
'Interactive UI for visualizing raw GPS traces snapped to the road network (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 = renderMapMatchingAppHtml({ 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