kepler-mcp-demo
by geodaai
README.md
# kepler-mcp-demo
Drive the kepler.gl demo map from **Claude / Codex** through the
[`@kepler.gl/mcp`](https://github.com/kepler.gl/kepler.gl/tree/master/src/mcp) map
surface — mapping-only, no analysis engine, no API key.
```
claude/codex ──Streamable HTTP MCP──▶ kepler-mcp-demo daemon (node)
(or stdio spawn) │ /mcp JSON-RPC for agents
│ /ws ▲ outbound (reverse connect)
│ /files data for map.load-data
https://kepler.gl/demo ──────────┘
```
The kepler.gl demo page owns the map. It registers the `map.*` commands from
`@kepler.gl/mcp` (`getKeplerCommands(ctx)`) against its own redux store and
serves them over a WebSocket bridge. This node process is deliberately map-agnostic:
the tool catalog comes from the page's connection, so the map surface is wholly
owned by `@kepler.gl/mcp`.
## What you get
The `map.*` commands the demo page serves (the DuckDB-free subset):
| Tool (MCP name) | Command | Needs |
|---|---|---|
| `map_get_boundary` | `map.get-boundary` | current view bbox |
| `map_load_data` | `map.load-data` | load a CSV/GeoJSON/... from a URL (see `files/`) |
| `map_add_layer` | `map.add-layer` | add point/h3/hexagon/... layer |
| `map_update_layer_color` | `map.update-layer-color` | recolor a layer |
| `map_set_basemap` | `map.set-basemap` | change the basemap style |
| `map_get_dataset_context`, `map_add_time_filter`, `map_toggle_time_filter`, `map_split_view` | … | more `map.*` commands |
| `harness_status` | (built-in) | is a page connected? |
`map.create-table` / `map.add-column` / `map.save-data` need a DuckDB connector
and are intentionally not exposed by the mapping-only bridge.
## Run (daemon mode — recommended)
First install and build (and rebuild after any source change — a long-running
daemon serves stale `dist/`):
```bash
pnpm install
pnpm build
```
### Start the server
Foreground:
```bash
node dist/index.js --port 8765 --token mytoken --demo-url https://kepler.gl/demo
```
Detached (survives the terminal closing; log lands in `/tmp/kepler-mcp-demo.log`):
```bash
cd /path/to/kepler-mcp-demo && nohup node dist/index.js --port 8765 --token mytoken \
--demo-url https://kepler.gl/demo > /tmp/kepler-mcp-demo.log 2>&1 &
```
Flags:
| Flag | Default | Purpose |
|---|---|---|
| `--port` | `8765` | port for `/mcp`, `/ws` and `/files` |
| `--token` | random | shared secret — the page *and* MCP clients must present it. Pick a fixed one (`--token mytoken`) so the page URL and `claude mcp add` header stay stable across restarts. |
| `--demo-url` | `https://kepler.gl/demo` | the page the daemon tells agents/users to open — e.g. `--demo-url https://deploy-preview-3678--keplergl.netlify.app/demo` when testing a deploy preview |
Check it's up:
```bash
curl -s http://localhost:8765/ # status page
lsof -nP -i :8765 -sTCP:LISTEN # or check the listener
tail -f /tmp/kepler-mcp-demo.log # see the banner + page connect/disconnect
```
Stop it:
```bash
lsof -ti :8765 | xargs kill
```
### Start-once lifecycle
The daemon must be started **once** — not per session. Sessions (and
`claude mcp list` health checks) just connect to it; the page stays connected
across session restarts. You only need to run the start command again after a
reboot, after killing the process, or after `pnpm build`.
1. **Register the HTTP MCP server** (once; pick the token so the page can use it):
```
claude mcp add --transport http kepler-demo http://127.0.0.1:8765/mcp \
--header "x-kepler-demo-token: <yours>"
```
2. **Connect the page** (once): open `https://kepler.gl/demo` (or the local
demo-app) with `?mcp=<token>&mcpPort=8765`, or toggle **Connect to map
harness** and paste the token. It stays connected across session
restarts — nothing to redo per session.
3. **Start sessions freely.** Each session initially sees only `harness_status`;
the page's catalog arrives via a live `tools/list_changed`. Because the
daemon owns the port, parallel sessions and health checks (`claude mcp
list`) no longer collide.
> Tip: register with `-s user` (`claude mcp add -s user --transport http …`)
> so the server is visible from *any* directory, not just the one you ran
> `claude mcp add` in.
### Optional: auto-start with launchd (macOS)
To never think about starting the daemon again — it comes back after every
reboot — create `~/Library/LaunchAgents/com.kepler-mcp-demo.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.kepler-mcp-demo</string>
<key>ProgramArguments</key><array>
<string>/usr/local/bin/node</string> <!-- or `which node` -->
<string>/path/to/kepler-mcp-demo/dist/index.js</string>
<string>--port</string><string>8765</string>
<string>--token</string><string>mytoken</string>
</array>
<key>KeepAlive</key><true/>
<key>RunAtLoad</key><true/>
<key>StandardOutPath</key><string>/tmp/kepler-mcp-demo.log</string>
<key>StandardErrorPath</key><string>/tmp/kepler-mcp-demo.log</string>
</dict></plist>
```
Then: `launchctl load ~/Library/LaunchAgents/com.kepler-mcp-demo.plist`.
## Run (stdio — per-session spawn, also supported)
```bash
claude mcp add kepler-demo -- node /abs/path/to/kepler-mcp-demo/dist/index.js --port 8765 --token <yours>
```
Pass `--port`/`--token` so the page can connect to the instance the client
spawns. The session starts first and the page connects second; the bridge
pushes `tools/list_changed` so the map tools appear without a restart. Note the
single-instance rule: while one such session is open, port 8765 is taken (the
server exits with a legible message if not).
Security: the WS bridge requires the per-launch token; the static file server only
serves the `files/` directory (path-traversal blocked); the page connects out to
your localhost, so a remote Claude cannot reach your browser.
## Examples
- [`examples/random-points.prompt.md`](examples/random-points.prompt.md) — the
"generate 10,000 random points in the current map view" session.
## Verify headlessly
```bash
pnpm smoke
```
Spawns the server, connects a fake page over WS, and exercises the MCP
`tools/list` / `tools/call` round trip through the bridge.
TDQS
A4.1/5.0
Scored across 1 tool
Disambiguation5/5
With only one tool, there is no possibility of confusion or overlap. The single tool has a clear, specific purpose.
Naming Consistency5/5
The single tool name 'harness_status' is clear and there are no other names to compare against, so no inconsistency exists.
Tool Count2/5
One tool is too few for the apparent scope of a kepler.gl bridge that should serve multiple map.* commands. The minimal count severely limits the server's utility.
Completeness1/5
The server only provides a status check, while the tool description implies a broader set of map.* commands that are not exposed. Agents cannot actually interact with the kepler.gl demo page beyond checking its status.
Maintenance
ActivityMaintained
ResponsivenessNo issues