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

- **`union_tool`, `intersect_tool`, `difference_tool` now render as a live Mapbox GL JS map** with input polygons in muted blue and the result in an operation-keyed color (green for union, purple for intersect, orange for difference). All three share a single `PolygonOpsAppUIResource` and one `renderPolygonOpsAppHtml` template. Same dual-spec dispatch (MCP Apps + inline MCP-UI).
- **`ground_location_tool` now renders as a live Mapbox GL JS map** showing the reverse-geocoded origin marker + nearby POIs (numbered orange pins) + the isochrone polygons when present. Same dual-spec dispatch (MCP Apps + inline MCP-UI) and shared `renderGroundLocationAppHtml` template.
- **`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.
Expand Down
2 changes: 2 additions & 0 deletions src/resources/resourceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { OptimizationAppUIResource } from './ui-apps/OptimizationAppUIResource.j
import { SearchAppUIResource } from './ui-apps/SearchAppUIResource.js';
import { MapMatchingAppUIResource } from './ui-apps/MapMatchingAppUIResource.js';
import { GroundLocationAppUIResource } from './ui-apps/GroundLocationAppUIResource.js';
import { PolygonOpsAppUIResource } from './ui-apps/PolygonOpsAppUIResource.js';
import { VersionResource } from './version/VersionResource.js';
import { httpRequest } from '../utils/httpPipeline.js';

Expand All @@ -26,6 +27,7 @@ export const ALL_RESOURCES = [
new SearchAppUIResource({ httpRequest }),
new MapMatchingAppUIResource({ httpRequest }),
new GroundLocationAppUIResource({ httpRequest }),
new PolygonOpsAppUIResource({ httpRequest }),
new VersionResource()
] as const;

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

/**
* Serves HTML for union/intersect/difference polygon-op MCP Apps.
*
* The input polygons are rendered in muted blue (semi-transparent fill +
* outline) and the result polygon is overlaid in a brighter color keyed to
* the operation (green=union, purple=intersect, orange=difference).
*/
export class PolygonOpsAppUIResource extends BaseResource {
readonly name = 'Polygon Ops App UI';
readonly uri = 'ui://mapbox/polygon-ops-app/index.html';
readonly description =
'Interactive UI for visualizing polygon union/intersect/difference results (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 = renderPolygonOpsAppHtml({
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