Skip to content

Commit 2b2ba53

Browse files
Merge pull request #42 from mapbox/mcpui
[mcp-ui] Adding MCP-UI support to those that support URLs
2 parents 7201837 + 86a2908 commit 2b2ba53

File tree

12 files changed

+430
-37
lines changed

12 files changed

+430
-37
lines changed

CLAUDE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,29 @@ Each tool execution span includes:
130130
- Tools can be enabled/disabled at startup (see `TOOL_CONFIGURATION.md`)
131131
- Example: `node dist/esm/index.js --enable-tools list_styles_tool,create_style_tool`
132132

133+
### MCP-UI Support (Enabled by Default)
134+
135+
MCP-UI allows tools that return URLs to also provide interactive iframe resources. **Enabled by default** and fully backwards compatible.
136+
137+
**Supported tools:**
138+
139+
- `preview_style_tool` - Embeds style previews
140+
- `geojson_preview_tool` - Embeds GeoJSON visualizations
141+
- `style_comparison_tool` - Embeds style comparisons
142+
143+
**How it works:**
144+
145+
- Tools return both text URL and UIResource
146+
- Clients without MCP-UI support (e.g., Claude Desktop) ignore UIResource
147+
- Clients with MCP-UI support (e.g., Goose) render iframes
148+
149+
**Disable if needed:**
150+
151+
- Environment variable: `ENABLE_MCP_UI=false`
152+
- Command-line flag: `--disable-mcp-ui`
153+
154+
**Note:** You rarely need to disable this. See [mcpui.dev](https://mcpui.dev) for compatible clients.
155+
133156
## Mapbox Token Scopes
134157

135158
- Each tool requires specific Mapbox token scopes (see `README.md` for details)

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,42 @@ Set `VERBOSE_ERRORS=true` to get detailed error messages from the MCP server. Th
671671

672672
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.
673673

674+
#### ENABLE_MCP_UI
675+
676+
**MCP-UI Support (Enabled by Default)**
677+
678+
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.
679+
680+
**Supported Tools:**
681+
682+
- `preview_style_tool` - Embeds Mapbox style previews
683+
- `geojson_preview_tool` - Embeds geojson.io visualizations
684+
- `style_comparison_tool` - Embeds side-by-side style comparisons
685+
686+
**How it Works:**
687+
688+
- Tools return **both** a text URL (always works) and a UIResource for iframe embedding
689+
- Clients that don't support MCP-UI (like Claude Desktop) simply ignore the UIResource and use the text URL
690+
- Clients that support MCP-UI (like Goose) can render the iframe for a richer experience
691+
692+
**Disabling MCP-UI (Optional):**
693+
694+
If you want to disable MCP-UI support:
695+
696+
Via environment variable:
697+
698+
```bash
699+
export ENABLE_MCP_UI=false
700+
```
701+
702+
Or via command-line flag:
703+
704+
```bash
705+
node dist/esm/index.js --disable-mcp-ui
706+
```
707+
708+
**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.
709+
674710
## Troubleshooting
675711

676712
**Issue:** Tools fail with authentication errors

package-lock.json

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"mcp"
4747
],
4848
"dependencies": {
49+
"@mcp-ui/server": "^5.13.1",
4950
"@modelcontextprotocol/sdk": "^1.17.5",
5051
"@opentelemetry/api": "^1.9.0",
5152
"@opentelemetry/auto-instrumentations-node": "^0.56.0",

src/config/toolConfig.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import { ToolInstance } from '../tools/toolRegistry.js';
33
export interface ToolConfig {
44
enabledTools?: string[];
55
disabledTools?: string[];
6+
enableMcpUi?: boolean;
67
}
78

89
export function parseToolConfigFromArgs(): ToolConfig {
910
const args = process.argv.slice(2);
1011
const config: ToolConfig = {};
1112

13+
// Check environment variable first (takes precedence)
14+
if (process.env.ENABLE_MCP_UI !== undefined) {
15+
config.enableMcpUi = process.env.ENABLE_MCP_UI === 'true';
16+
}
17+
1218
for (let i = 0; i < args.length; i++) {
1319
const arg = args[i];
1420

@@ -22,9 +28,19 @@ export function parseToolConfigFromArgs(): ToolConfig {
2228
if (value) {
2329
config.disabledTools = value.split(',').map((t) => t.trim());
2430
}
31+
} else if (arg === '--disable-mcp-ui') {
32+
// Command-line flag can disable it if env var not set
33+
if (config.enableMcpUi === undefined) {
34+
config.enableMcpUi = false;
35+
}
2536
}
2637
}
2738

39+
// Default to true if not set (enabled by default)
40+
if (config.enableMcpUi === undefined) {
41+
config.enableMcpUi = true;
42+
}
43+
2844
return config;
2945
}
3046

@@ -53,3 +69,27 @@ export function filterTools(
5369

5470
return filteredTools;
5571
}
72+
73+
/**
74+
* Check if MCP-UI support is enabled.
75+
* MCP-UI is enabled by default and can be explicitly disabled via:
76+
* - Environment variable: ENABLE_MCP_UI=false
77+
* - Command-line flag: --disable-mcp-ui
78+
*
79+
* @returns true if MCP-UI is enabled (default), false if explicitly disabled
80+
*/
81+
export function isMcpUiEnabled(): boolean {
82+
// Check environment variable first (takes precedence)
83+
if (process.env.ENABLE_MCP_UI === 'false') {
84+
return false;
85+
}
86+
87+
// Check command-line arguments
88+
const args = process.argv.slice(2);
89+
if (args.includes('--disable-mcp-ui')) {
90+
return false;
91+
}
92+
93+
// Default to enabled
94+
return true;
95+
}

src/tools/geojson-preview-tool/GeojsonPreviewTool.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
// Licensed under the MIT License.
33

44
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
5+
import { createUIResource } from '@mcp-ui/server';
6+
import { createHash } from 'node:crypto';
57
import { GeoJSON } from 'geojson';
68
import { BaseTool } from '../BaseTool.js';
79
import {
810
GeojsonPreviewSchema,
911
GeojsonPreviewInput
1012
} from './GeojsonPreviewTool.input.schema.js';
13+
import { isMcpUiEnabled } from '../../config/toolConfig.js';
1114

1215
export class GeojsonPreviewTool extends BaseTool<typeof GeojsonPreviewSchema> {
1316
name = 'geojson_preview_tool';
@@ -49,6 +52,36 @@ export class GeojsonPreviewTool extends BaseTool<typeof GeojsonPreviewSchema> {
4952
);
5053
}
5154

55+
/**
56+
* Generate a Mapbox Static Images API URL for the GeoJSON data
57+
* @see https://docs.mapbox.com/api/maps/static-images/
58+
*/
59+
private generateStaticImageUrl(geojsonData: GeoJSON): string | null {
60+
const accessToken = process.env.MAPBOX_ACCESS_TOKEN;
61+
if (!accessToken) {
62+
return null; // Fallback to geojson.io if no token available
63+
}
64+
65+
// Create a simplified GeoJSON for the overlay
66+
// The Static API requires specific format for GeoJSON overlays
67+
const geojsonString = JSON.stringify(geojsonData);
68+
const encodedGeoJSON = encodeURIComponent(geojsonString);
69+
70+
// Use Mapbox Streets style with auto-bounds fitting
71+
// Format: /styles/v1/{username}/{style_id}/static/geojson({geojson})/auto/{width}x{height}@2x
72+
const staticImageUrl =
73+
`https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/` +
74+
`geojson(${encodedGeoJSON})/auto/1000x700@2x` +
75+
`?access_token=${accessToken}`;
76+
77+
// Check if URL is too long (browsers typically limit to ~8192 chars)
78+
if (staticImageUrl.length > 8000) {
79+
return null; // Fallback to geojson.io for large GeoJSON
80+
}
81+
82+
return staticImageUrl;
83+
}
84+
5285
protected async execute(input: GeojsonPreviewInput): Promise<CallToolResult> {
5386
try {
5487
// Parse and validate JSON format
@@ -72,14 +105,61 @@ export class GeojsonPreviewTool extends BaseTool<typeof GeojsonPreviewSchema> {
72105
const encodedGeoJSON = encodeURIComponent(geojsonString);
73106
const geojsonIOUrl = `https://geojson.io/#data=data:application/json,${encodedGeoJSON}`;
74107

108+
// Build content array with URL
109+
const content: CallToolResult['content'] = [
110+
{
111+
type: 'text',
112+
text: geojsonIOUrl
113+
}
114+
];
115+
116+
// Conditionally add MCP-UI resource if enabled
117+
if (isMcpUiEnabled()) {
118+
// Create content-addressable URI using hash of GeoJSON
119+
// This enables client-side caching - same GeoJSON = same URI
120+
const contentHash = createHash('md5')
121+
.update(geojsonString)
122+
.digest('hex')
123+
.substring(0, 16); // Use first 16 chars for brevity
124+
125+
// Try to generate a Mapbox Static Image URL
126+
const staticImageUrl = this.generateStaticImageUrl(geojsonData);
127+
128+
if (staticImageUrl) {
129+
// Use Mapbox Static Images API - embeds as an image
130+
const uiResource = createUIResource({
131+
uri: `ui://mapbox/geojson-preview/${contentHash}`,
132+
content: {
133+
type: 'externalUrl',
134+
iframeUrl: staticImageUrl
135+
},
136+
encoding: 'text',
137+
uiMetadata: {
138+
'preferred-frame-size': ['1000px', '700px']
139+
}
140+
});
141+
content.push(uiResource);
142+
} else {
143+
// Fallback to geojson.io URL (for large GeoJSON or when no token)
144+
// Note: geojson.io may not work in iframes due to X-Frame-Options
145+
const uiResource = createUIResource({
146+
uri: `ui://mapbox/geojson-preview/${contentHash}`,
147+
content: {
148+
type: 'externalUrl',
149+
iframeUrl: geojsonIOUrl
150+
},
151+
encoding: 'text',
152+
uiMetadata: {
153+
'preferred-frame-size': ['1000px', '700px']
154+
}
155+
});
156+
content.push(uiResource);
157+
}
158+
}
159+
75160
return {
76161
isError: false,
77-
content: [
78-
{
79-
type: 'text',
80-
text: geojsonIOUrl
81-
}
82-
]
162+
content
83163
};
84164
} catch (error) {
85165
const errorMessage =

src/tools/preview-style-tool/PreviewStyleTool.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2+
import { createUIResource } from '@mcp-ui/server';
23
import { BaseTool } from '../BaseTool.js';
34
import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js';
45
import {
56
PreviewStyleSchema,
67
PreviewStyleInput
78
} from './PreviewStyleTool.input.schema.js';
89
import { getUserNameFromToken } from '../../utils/jwtUtils.js';
10+
import { isMcpUiEnabled } from '../../config/toolConfig.js';
911

1012
export class PreviewStyleTool extends BaseTool<typeof PreviewStyleSchema> {
1113
readonly name = 'preview_style_tool';
@@ -63,13 +65,32 @@ export class PreviewStyleTool extends BaseTool<typeof PreviewStyleSchema> {
6365

6466
const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${userName}/${input.styleId}.html?${params.toString()}${hashFragment}`;
6567

66-
return {
67-
content: [
68-
{
69-
type: 'text',
70-
text: url
68+
// Build content array with URL
69+
const content: CallToolResult['content'] = [
70+
{
71+
type: 'text',
72+
text: url
73+
}
74+
];
75+
76+
// Conditionally add MCP-UI resource if enabled
77+
if (isMcpUiEnabled()) {
78+
const uiResource = createUIResource({
79+
uri: `ui://mapbox/preview-style/${userName}/${input.styleId}`,
80+
content: {
81+
type: 'externalUrl',
82+
iframeUrl: url
83+
},
84+
encoding: 'text',
85+
uiMetadata: {
86+
'preferred-frame-size': ['1000px', '700px']
7187
}
72-
],
88+
});
89+
content.push(uiResource);
90+
}
91+
92+
return {
93+
content,
7394
isError: false
7495
};
7596
}

0 commit comments

Comments
 (0)