Skip to main content
Glama
geodaai
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