A reusable JavaScript library for interactive map applications with LLM-powered data analysis. MapLibre GL JS on the front end, agentic tool-use with MCP (Model Context Protocol) for SQL analytics via DuckDB.
This repo is the core library. Individual apps (different datasets, URLs, branding) import these modules from the CDN and provide their own configuration.
Use boettiger-lab/geo-agent-template — click "Use this template" on GitHub. You get a ready-to-deploy repo with index.html, layers-input.json, system-prompt.md, and k8s/ manifests pre-wired. Edit those four files for your dataset and you're done.
Live demo: https://boettiger-lab.github.io/geo-agent-template/
Client apps load the core modules directly from jsdelivr:
<!-- Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/boettiger-lab/geo-agent@v1.0.0/app/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/boettiger-lab/geo-agent@v1.0.0/app/chat.css">
<!-- App (all modules resolve from CDN via relative imports) -->
<script type="module" src="https://cdn.jsdelivr.net/gh/boettiger-lab/geo-agent@v1.0.0/app/main.js"></script>| CDN reference | Behavior |
|---|---|
@v1.0.0 |
Pinned — immutable, use for production |
@main |
Tracks latest commit — use for staging/dev |
Releasing: tag a commit on main → git tag v1.1.0 && git push --tags → all apps on @main get it immediately; production apps upgrade by changing their tag.
┌───────────────────────────────┐
│ This repo (core library) │
│ served via CDN │
└──────────┬────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌─────▼──────┐ ┌──────▼─────┐ ┌───────▼──────┐
│ App 1 │ │ App 2 │ │ App 3 │
│ ca-lands │ │ wetlands │ │ fire-risk │
│ │ │ │ │ │
│ index.html │ │ index.html │ │ index.html │
│ config │ │ config │ │ config │
│ k8s/ │ │ k8s/ │ │ k8s/ │
└────────────┘ └────────────┘ └──────────────┘
Each client app is a tiny repo (~4–5 files) that provides:
index.html— loads core JS/CSS from CDN, sets page titlelayers-input.json— STAC collections, assets, and (optionally) LLM configsystem-prompt.md— LLM personality and guidelinesk8s/— Kubernetes deployment manifests (hostname, replicas, secrets) — optional
| File | Responsibility |
|---|---|
main.js |
Bootstrap — wires all modules together |
dataset-catalog.js |
Loads STAC collections, builds unified dataset records |
map-manager.js |
Creates MapLibre map, manages layers/filters/styles |
map-tools.js |
9 local tools the LLM can call (show/hide/filter/style + dataset info) |
tool-registry.js |
Unified registry for local + remote (MCP) tools, single dispatch |
mcp-client.js |
MCP transport wrapper — connect, lazy reconnect, callTool |
agent.js |
LLM orchestration loop — agentic while-loop with tool-use |
chat-ui.js |
Chat UI with collapsible tool-call blocks |
See docs/agent-loop.md for a detailed walkthrough of the agentic loop, how pre-call explanations are generated, and options for extending the approval flow.
- STAC catalog is the single source of truth for dataset metadata
- Each collection provides visual assets (PMTiles/COG for map) and parquet assets (H3-indexed for SQL)
- The agent can query parquet via the MCP SQL tool and control the map via local tools
Client apps provide layers-input.json. All fields except collections are optional.
{
"catalog": "https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json",
"titiler_url": "https://titiler.nrp-nautilus.io",
"mcp_url": "https://duckdb-mcp.nrp-nautilus.io/mcp",
"view": { "center": [-119.4, 36.8], "zoom": 6 },
"llm": {
"user_provided": true,
"default_endpoint": "https://openrouter.ai/api/v1",
"models": [
{ "value": "anthropic/claude-sonnet-4", "label": "Claude Sonnet" },
{ "value": "google/gemini-2.5-flash", "label": "Gemini Flash" }
]
},
"collections": [
{
"collection_id": "cpad-2025b",
"assets": ["cpad-holdings-pmtiles", "cpad-units-pmtiles"]
},
{
"collection_id": "irrecoverable-carbon",
"assets": [
{ "id": "irrecoverable-total-2018-cog", "display_name": "Irrecoverable Carbon (2018)" },
{ "id": "manageable-total-2018-cog", "display_name": "Manageable Carbon (2018)" }
]
}
]
}- String collection entries load all visual assets
- Object entries with
assetscherry-pick specific STAC asset IDs for map layers - Asset filtering only affects map toggles — all parquet/H3 data remains available for SQL
There are two ways to supply model credentials:
Server-provided (Kubernetes / managed deployments): omit the llm section from layers-input.json and provide a config.json on the same server with llm_models + API keys. The k8s/ manifests in the template repo inject secrets this way at deploy time.
User-provided (static sites — GitHub Pages, Netlify, etc.): set "user_provided": true in the llm section. A ⚙ button appears in the chat footer; visitors enter their own API key (stored in localStorage, never sent to the hosting server). The default_endpoint is pre-filled — OpenRouter is a good default giving access to many models via a single key.
cd app && python -m http.server 8000The app/ directory includes its own index.html, layers-input.json, and system-prompt.md for local development. Changes here are what client apps consume via CDN.
- Push to
main— staging apps (using@main) pick up changes on next load - Test on staging
- Tag a release:
git tag v1.1.0 && git push --tags - Update production apps' importmap to
@v1.1.0
| Option | How secrets are handled |
|---|---|
| GitHub Pages (or any static host) | User enters their own API key in-browser |
| Hugging Face Spaces | Mount a config.json as a Space secret-file |
| Kubernetes | Secrets injected into config.json via ConfigMap + init container |
See the geo-agent-template repo and deployment docs for full details.