Skip to content
Merged
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
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it's static image api and not geojson.io?

- `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
Expand Down
14 changes: 12 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions src/config/toolConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
92 changes: 86 additions & 6 deletions src/tools/geojson-preview-tool/GeojsonPreviewTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
// Licensed under the MIT License.

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 {
GeojsonPreviewSchema,
GeojsonPreviewInput
} from './GeojsonPreviewTool.input.schema.js';
import { isMcpUiEnabled } from '../../config/toolConfig.js';

export class GeojsonPreviewTool extends BaseTool<typeof GeojsonPreviewSchema> {
name = 'geojson_preview_tool';
Expand Down Expand Up @@ -49,6 +52,36 @@ export class GeojsonPreviewTool extends BaseTool<typeof GeojsonPreviewSchema> {
);
}

/**
* 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<CallToolResult> {
try {
// Parse and validate JSON format
Expand All @@ -72,14 +105,61 @@ export class GeojsonPreviewTool extends BaseTool<typeof GeojsonPreviewSchema> {
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()) {
// 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);
}
}

return {
isError: false,
content: [
{
type: 'text',
text: geojsonIOUrl
}
]
content
};
} catch (error) {
const errorMessage =
Expand Down
33 changes: 27 additions & 6 deletions src/tools/preview-style-tool/PreviewStyleTool.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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 {
PreviewStyleSchema,
PreviewStyleInput
} from './PreviewStyleTool.input.schema.js';
import { getUserNameFromToken } from '../../utils/jwtUtils.js';
import { isMcpUiEnabled } from '../../config/toolConfig.js';

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

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

return {
content: [
{
type: 'text',
text: url
// 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',
uiMetadata: {
'preferred-frame-size': ['1000px', '700px']
}
],
});
content.push(uiResource);
}

return {
content,
isError: false
};
}
Expand Down
Loading
Loading