kepler-mcp-app
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.modelContextregistration), andthe 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_skillFlow: 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.
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.mdRun
npm install
npm run build # vite → dist/mcp-app.html; esbuild → dist/index.js
npm start # node dist/index.js → http://localhost:3001/mcpnpm 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-hostCall 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:3001Add 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 allowsbasemaps.cartocdn.comand*.basemaps.cartocdn.com. The wildcard matters: the CARTO vector TileJSON advertises its tiles on sharded hostnames (tiles-a…d.basemaps.cartocdn.com), so allowing onlytiles.basemaps.cartocdn.comsilently blocks the tile fetches and the map stays black (thebackgroundlayer still paints, which looks like a working dark map).data:is also allowed inconnect-srcsomap_load_datacan fetch data URLs.KeplerGl sizing: the
<KeplerGl>component'swidth/heightprops must be pixel numbers — the root div gets inlinewidth: ${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.tsxtracks the iframe viewport (window.innerWidth/Height+ aresizelistener) instead.DuckDB commands:
map.create-table,map.add-column, andmap.save-dataneed a DuckDB connector and are filtered out of the catalog. The skill tool (kepler_get_map_skill) notes this.@kepler.gl/mcpis not on npm yet; it is vendored from PR #3678 intovendor/@kepler.gl/mcp/and depended on viafile:.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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