From e846426f046793e75d8feb0728404c3dd8b4056e Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Fri, 7 Nov 2025 09:38:09 -0500 Subject: [PATCH 1/3] [mcp-ui] Adding MCP-UI support to those that support URLs --- CLAUDE.md | 23 +++++++ README.md | 36 ++++++++++ package-lock.json | 14 +++- package.json | 1 + src/config/toolConfig.ts | 40 +++++++++++ .../GeojsonPreviewTool.ts | 30 +++++++-- .../preview-style-tool/PreviewStyleTool.ts | 30 +++++++-- .../StyleComparisonTool.ts | 30 +++++++-- test/config/toolConfig.test.ts | 66 +++++++++++++++---- .../GeojsonPreviewTool.test.ts | 36 +++++++++- .../PreviewStyleTool.test.ts | 40 ++++++++++- 11 files changed, 311 insertions(+), 35 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ec691ae..8887f5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -130,6 +130,29 @@ Each tool execution span includes: - Tools can be enabled/disabled at startup (see `TOOL_CONFIGURATION.md`) - Example: `node dist/esm/index.js --enable-tools list_styles_tool,create_style_tool` +### MCP-UI Support (Enabled by Default) + +MCP-UI allows tools that return URLs to also provide interactive iframe resources. **Enabled by default** and fully backwards compatible. + +**Supported tools:** + +- `preview_style_tool` - Embeds style previews +- `geojson_preview_tool` - Embeds GeoJSON visualizations +- `style_comparison_tool` - Embeds style comparisons + +**How it works:** + +- Tools return both text URL and UIResource +- Clients without MCP-UI support (e.g., Claude Desktop) ignore UIResource +- Clients with MCP-UI support (e.g., Goose) render iframes + +**Disable if needed:** + +- Environment variable: `ENABLE_MCP_UI=false` +- Command-line flag: `--disable-mcp-ui` + +**Note:** You rarely need to disable this. See [mcpui.dev](https://mcpui.dev) for compatible clients. + ## Mapbox Token Scopes - Each tool requires specific Mapbox token scopes (see `README.md` for details) diff --git a/README.md b/README.md index 4db8fd3..b542c8a 100644 --- a/README.md +++ b/README.md @@ -671,6 +671,42 @@ Set `VERBOSE_ERRORS=true` to get detailed error messages from the MCP server. Th By default, the server returns generic error messages. With verbose errors enabled, you'll receive the actual error details, which can help diagnose API connection issues, invalid parameters, or other problems. +#### ENABLE_MCP_UI + +**MCP-UI Support (Enabled by Default)** + +MCP-UI allows tools that return URLs to also return interactive iframe resources that can be embedded directly in supporting MCP clients. **This is enabled by default** and is fully backwards compatible with all MCP clients. + +**Supported Tools:** + +- `preview_style_tool` - Embeds Mapbox style previews +- `geojson_preview_tool` - Embeds geojson.io visualizations +- `style_comparison_tool` - Embeds side-by-side style comparisons + +**How it Works:** + +- Tools return **both** a text URL (always works) and a UIResource for iframe embedding +- Clients that don't support MCP-UI (like Claude Desktop) simply ignore the UIResource and use the text URL +- Clients that support MCP-UI (like Goose) can render the iframe for a richer experience + +**Disabling MCP-UI (Optional):** + +If you want to disable MCP-UI support: + +Via environment variable: + +```bash +export ENABLE_MCP_UI=false +``` + +Or via command-line flag: + +```bash +node dist/esm/index.js --disable-mcp-ui +``` + +**Note:** You typically don't need to disable this. The implementation is fully backwards compatible and doesn't affect clients that don't support MCP-UI. See [mcpui.dev](https://mcpui.dev) for compatible clients. + ## Troubleshooting **Issue:** Tools fail with authentication errors diff --git a/package-lock.json b/package-lock.json index c5b9f8e..8477ac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@mapbox/mcp-devkit-server", - "version": "0.4.5", + "version": "0.4.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mapbox/mcp-devkit-server", - "version": "0.4.5", + "version": "0.4.6", "license": "MIT", "dependencies": { + "@mcp-ui/server": "^5.13.1", "@modelcontextprotocol/sdk": "^1.17.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.56.0", @@ -1770,6 +1771,15 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@mcp-ui/server": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mcp-ui/server/-/server-5.13.1.tgz", + "integrity": "sha512-jTHy8MTa+Cppw80mFID5ranw+CrmePpncIub726PGg38WETGR8tg7CW0mD9YS6pBdRL+A+EDNCiGbVlPjDfQHA==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "*" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", diff --git a/package.json b/package.json index e2bc20d..2565412 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "mcp" ], "dependencies": { + "@mcp-ui/server": "^5.13.1", "@modelcontextprotocol/sdk": "^1.17.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.56.0", diff --git a/src/config/toolConfig.ts b/src/config/toolConfig.ts index d2b323c..d37f3ba 100644 --- a/src/config/toolConfig.ts +++ b/src/config/toolConfig.ts @@ -3,12 +3,18 @@ import { ToolInstance } from '../tools/toolRegistry.js'; export interface ToolConfig { enabledTools?: string[]; disabledTools?: string[]; + enableMcpUi?: boolean; } export function parseToolConfigFromArgs(): ToolConfig { const args = process.argv.slice(2); const config: ToolConfig = {}; + // Check environment variable first (takes precedence) + if (process.env.ENABLE_MCP_UI !== undefined) { + config.enableMcpUi = process.env.ENABLE_MCP_UI === 'true'; + } + for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -22,9 +28,19 @@ export function parseToolConfigFromArgs(): ToolConfig { if (value) { config.disabledTools = value.split(',').map((t) => t.trim()); } + } else if (arg === '--disable-mcp-ui') { + // Command-line flag can disable it if env var not set + if (config.enableMcpUi === undefined) { + config.enableMcpUi = false; + } } } + // Default to true if not set (enabled by default) + if (config.enableMcpUi === undefined) { + config.enableMcpUi = true; + } + return config; } @@ -53,3 +69,27 @@ export function filterTools( return filteredTools; } + +/** + * Check if MCP-UI support is enabled. + * MCP-UI is enabled by default and can be explicitly disabled via: + * - Environment variable: ENABLE_MCP_UI=false + * - Command-line flag: --disable-mcp-ui + * + * @returns true if MCP-UI is enabled (default), false if explicitly disabled + */ +export function isMcpUiEnabled(): boolean { + // Check environment variable first (takes precedence) + if (process.env.ENABLE_MCP_UI === 'false') { + return false; + } + + // Check command-line arguments + const args = process.argv.slice(2); + if (args.includes('--disable-mcp-ui')) { + return false; + } + + // Default to enabled + return true; +} diff --git a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts index 6d1e640..c6e74a7 100644 --- a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts +++ b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts @@ -2,12 +2,14 @@ // Licensed under the MIT License. import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { createUIResource } from '@mcp-ui/server'; import { GeoJSON } from 'geojson'; import { BaseTool } from '../BaseTool.js'; import { GeojsonPreviewSchema, GeojsonPreviewInput } from './GeojsonPreviewTool.input.schema.js'; +import { isMcpUiEnabled } from '../../config/toolConfig.js'; export class GeojsonPreviewTool extends BaseTool { name = 'geojson_preview_tool'; @@ -72,14 +74,30 @@ export class GeojsonPreviewTool extends BaseTool { const encodedGeoJSON = encodeURIComponent(geojsonString); const geojsonIOUrl = `https://geojson.io/#data=data:application/json,${encodedGeoJSON}`; + // Build content array with URL + const content: CallToolResult['content'] = [ + { + type: 'text', + text: geojsonIOUrl + } + ]; + + // Conditionally add MCP-UI resource if enabled + if (isMcpUiEnabled()) { + const uiResource = createUIResource({ + uri: `ui://mapbox/geojson-preview/${Date.now()}`, + content: { + type: 'externalUrl', + iframeUrl: geojsonIOUrl + }, + encoding: 'text' + }); + content.push(uiResource); + } + return { isError: false, - content: [ - { - type: 'text', - text: geojsonIOUrl - } - ] + content }; } catch (error) { const errorMessage = diff --git a/src/tools/preview-style-tool/PreviewStyleTool.ts b/src/tools/preview-style-tool/PreviewStyleTool.ts index bb0adc8..b50af2c 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.ts @@ -1,4 +1,5 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { createUIResource } from '@mcp-ui/server'; import { BaseTool } from '../BaseTool.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { @@ -6,6 +7,7 @@ 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'; @@ -63,13 +65,29 @@ export class PreviewStyleTool extends BaseTool { const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${userName}/${input.styleId}.html?${params.toString()}${hashFragment}`; + // Build content array with URL + const content: CallToolResult['content'] = [ + { + type: 'text', + text: url + } + ]; + + // 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' + }); + content.push(uiResource); + } + return { - content: [ - { - type: 'text', - text: url - } - ], + content, isError: false }; } diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.ts b/src/tools/style-comparison-tool/StyleComparisonTool.ts index 1bfb6ac..700b8ac 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.ts @@ -2,12 +2,14 @@ // Licensed under the MIT License. import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { createUIResource } from '@mcp-ui/server'; import { BaseTool } from '../BaseTool.js'; import { StyleComparisonSchema, StyleComparisonInput } from './StyleComparisonTool.schema.js'; import { getUserNameFromToken } from '../../utils/jwtUtils.js'; +import { isMcpUiEnabled } from '../../config/toolConfig.js'; export class StyleComparisonTool extends BaseTool< typeof StyleComparisonSchema @@ -99,13 +101,29 @@ export class StyleComparisonTool extends BaseTool< url += `#${input.zoom}/${input.latitude}/${input.longitude}`; } + // Build content array with URL + const content: CallToolResult['content'] = [ + { + type: 'text', + text: url + } + ]; + + // 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' + }); + content.push(uiResource); + } + return { - content: [ - { - type: 'text', - text: url - } - ], + content, isError: false }; } diff --git a/test/config/toolConfig.test.ts b/test/config/toolConfig.test.ts index 74c34e9..c5e3d3f 100644 --- a/test/config/toolConfig.test.ts +++ b/test/config/toolConfig.test.ts @@ -31,17 +31,18 @@ describe('Tool Configuration', () => { }); describe('parseToolConfigFromArgs', () => { - it('should return empty config when no arguments provided', () => { + it('should return config with enableMcpUi true when no arguments provided', () => { process.argv = ['node', 'index.js']; const config = parseToolConfigFromArgs(); - expect(config).toEqual({}); + expect(config).toEqual({ enableMcpUi: true }); }); it('should parse --enable-tools with single tool', () => { process.argv = ['node', 'index.js', '--enable-tools', 'list_styles_tool']; const config = parseToolConfigFromArgs(); expect(config).toEqual({ - enabledTools: ['list_styles_tool'] + enabledTools: ['list_styles_tool'], + enableMcpUi: true }); }); @@ -58,7 +59,8 @@ describe('Tool Configuration', () => { 'list_styles_tool', 'create_style_tool', 'preview_style_tool' - ] + ], + enableMcpUi: true }); }); @@ -75,7 +77,8 @@ describe('Tool Configuration', () => { 'list_styles_tool', 'create_style_tool', 'preview_style_tool' - ] + ], + enableMcpUi: true }); }); @@ -88,7 +91,8 @@ describe('Tool Configuration', () => { ]; const config = parseToolConfigFromArgs(); expect(config).toEqual({ - disabledTools: ['delete_style_tool'] + disabledTools: ['delete_style_tool'], + enableMcpUi: true }); }); @@ -101,7 +105,8 @@ describe('Tool Configuration', () => { ]; const config = parseToolConfigFromArgs(); expect(config).toEqual({ - disabledTools: ['delete_style_tool', 'update_style_tool'] + disabledTools: ['delete_style_tool', 'update_style_tool'], + enableMcpUi: true }); }); @@ -117,20 +122,21 @@ describe('Tool Configuration', () => { const config = parseToolConfigFromArgs(); expect(config).toEqual({ enabledTools: ['list_styles_tool'], - disabledTools: ['delete_style_tool'] + disabledTools: ['delete_style_tool'], + enableMcpUi: true }); }); it('should handle missing value for --enable-tools', () => { process.argv = ['node', 'index.js', '--enable-tools']; const config = parseToolConfigFromArgs(); - expect(config).toEqual({}); + expect(config).toEqual({ enableMcpUi: true }); }); it('should handle missing value for --disable-tools', () => { process.argv = ['node', 'index.js', '--disable-tools']; const config = parseToolConfigFromArgs(); - expect(config).toEqual({}); + expect(config).toEqual({ enableMcpUi: true }); }); it('should ignore unknown arguments', () => { @@ -144,7 +150,45 @@ describe('Tool Configuration', () => { ]; const config = parseToolConfigFromArgs(); expect(config).toEqual({ - enabledTools: ['list_styles_tool'] + enabledTools: ['list_styles_tool'], + enableMcpUi: true + }); + }); + + it('should disable MCP-UI with --disable-mcp-ui flag', () => { + process.argv = ['node', 'index.js', '--disable-mcp-ui']; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ enableMcpUi: false }); + }); + + it('should enable MCP-UI with ENABLE_MCP_UI=true env var', () => { + process.env.ENABLE_MCP_UI = 'true'; + process.argv = ['node', 'index.js']; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ enableMcpUi: true }); + delete process.env.ENABLE_MCP_UI; + }); + + it('should prioritize env var over command-line flag for MCP-UI', () => { + process.env.ENABLE_MCP_UI = 'false'; + process.argv = ['node', 'index.js']; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ enableMcpUi: false }); + delete process.env.ENABLE_MCP_UI; + }); + + it('should combine MCP-UI flag with tool config', () => { + process.argv = [ + 'node', + 'index.js', + '--disable-mcp-ui', + '--enable-tools', + 'preview_style_tool' + ]; + const config = parseToolConfigFromArgs(); + expect(config).toEqual({ + enabledTools: ['preview_style_tool'], + enableMcpUi: false }); }); }); diff --git a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts index f62b670..f9ea8a9 100644 --- a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts +++ b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts @@ -26,7 +26,7 @@ describe('GeojsonPreviewTool', () => { }); }); - it('should generate geojson.io URL for Point geometry', async () => { + it('should generate geojson.io URL and MCP-UI resource for Point geometry (default)', async () => { const tool = new GeojsonPreviewTool(); const pointGeoJSON = { type: 'Point', @@ -36,7 +36,7 @@ describe('GeojsonPreviewTool', () => { const result = await tool.run({ geojson: JSON.stringify(pointGeoJSON) }); expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); + expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); const content = result.content[0]; if (content.type === 'text') { @@ -47,6 +47,38 @@ describe('GeojsonPreviewTool', () => { encodeURIComponent(JSON.stringify(pointGeoJSON)) ); } + + // Verify MCP-UI resource is included by default + expect(result.content[1]).toMatchObject({ + type: 'resource', + resource: { + uri: expect.stringMatching(/^ui:\/\/mapbox\/geojson-preview\//), + mimeType: 'text/uri-list', + text: expect.stringContaining( + 'https://geojson.io/#data=data:application/json,' + ) + } + }); + }); + + it('returns only URL when MCP-UI is disabled', async () => { + // Disable MCP-UI for this test + process.env.ENABLE_MCP_UI = 'false'; + + const tool = new GeojsonPreviewTool(); + const pointGeoJSON = { + type: 'Point', + coordinates: [-122.4194, 37.7749] + }; + + const result = await tool.run({ geojson: JSON.stringify(pointGeoJSON) }); + + expect(result.isError).toBe(false); + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe('text'); + + // Clean up + delete process.env.ENABLE_MCP_UI; }); it('should handle GeoJSON as string', async () => { diff --git a/test/tools/preview-style-tool/PreviewStyleTool.test.ts b/test/tools/preview-style-tool/PreviewStyleTool.test.ts index ea70336..e8315c0 100644 --- a/test/tools/preview-style-tool/PreviewStyleTool.test.ts +++ b/test/tools/preview-style-tool/PreviewStyleTool.test.ts @@ -136,7 +136,7 @@ describe('PreviewStyleTool', () => { }); }); - it('returns URL on success', async () => { + it('returns URL and MCP-UI resource on success (default)', async () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', accessToken: TEST_ACCESS_TOKEN, @@ -145,7 +145,7 @@ describe('PreviewStyleTool', () => { }); expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); + expect(result.content).toHaveLength(2); expect(result.content[0]).toMatchObject({ type: 'text', text: expect.stringContaining( @@ -158,5 +158,41 @@ describe('PreviewStyleTool', () => { type: 'text', text: expect.stringContaining('fresh=true') }); + + // Verify MCP-UI resource is included by default + expect(result.content[1]).toMatchObject({ + type: 'resource', + resource: { + uri: expect.stringMatching(/^ui:\/\/mapbox\/preview-style\//), + mimeType: 'text/uri-list', + text: expect.stringContaining( + 'https://api.mapbox.com/styles/v1/test-user/test-style.html?access_token=pk.' + ) + } + }); + }); + + it('returns only URL when MCP-UI is disabled', async () => { + // Disable MCP-UI for this test + process.env.ENABLE_MCP_UI = 'false'; + + const result = await new PreviewStyleTool().run({ + styleId: 'test-style', + accessToken: TEST_ACCESS_TOKEN, + title: false, + zoomwheel: false + }); + + expect(result.isError).toBe(false); + expect(result.content).toHaveLength(1); + 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; }); }); From 2817d8f8bd46b34bf497cab6cc10a09ede93a97c Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Fri, 7 Nov 2025 10:51:24 -0500 Subject: [PATCH 2/3] [mcp-ui] Adding MCP-UI support to those that support URLs --- .../GeojsonPreviewTool.ts | 10 ++++- .../StyleComparisonTool.test.ts | 37 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts index c6e74a7..d98617f 100644 --- a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts +++ b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts @@ -3,6 +3,7 @@ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { createUIResource } from '@mcp-ui/server'; +import { createHash } from 'node:crypto'; import { GeoJSON } from 'geojson'; import { BaseTool } from '../BaseTool.js'; import { @@ -84,8 +85,15 @@ export class GeojsonPreviewTool extends BaseTool { // 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 + const uiResource = createUIResource({ - uri: `ui://mapbox/geojson-preview/${Date.now()}`, + uri: `ui://mapbox/geojson-preview/${contentHash}`, content: { type: 'externalUrl', iframeUrl: geojsonIOUrl diff --git a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts index 4856ebf..b90ecfa 100644 --- a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts +++ b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts @@ -17,7 +17,7 @@ describe('StyleComparisonTool', () => { }); describe('run', () => { - it('should generate comparison URL with provided access token', async () => { + it('should generate comparison URL and MCP-UI resource with provided access token (default)', async () => { const input = { before: 'mapbox/streets-v12', after: 'mapbox/outdoors-v12', @@ -27,12 +27,27 @@ describe('StyleComparisonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); + expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); const url = (result.content[0] as { type: 'text'; text: string }).text; expect(url).toContain('https://agent.mapbox.com/tools/style-compare'); expect(url).toContain('access_token=pk.test.token'); expect(url).toContain('before=mapbox%2Fstreets-v12'); expect(url).toContain('after=mapbox%2Foutdoors-v12'); + + // Verify MCP-UI resource is included by default + expect(result.content[1]).toMatchObject({ + type: 'resource', + resource: { + uri: expect.stringMatching( + /^ui:\/\/mapbox\/style-comparison\/mapbox\/streets-v12\/mapbox\/outdoors-v12$/ + ), + mimeType: 'text/uri-list', + text: expect.stringContaining( + 'https://agent.mapbox.com/tools/style-compare' + ) + } + }); }); it('should require access token', async () => { @@ -198,6 +213,26 @@ describe('StyleComparisonTool', () => { const url2 = (result2.content[0] as { type: 'text'; text: string }).text; 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'; + + const input = { + before: 'mapbox/streets-v12', + after: 'mapbox/outdoors-v12', + accessToken: 'pk.test.token' + }; + + const result = await tool.run(input); + + expect(result.isError).toBe(false); + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe('text'); + + // Clean up + delete process.env.ENABLE_MCP_UI; + }); }); describe('metadata', () => { From 86a290844cbd1b824be1806cb39988d4abce7be8 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Fri, 7 Nov 2025 13:16:27 -0500 Subject: [PATCH 3/3] [mcp-ui] Adding MCP-UI support to those that support URLs --- .../GeojsonPreviewTool.ts | 72 ++++++++++++++++--- .../preview-style-tool/PreviewStyleTool.ts | 5 +- .../StyleComparisonTool.ts | 5 +- test/config/toolConfig.test.ts | 10 ++- .../GeojsonPreviewTool.test.ts | 12 +++- 5 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts index d98617f..c8ecf6f 100644 --- a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts +++ b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts @@ -52,6 +52,36 @@ export class GeojsonPreviewTool extends BaseTool { ); } + /** + * Generate a Mapbox Static Images API URL for the GeoJSON data + * @see https://docs.mapbox.com/api/maps/static-images/ + */ + private generateStaticImageUrl(geojsonData: GeoJSON): string | null { + const accessToken = process.env.MAPBOX_ACCESS_TOKEN; + if (!accessToken) { + return null; // Fallback to geojson.io if no token available + } + + // 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 + // 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/` + + `geojson(${encodedGeoJSON})/auto/1000x700@2x` + + `?access_token=${accessToken}`; + + // Check if URL is too long (browsers typically limit to ~8192 chars) + if (staticImageUrl.length > 8000) { + return null; // Fallback to geojson.io for large GeoJSON + } + + return staticImageUrl; + } + protected async execute(input: GeojsonPreviewInput): Promise { try { // Parse and validate JSON format @@ -92,15 +122,39 @@ export class GeojsonPreviewTool extends BaseTool { .digest('hex') .substring(0, 16); // Use first 16 chars for brevity - const uiResource = createUIResource({ - uri: `ui://mapbox/geojson-preview/${contentHash}`, - content: { - type: 'externalUrl', - iframeUrl: geojsonIOUrl - }, - encoding: 'text' - }); - content.push(uiResource); + // 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); + } } return { diff --git a/src/tools/preview-style-tool/PreviewStyleTool.ts b/src/tools/preview-style-tool/PreviewStyleTool.ts index b50af2c..cf028cf 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.ts @@ -81,7 +81,10 @@ export class PreviewStyleTool extends BaseTool { type: 'externalUrl', iframeUrl: url }, - encoding: 'text' + encoding: 'text', + uiMetadata: { + 'preferred-frame-size': ['1000px', '700px'] + } }); content.push(uiResource); } diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.ts b/src/tools/style-comparison-tool/StyleComparisonTool.ts index 700b8ac..f9ce238 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.ts @@ -117,7 +117,10 @@ export class StyleComparisonTool extends BaseTool< type: 'externalUrl', iframeUrl: url }, - encoding: 'text' + encoding: 'text', + uiMetadata: { + 'preferred-frame-size': ['1000px', '700px'] + } }); content.push(uiResource); } diff --git a/test/config/toolConfig.test.ts b/test/config/toolConfig.test.ts index c5e3d3f..16b9756 100644 --- a/test/config/toolConfig.test.ts +++ b/test/config/toolConfig.test.ts @@ -19,15 +19,23 @@ vi.mock('../../src/utils/versionUtils.js', () => ({ describe('Tool Configuration', () => { // Save original argv const originalArgv = process.argv; + const originalEnableMcpUi = process.env.ENABLE_MCP_UI; beforeEach(() => { // Reset argv before each test process.argv = [...originalArgv]; + // Reset environment variable before each test + delete process.env.ENABLE_MCP_UI; }); afterAll(() => { - // Restore original argv + // Restore original argv and env process.argv = originalArgv; + if (originalEnableMcpUi !== undefined) { + process.env.ENABLE_MCP_UI = originalEnableMcpUi; + } else { + delete process.env.ENABLE_MCP_UI; + } }); describe('parseToolConfigFromArgs', () => { diff --git a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts index f9ea8a9..b2cddf3 100644 --- a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts +++ b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts @@ -49,16 +49,22 @@ describe('GeojsonPreviewTool', () => { } // Verify MCP-UI resource is included by default + // Could be either Mapbox Static Images API or geojson.io (fallback) expect(result.content[1]).toMatchObject({ type: 'resource', resource: { uri: expect.stringMatching(/^ui:\/\/mapbox\/geojson-preview\//), mimeType: 'text/uri-list', - text: expect.stringContaining( - 'https://geojson.io/#data=data:application/json,' - ) + text: expect.stringMatching(/^https:\/\//) } }); + + // Verify the iframe URL is either Mapbox Static Images API or geojson.io + const iframeUrl = (result.content[1] as any).resource.text; + expect( + iframeUrl.includes('api.mapbox.com/styles') || + iframeUrl.includes('geojson.io') + ).toBe(true); }); it('returns only URL when MCP-UI is disabled', async () => {