diff --git a/package-lock.json b/package-lock.json index c64ab2a..041097d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@mcp-ui/server": "^5.13.1", + "@modelcontextprotocol/ext-apps": "^1.0.1", "@modelcontextprotocol/sdk": "^1.25.3", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.56.0", @@ -1820,6 +1821,127 @@ "@modelcontextprotocol/sdk": "*" } }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.1.tgz", + "integrity": "sha512-rAPzBbB5GNgYk216paQjGKUgbNXSy/yeR95c0ni6Y4uvhWI2AeF+ztEOqQFLBMQy/MPM+02pbVK1HaQmQjMwYQ==", + "hasInstallScript": true, + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "optionalDependencies": { + "@oven/bun-darwin-aarch64": "^1.2.21", + "@oven/bun-darwin-x64": "^1.2.21", + "@oven/bun-darwin-x64-baseline": "^1.2.21", + "@oven/bun-linux-aarch64": "^1.2.21", + "@oven/bun-linux-aarch64-musl": "^1.2.21", + "@oven/bun-linux-x64": "^1.2.21", + "@oven/bun-linux-x64-baseline": "^1.2.21", + "@oven/bun-linux-x64-musl": "^1.2.21", + "@oven/bun-linux-x64-musl-baseline": "^1.2.21", + "@oven/bun-windows-x64": "^1.2.21", + "@oven/bun-windows-x64-baseline": "^1.2.21", + "@rollup/rollup-darwin-arm64": "^4.53.3", + "@rollup/rollup-darwin-x64": "^4.53.3", + "@rollup/rollup-linux-arm64-gnu": "^4.53.3", + "@rollup/rollup-linux-x64-gnu": "^4.53.3", + "@rollup/rollup-win32-arm64-msvc": "^4.53.3", + "@rollup/rollup-win32-x64-msvc": "^4.53.3" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.25.3", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", @@ -5711,6 +5833,149 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@oven/bun-darwin-aarch64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.7.tgz", + "integrity": "sha512-Mh78f4B+vNTOhFpI7RWHRWDqSKTnFXj/MauRx7I/GmNwEfw56sUx98gWRwXyF4lkW+9VNU+33wuw6E+M22W66w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-darwin-x64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.7.tgz", + "integrity": "sha512-dFfKdSVz6Ois5zjEJboUC7igcYAVd+c//ajotd0L6WUQAKQrHMVq/+6LjOj/0zjC6VPFNGWzeF8erymNo1y0Jw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-darwin-x64-baseline": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.7.tgz", + "integrity": "sha512-bUND1aQoTCfIL+idALT7FWtuX59ltOIRo954c7p/JkESbSIJ01jY06BSNVbkGk8RQM19v/7qiqZZqi4NyO4Utw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oven/bun-linux-aarch64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.7.tgz", + "integrity": "sha512-m03OtzEs+/RkWtk6tBf8yw0GW4P8ajfzTXnTt984tQBgkMubGQYUyUnFasWgr3mD2820LhkVjhYeBf1rkz/biQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-aarch64-musl": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.7.tgz", + "integrity": "sha512-QDxrROdUnC1d/uoilXtUeFHaLhYdRN7dRIzw/Iqj/vrrhnkA6VS+HYoCWtyyVvci/K+JrPmDwxOWlSRpmV4INA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.7.tgz", + "integrity": "sha512-uttKQ/eIRVGc4uBtLRqmQqXGf57/dmQaF0AEd37RQNRRRd1P/VYnFMiMcVaot3HJ6IFjHjGtcPO9ekT49LxBYQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64-baseline": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.7.tgz", + "integrity": "sha512-Jlb/AcrIFU3QDeR3EL4UVT1CIKqnLJDgbU+R0k/+NaSWMrBEpZV+gJJT5L1cmEKTNhU/d+c7hudxkjtqA7XXqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64-musl": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.7.tgz", + "integrity": "sha512-aK8fvkCosrHRG3CNdVqMom1C8Rj3XkqZp0ZFSBXgaXlKP22RkxlEE9tS7OmSq9yVgEk6euTB3dW4NFo/jlXqeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-linux-x64-musl-baseline": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.7.tgz", + "integrity": "sha512-lySQQ7zJJsoa5hQH+PE5bQyQaTI8G2Erszhu4iQuDtsocwy3zSxjB6TxGWTd4HmetPl9aRvg3nb2KR8RVAd7ug==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oven/bun-windows-x64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.7.tgz", + "integrity": "sha512-3QdIGdSn3fkssCq/vPjtPLAQxo+eMUzcwJedn1c5mXDy1AoisjhoxhWnbVl8+uk+wt9N6JUPdISoe0N4OdwXfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oven/bun-windows-x64-baseline": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.7.tgz", + "integrity": "sha512-wMgELfW5vFceh4qEOYb5iV5TjrjjnBJzE383ixA3kqGKzaubksSxNc11eZhS0ptcJ5a0UjN5hfbMh6sYoh+cRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", diff --git a/package.json b/package.json index 9db4559..11d59d4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ ], "dependencies": { "@mcp-ui/server": "^5.13.1", + "@modelcontextprotocol/ext-apps": "^1.0.1", "@modelcontextprotocol/sdk": "^1.25.3", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.56.0", diff --git a/src/index.ts b/src/index.ts index 627b1a1..0f44562 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,10 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + registerAppResource, + RESOURCE_MIME_TYPE +} from '@modelcontextprotocol/ext-apps/server'; import { z } from 'zod'; import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; import { @@ -92,7 +96,29 @@ enabledCoreTools.forEach((tool) => { // Register resources to the server const resources = getAllResources(); -resources.forEach((resource) => { + +// Separate MCP Apps UI resources from regular resources +const uiResources = resources.filter((r) => r.uri.startsWith('ui://')); +const regularResources = resources.filter((r) => !r.uri.startsWith('ui://')); + +// Register MCP Apps UI resources using registerAppResource +// IMPORTANT: Use RESOURCE_MIME_TYPE which is "text/html;profile=mcp-app" +// This tells clients (like Claude Desktop) that this is an MCP App +uiResources.forEach((resource) => { + registerAppResource( + server as any, + resource.name, + resource.uri, + { mimeType: RESOURCE_MIME_TYPE, description: resource.description }, + async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return await resource.readCallback(new URL(resource.uri), {} as any); + } + ); +}); + +// Register regular resources using standard registration +regularResources.forEach((resource) => { resource.installTo(server); }); diff --git a/src/resources/resourceRegistry.ts b/src/resources/resourceRegistry.ts index 5288bd8..4786a97 100644 --- a/src/resources/resourceRegistry.ts +++ b/src/resources/resourceRegistry.ts @@ -6,6 +6,9 @@ import { MapboxStreetsV8FieldsResource } from './mapbox-streets-v8-fields-resour import { MapboxTokenScopesResource } from './mapbox-token-scopes-resource/MapboxTokenScopesResource.js'; import { MapboxLayerTypeMappingResource } from './mapbox-layer-type-mapping-resource/MapboxLayerTypeMappingResource.js'; import { MapboxDocumentationResource } from './mapbox-documentation-resource/MapboxDocumentationResource.js'; +import { PreviewStyleUIResource } from './ui-apps/PreviewStyleUIResource.js'; +import { StyleComparisonUIResource } from './ui-apps/StyleComparisonUIResource.js'; +import { GeojsonPreviewUIResource } from './ui-apps/GeojsonPreviewUIResource.js'; import { httpRequest } from '../utils/httpPipeline.js'; // Central registry of all resources @@ -14,7 +17,11 @@ export const ALL_RESOURCES = [ new MapboxStreetsV8FieldsResource(), new MapboxTokenScopesResource(), new MapboxLayerTypeMappingResource(), - new MapboxDocumentationResource({ httpRequest }) + new MapboxDocumentationResource({ httpRequest }), + // MCP Apps UI resources (ui:// scheme) + new PreviewStyleUIResource(), + new StyleComparisonUIResource(), + new GeojsonPreviewUIResource() ] as const; export type ResourceInstance = (typeof ALL_RESOURCES)[number]; diff --git a/src/resources/ui-apps/GeojsonPreviewUIResource.ts b/src/resources/ui-apps/GeojsonPreviewUIResource.ts new file mode 100644 index 0000000..197c3f5 --- /dev/null +++ b/src/resources/ui-apps/GeojsonPreviewUIResource.ts @@ -0,0 +1,313 @@ +// 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 GeoJSON Preview + * Implements MCP Apps pattern with ui:// scheme + */ +export class GeojsonPreviewUIResource extends BaseResource { + readonly name = 'GeoJSON Preview UI'; + readonly uri = 'ui://mapbox/geojson-preview/index.html'; + readonly description = + 'Interactive UI for previewing GeoJSON data (MCP Apps)'; + readonly mimeType = RESOURCE_MIME_TYPE; + + public async readCallback( + _uri: URL, + _extra: RequestHandlerExtra + ): Promise { + // Generate HTML with embedded iframe for GeoJSON visualization + const html = ` + + + + + GeoJSON Preview + + + +
Loading GeoJSON preview...
+
+ GeoJSON Preview +
+
Click to view full size
+ + + + +`; + + return { + contents: [ + { + uri: this.uri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + _meta: { + ui: { + csp: { + connectDomains: ['https://api.mapbox.com'], + resourceDomains: ['https://api.mapbox.com'] + }, + preferredSize: { + width: 1200, + height: 900 + } + } + } + } + ] + }; + } +} diff --git a/src/resources/ui-apps/PreviewStyleUIResource.ts b/src/resources/ui-apps/PreviewStyleUIResource.ts new file mode 100644 index 0000000..61d176a --- /dev/null +++ b/src/resources/ui-apps/PreviewStyleUIResource.ts @@ -0,0 +1,188 @@ +// 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 Mapbox Style Preview + * Implements MCP Apps pattern with ui:// scheme + */ +export class PreviewStyleUIResource extends BaseResource { + readonly name = 'Mapbox Style Preview UI'; + readonly uri = 'ui://mapbox/preview-style/index.html'; + readonly description = + 'Interactive UI for previewing Mapbox styles (MCP Apps)'; + readonly mimeType = RESOURCE_MIME_TYPE; + + public async readCallback( + _uri: URL, + _extra: RequestHandlerExtra + ): Promise { + const html = ` + + + + + Mapbox Style Preview + + + +
Loading style preview...
+ + + + + +`; + + return { + contents: [ + { + uri: this.uri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + _meta: { + ui: { + csp: { + connectDomains: ['https://*.mapbox.com'], + resourceDomains: ['https://*.mapbox.com'], + frameDomains: ['https://api.mapbox.com'] + } + } + } + } + ] + }; + } +} diff --git a/src/resources/ui-apps/StyleComparisonUIResource.ts b/src/resources/ui-apps/StyleComparisonUIResource.ts new file mode 100644 index 0000000..0618a78 --- /dev/null +++ b/src/resources/ui-apps/StyleComparisonUIResource.ts @@ -0,0 +1,188 @@ +// 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 Mapbox Style Comparison + * Implements MCP Apps pattern with ui:// scheme + */ +export class StyleComparisonUIResource extends BaseResource { + readonly name = 'Mapbox Style Comparison UI'; + readonly uri = 'ui://mapbox/style-comparison/index.html'; + readonly description = + 'Interactive UI for comparing Mapbox styles side-by-side (MCP Apps)'; + readonly mimeType = RESOURCE_MIME_TYPE; + + public async readCallback( + _uri: URL, + _extra: RequestHandlerExtra + ): Promise { + const html = ` + + + + + Mapbox Style Comparison + + + +
Loading style comparison...
+ + + + + +`; + + return { + contents: [ + { + uri: this.uri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + _meta: { + ui: { + csp: { + connectDomains: ['https://*.mapbox.com'], + resourceDomains: ['https://*.mapbox.com'], + frameDomains: ['https://agent.mapbox.com'] + } + } + } + } + ] + }; + } +} diff --git a/src/tools/BaseTool.ts b/src/tools/BaseTool.ts index eb0e389..e91b367 100644 --- a/src/tools/BaseTool.ts +++ b/src/tools/BaseTool.ts @@ -23,6 +23,16 @@ export abstract class BaseTool< readonly inputSchema: InputSchema; readonly outputSchema?: OutputSchema; + readonly meta?: { + ui?: { + resourceUri?: string; + csp?: { + connectDomains?: string[]; + resourceDomains?: string[]; + frameDomains?: string[]; + }; + }; + }; protected server: McpServer | null = null; constructor(params: { @@ -74,6 +84,16 @@ export abstract class BaseTool< // eslint-disable-next-line @typescript-eslint/no-explicit-any outputSchema?: any; annotations?: ToolAnnotations; + _meta?: { + ui?: { + resourceUri?: string; + csp?: { + connectDomains?: string[]; + resourceDomains?: string[]; + frameDomains?: string[]; + }; + }; + }; } = { title: this.annotations.title, description: this.description, @@ -91,6 +111,11 @@ export abstract class BaseTool< (this.outputSchema as unknown as z.ZodObject).shape; } + // Add _meta for MCP Apps support if provided (includes CSP configuration) + if (this.meta) { + config._meta = this.meta; + } + return server.registerTool( this.name, config, diff --git a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts index c8ecf6f..02f0e22 100644 --- a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts +++ b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts @@ -10,7 +10,6 @@ import { GeojsonPreviewSchema, GeojsonPreviewInput } from './GeojsonPreviewTool.input.schema.js'; -import { isMcpUiEnabled } from '../../config/toolConfig.js'; export class GeojsonPreviewTool extends BaseTool { name = 'geojson_preview_tool'; @@ -24,6 +23,16 @@ export class GeojsonPreviewTool extends BaseTool { title: 'Preview GeoJSON Data Tool' }; + readonly meta = { + ui: { + resourceUri: 'ui://mapbox/geojson-preview/index.html', + csp: { + connectDomains: ['https://api.mapbox.com'], + resourceDomains: ['https://api.mapbox.com'] + } + } + }; + constructor() { super({ inputSchema: GeojsonPreviewSchema }); } @@ -63,14 +72,13 @@ export class GeojsonPreviewTool extends BaseTool { } // Create a simplified GeoJSON for the overlay - // The Static API requires specific format for GeoJSON overlays const geojsonString = JSON.stringify(geojsonData); const encodedGeoJSON = encodeURIComponent(geojsonString); - // Use Mapbox Streets style with auto-bounds fitting + // Use Mapbox Light basemap style with auto-bounds fitting and retina display (@2x) // Format: /styles/v1/{username}/{style_id}/static/geojson({geojson})/auto/{width}x{height}@2x const staticImageUrl = - `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/` + + `https://api.mapbox.com/styles/v1/mapbox/light-v11/static/` + `geojson(${encodedGeoJSON})/auto/1000x700@2x` + `?access_token=${accessToken}`; @@ -105,61 +113,47 @@ export class GeojsonPreviewTool extends BaseTool { const encodedGeoJSON = encodeURIComponent(geojsonString); const geojsonIOUrl = `https://geojson.io/#data=data:application/json,${encodedGeoJSON}`; + // Try to generate a Mapbox Static Image URL + // The MCP App will fetch this and convert to blob URL to work with CSP + const staticImageUrl = this.generateStaticImageUrl(geojsonData); + + // Use static image URL if available (MCP App will handle CSP via blob URL), + // otherwise fall back to geojson.io + const displayUrl = staticImageUrl || geojsonIOUrl; + // Build content array with URL const content: CallToolResult['content'] = [ { type: 'text', - text: geojsonIOUrl + text: displayUrl } ]; - // Conditionally add MCP-UI resource if enabled - if (isMcpUiEnabled()) { - // Create content-addressable URI using hash of GeoJSON - // This enables client-side caching - same GeoJSON = same URI - const contentHash = createHash('md5') - .update(geojsonString) - .digest('hex') - .substring(0, 16); // Use first 16 chars for brevity - - // Try to generate a Mapbox Static Image URL - const staticImageUrl = this.generateStaticImageUrl(geojsonData); - - if (staticImageUrl) { - // Use Mapbox Static Images API - embeds as an image - const uiResource = createUIResource({ - uri: `ui://mapbox/geojson-preview/${contentHash}`, - content: { - type: 'externalUrl', - iframeUrl: staticImageUrl - }, - encoding: 'text', - uiMetadata: { - 'preferred-frame-size': ['1000px', '700px'] - } - }); - content.push(uiResource); - } else { - // Fallback to geojson.io URL (for large GeoJSON or when no token) - // Note: geojson.io may not work in iframes due to X-Frame-Options - const uiResource = createUIResource({ - uri: `ui://mapbox/geojson-preview/${contentHash}`, - content: { - type: 'externalUrl', - iframeUrl: geojsonIOUrl - }, - encoding: 'text', - uiMetadata: { - 'preferred-frame-size': ['1000px', '700px'] - } - }); - content.push(uiResource); + // Add MCP-UI resource (for legacy MCP-UI clients) + // Create content-addressable URI using hash of GeoJSON + // This enables client-side caching - same GeoJSON = same URI + const contentHash = createHash('md5') + .update(geojsonString) + .digest('hex') + .substring(0, 16); // Use first 16 chars for brevity + + // Use the same URL for MCP-UI as we returned in text content + const uiResource = createUIResource({ + uri: `ui://mapbox/geojson-preview/${contentHash}`, + content: { + type: 'externalUrl', + iframeUrl: displayUrl + }, + encoding: 'text', + uiMetadata: { + 'preferred-frame-size': ['1000px', '700px'] } - } + }); + content.push(uiResource); return { - isError: false, - content + content, + isError: false }; } catch (error) { const errorMessage = diff --git a/src/tools/preview-style-tool/PreviewStyleTool.ts b/src/tools/preview-style-tool/PreviewStyleTool.ts index cf028cf..e42cbe0 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.ts @@ -7,7 +7,6 @@ import { PreviewStyleInput } from './PreviewStyleTool.input.schema.js'; import { getUserNameFromToken } from '../../utils/jwtUtils.js'; -import { isMcpUiEnabled } from '../../config/toolConfig.js'; export class PreviewStyleTool extends BaseTool { readonly name = 'preview_style_tool'; @@ -21,6 +20,17 @@ export class PreviewStyleTool extends BaseTool { title: 'Preview Mapbox Style Tool' }; + readonly meta = { + ui: { + resourceUri: 'ui://mapbox/preview-style/index.html', + csp: { + connectDomains: ['https://*.mapbox.com'], + resourceDomains: ['https://*.mapbox.com'], + frameDomains: ['https://*.mapbox.com'] + } + } + }; + constructor() { super({ inputSchema: PreviewStyleSchema }); } @@ -73,21 +83,19 @@ export class PreviewStyleTool extends BaseTool { } ]; - // Conditionally add MCP-UI resource if enabled - if (isMcpUiEnabled()) { - const uiResource = createUIResource({ - uri: `ui://mapbox/preview-style/${userName}/${input.styleId}`, - content: { - type: 'externalUrl', - iframeUrl: url - }, - encoding: 'text', - uiMetadata: { - 'preferred-frame-size': ['1000px', '700px'] - } - }); - content.push(uiResource); - } + // Add MCP-UI resource (for legacy MCP-UI clients) + const uiResource = createUIResource({ + uri: `ui://mapbox/preview-style/${userName}/${input.styleId}`, + content: { + type: 'externalUrl', + iframeUrl: url + }, + encoding: 'text', + uiMetadata: { + 'preferred-frame-size': ['1000px', '700px'] + } + }); + content.push(uiResource); return { content, diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.ts b/src/tools/style-comparison-tool/StyleComparisonTool.ts index f9ce238..2edf613 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.ts @@ -9,7 +9,6 @@ import { StyleComparisonInput } from './StyleComparisonTool.schema.js'; import { getUserNameFromToken } from '../../utils/jwtUtils.js'; -import { isMcpUiEnabled } from '../../config/toolConfig.js'; export class StyleComparisonTool extends BaseTool< typeof StyleComparisonSchema @@ -25,6 +24,17 @@ export class StyleComparisonTool extends BaseTool< title: 'Compare Mapbox Styles Tool' }; + readonly meta = { + ui: { + resourceUri: 'ui://mapbox/style-comparison/index.html', + csp: { + connectDomains: ['https://*.mapbox.com'], + resourceDomains: ['https://*.mapbox.com'], + frameDomains: ['https://*.mapbox.com'] + } + } + }; + constructor() { super({ inputSchema: StyleComparisonSchema }); } @@ -109,21 +119,19 @@ export class StyleComparisonTool extends BaseTool< } ]; - // Conditionally add MCP-UI resource if enabled - if (isMcpUiEnabled()) { - const uiResource = createUIResource({ - uri: `ui://mapbox/style-comparison/${beforeStyleId}/${afterStyleId}`, - content: { - type: 'externalUrl', - iframeUrl: url - }, - encoding: 'text', - uiMetadata: { - 'preferred-frame-size': ['1000px', '700px'] - } - }); - content.push(uiResource); - } + // Add MCP-UI resource (for legacy MCP-UI clients) + const uiResource = createUIResource({ + uri: `ui://mapbox/style-comparison/${beforeStyleId}/${afterStyleId}`, + content: { + type: 'externalUrl', + iframeUrl: url + }, + encoding: 'text', + uiMetadata: { + 'preferred-frame-size': ['1000px', '700px'] + } + }); + content.push(uiResource); return { content, diff --git a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts index b2cddf3..f504996 100644 --- a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts +++ b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts @@ -40,11 +40,9 @@ describe('GeojsonPreviewTool', () => { expect(result.content[0].type).toBe('text'); const content = result.content[0]; if (content.type === 'text') { + // Should return Mapbox Static Images URL or geojson.io fallback expect(content.text).toMatch( - /^https:\/\/geojson\.io\/#data=data:application\/json,/ - ); - expect(content.text).toContain( - encodeURIComponent(JSON.stringify(pointGeoJSON)) + /^https:\/\/(api\.mapbox\.com\/styles\/|geojson\.io)/ ); } @@ -67,10 +65,7 @@ describe('GeojsonPreviewTool', () => { ).toBe(true); }); - it('returns only URL when MCP-UI is disabled', async () => { - // Disable MCP-UI for this test - process.env.ENABLE_MCP_UI = 'false'; - + it('returns URL and MCP-UI resource for backward compatibility', async () => { const tool = new GeojsonPreviewTool(); const pointGeoJSON = { type: 'Point', @@ -80,11 +75,11 @@ describe('GeojsonPreviewTool', () => { const result = await tool.run({ geojson: JSON.stringify(pointGeoJSON) }); expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); + // Now returns both URL and MCP-UI resource + expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); - - // Clean up - delete process.env.ENABLE_MCP_UI; + // Second item is MCP-UI resource + expect(result.content[1].type).toBe('resource'); }); it('should handle GeoJSON as string', async () => { @@ -102,12 +97,14 @@ describe('GeojsonPreviewTool', () => { const result = await tool.run({ geojson: geoJSONString }); expect(result.isError).toBe(false); + // Now returns both URL and MCP-UI resource + expect(result.content).toHaveLength(2); const content = result.content[0]; if (content.type === 'text') { + // Should return Mapbox Static Images URL or geojson.io fallback expect(content.text).toMatch( - /^https:\/\/geojson\.io\/#data=data:application\/json,/ + /^https:\/\/(api\.mapbox\.com\/styles\/|geojson\.io)/ ); - expect(content.text).toContain(encodeURIComponent(geoJSONString)); } }); @@ -140,13 +137,13 @@ describe('GeojsonPreviewTool', () => { }); expect(result.isError).toBe(false); + // Now returns both URL and MCP-UI resource + expect(result.content).toHaveLength(2); const content = result.content[0]; if (content.type === 'text') { + // Should return Mapbox Static Images URL or geojson.io fallback expect(content.text).toMatch( - /^https:\/\/geojson\.io\/#data=data:application\/json,/ - ); - expect(content.text).toContain( - encodeURIComponent(JSON.stringify(featureCollection)) + /^https:\/\/(api\.mapbox\.com\/styles\/|geojson\.io)/ ); } }); @@ -195,16 +192,13 @@ describe('GeojsonPreviewTool', () => { const result = await tool.run({ geojson: JSON.stringify(geoJSON) }); expect(result.isError).toBe(false); + // Now returns both URL and MCP-UI resource + expect(result.content).toHaveLength(2); const content = result.content[0]; if (content.type === 'text') { + // Should return Mapbox Static Images URL or geojson.io fallback expect(content.text).toMatch( - /^https:\/\/geojson\.io\/#data=data:application\/json,/ - ); - // Verify URL contains properly encoded content - expect(content.text).toContain('%22'); // Encoded quotes - expect(content.text).toContain('%26'); // Encoded ampersand - expect(content.text).toContain( - encodeURIComponent('Test & Special Characters!') + /^https:\/\/(api\.mapbox\.com\/styles\/|geojson\.io)/ ); } }); diff --git a/test/tools/preview-style-tool/PreviewStyleTool.test.ts b/test/tools/preview-style-tool/PreviewStyleTool.test.ts index e8315c0..87feef8 100644 --- a/test/tools/preview-style-tool/PreviewStyleTool.test.ts +++ b/test/tools/preview-style-tool/PreviewStyleTool.test.ts @@ -172,10 +172,7 @@ describe('PreviewStyleTool', () => { }); }); - it('returns only URL when MCP-UI is disabled', async () => { - // Disable MCP-UI for this test - process.env.ENABLE_MCP_UI = 'false'; - + it('returns URL and MCP-UI resource for backward compatibility', async () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', accessToken: TEST_ACCESS_TOKEN, @@ -184,15 +181,17 @@ describe('PreviewStyleTool', () => { }); expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); + // Now returns both URL (for text) and MCP-UI resource (for backward compat) + expect(result.content).toHaveLength(2); expect(result.content[0]).toMatchObject({ type: 'text', text: expect.stringContaining( 'https://api.mapbox.com/styles/v1/test-user/test-style.html?access_token=pk.' ) }); - - // Clean up - delete process.env.ENABLE_MCP_UI; + // Second item is MCP-UI resource + expect(result.content[1]).toMatchObject({ + type: 'resource' + }); }); }); diff --git a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts index b90ecfa..ee7013a 100644 --- a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts +++ b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts @@ -214,10 +214,7 @@ describe('StyleComparisonTool', () => { expect(url2).not.toContain('#'); }); - it('should return only URL when MCP-UI is disabled', async () => { - // Disable MCP-UI for this test - process.env.ENABLE_MCP_UI = 'false'; - + it('should return URL and MCP-UI resource for backward compatibility', async () => { const input = { before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', @@ -227,11 +224,11 @@ describe('StyleComparisonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); + // Now returns both URL (for text) and MCP-UI resource (for backward compat) + expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); - - // Clean up - delete process.env.ENABLE_MCP_UI; + // Second item is MCP-UI resource + expect(result.content[1].type).toBe('resource'); }); });