Skip to main content
Glama
geodaai
by geodaai

kepler-mcp-app

A demo of building an MCP App for kepler.gl — the new MCP Apps extension where a server returns an interactive HTML interface that the host (Claude Desktop / Claude.ai) renders in a sandboxed iframe inside the chat.

This is distinct from:

  • WebMCP (native document.modelContext registration), and

  • the existing kepler-mcp-demo (~/github/kepler-mcp-demo), where the map renders on the kepler.gl website and a node daemon routes MCP tool calls to the page over a WebSocket bridge.

Here, the kepler.gl map renders inside the MCP App iframe, and the map.* commands from @kepler.gl/mcp execute inside that iframe against the iframe's own kepler.gl redux store. No WebSocket bridge, no external website.

Architecture

Claude Desktop / Claude.ai (host)
  │  MCP (Streamable HTTP, e.g. via cloudflared tunnel)
  ▼
kepler-mcp-app server (node, port 3001)
  │  • serves ui://kepler-map/mcp-app.html  (bundled kepler.gl app, single HTML file)
  │  • registers kepler_show_map tool with _meta.ui.resourceUri → the UI resource
  ▼
MCP App iframe (sandboxed, rendered by host inside the chat)
  ├── kepler.gl map (React 19 + redux store, @kepler.gl/components KeplerGl)
  ├── App instance (@modelcontextprotocol/ext-apps)
  └── registers every map.* command from @kepler.gl/mcp as an app tool
      (map_load_data, map_add_layer, map_set_basemap, …) + kepler_get_map_skill

Flow: the agent calls kepler_show_map → the host fetches the UI resource and renders the kepler.gl app in a sandboxed iframe → the iframe boots kepler.gl, builds a KeplerContext from its own redux store, and registers the map.* commands via App.registerTool() → the agent can then call map_load_data / map_add_layer / … and the host routes each call to the iframe, which executes it against its redux store and returns the result. The app also calls app.updateModelContext() to tell the model the map is ready.

This is the same pattern as the official map-server example (CesiumJS globe) in modelcontextprotocol/ext-apps: the server registers the launch tool + UI resource, and the app registers the interactive tools.

Related MCP server: Felt MCP

Repo structure

kepler-mcp-app/
├── package.json            # deps: @kepler.gl/*, @modelcontextprotocol/ext-apps, react, redux, vite
├── tsconfig.json           # app + server typecheck
├── vite.config.ts          # vite + vite-plugin-singlefile → single mcp-app.html
├── server.ts               # MCP server: kepler_show_map tool + ui:// resource (with CSP)
├── main.ts                 # Streamable HTTP + stdio entry
├── mcp-app.html            # kepler.gl app HTML entry
├── src/
│   ├── mcp-app.tsx         # embeds <KeplerGl>, creates App, registers map.* tools
│   ├── kepler-store.ts     # redux store (keplerGlReducer + thunk)
│   ├── kepler-context.ts   # buildKeplerContext(store) — live accessors into the iframe's store
│   └── tool-utils.ts       # toMcpToolName, resultToText, toCallToolResult, skill tool
├── vendor/@kepler.gl/mcp/  # vendored @kepler.gl/mcp source (from PR #3678) + package.json
├── files/                  # sample data (random_points.csv) served at /files/*
└── README.md

Run

npm install
npm run build     # vite → dist/mcp-app.html; esbuild → dist/index.js
npm start         # node dist/index.js → http://localhost:3001/mcp

npm run dev rebuilds the app on change and restarts the server.

Test

Server smoke test:

curl -X POST http://localhost:3001/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

tools/list should return kepler_show_map with _meta.ui.resourceUri, and resources/read of ui://kepler-map/mcp-app.html should return the bundled HTML.

basic-host (from modelcontextprotocol/ext-apps):

SERVERS='["http://localhost:3001/mcp"]' npm start   # in examples/basic-host

Call kepler_show_map, confirm the kepler.gl map renders in the sandboxed iframe, then call map_load_data (data URL) + map_add_layer and confirm the map updates.

Claude Desktop:

npx cloudflared tunnel --url http://localhost:3001

Add the tunnel URL as a custom connector, then ask it to "show me a map of these points" and confirm the map renders in-chat and the map.* tools drive it.

Loading data

The sandboxed iframe cannot fetch localhost, so load local files as data URLs (≤2 MB):

data:text/csv;base64,<base64>

See files/README.md for a worked example. The server also serves files/ at /files/* with CORS for hosts whose iframe can reach it.

Notes

  • Basemap: kepler.gl 3.3.0-alpha.10 defaults to MapLibre with CARTO styles (dark-matter / positron / voyager) — no Mapbox token needed. The UI resource's CSP allows basemaps.cartocdn.com and *.basemaps.cartocdn.com. The wildcard matters: the CARTO vector TileJSON advertises its tiles on sharded hostnames (tiles-a…d.basemaps.cartocdn.com), so allowing only tiles.basemaps.cartocdn.com silently blocks the tile fetches and the map stays black (the background layer still paints, which looks like a working dark map). data: is also allowed in connect-src so map_load_data can fetch data URLs.

  • KeplerGl sizing: the <KeplerGl> component's width/height props must be pixel numbers — the root div gets inline width: ${width}px. Passing "100%" produces invalid CSS, the div collapses to height 0, and kepler's ResizeObserver then reports a zero size forever. src/mcp-app.tsx tracks the iframe viewport (window.innerWidth/Height + a resize listener) instead.

  • DuckDB commands: map.create-table, map.add-column, and map.save-data need a DuckDB connector and are filtered out of the catalog. The skill tool (kepler_get_map_skill) notes this.

  • @kepler.gl/mcp is not on npm yet; it is vendored from PR #3678 into vendor/@kepler.gl/mcp/ and depended on via file:.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    B
    maintenance
    Enables AI agents to become geospatially intelligent assistants with tools for location search, smart routing, round trip planning, reverse geocoding, isochrone analysis, route visualization, geofence management, and interactive map display.
    8
    29
    7
    Apache 2.0
  • A
    license
    A
    quality
    D
    maintenance
    Enables to interact with Felt maps programmatically through an MCP server. Supports listing, retrieving details, and creating maps with natural language.
    4
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to perceive and control a live MapLibre GL map over MCP, allowing querying rendered features, reading popups, navigating, and toggling layers.
    1
    MIT
  • F
    license
    A
    quality
    B
    maintenance
    MCP server that lets agents create, display, and export rich UI widgets (cards, dashboards, charts, forms) inline in conversations, with interactive iframe support in MCP Apps hosts and PNG image fallback for other clients.
    3
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/geodaai/kepler-mcp-app'

If you have feedback or need assistance with the MCP directory API, please join our Discord server