Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

jupyter-geoagent: Design Specification

Date: 2026-04-14 Repo: geojupyter/jupyter-geoagent

Problem

Geo-agent web apps require hand-authoring a layers-input.json config file, writing an index.html, and deploying to a URL. This creates friction for researchers who want to explore STAC catalog data, compose maps, and run spatial queries without writing code or managing infrastructure. The target user is someone accustomed to ArcGIS-style GIS workflows — they expect to click, not code.

Solution

A JupyterLab extension that provides a GUI-first, no-code map exploration experience powered by the same core modules as geo-agent. Users click “GeoAgent Map” in the JupyterLab launcher and get a fully interactive environment: browse STAC catalogs, add layers, style and filter data, run DuckDB queries via MCP, and export reproducible artifacts.

By living inside Jupyter, the extension sidesteps deployment friction (JupyterHub provides the URL and auth), while enabling future integration with jupyter-ai for LLM-driven workflows.

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    JupyterLab Frontend                       │
│                                                               │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────────┐  │
│  │   Catalog     │  │   MapLibre   │  │   Layer Panel     │  │
│  │   Browser     │  │   Map View   │  │   + Query Panel   │  │
│  │   (sidebar)   │  │   (center)   │  │   + Export Panel  │  │
│  └──────┬───────┘  └──────┬───────┘  └────────┬──────────┘  │
│         │                 │                    │              │
│         └────────────┬────┴────────────────────┘              │
│                      │                                        │
│              ┌───────▼────────┐                               │
│              │  ToolRegistry  │──── ToolCallRecorder           │
│              └───────┬────────┘                               │
│                      │                                        │
│         ┌────────────┼────────────┐                           │
│         │            │            │                           │
│    ┌────▼───┐  ┌─────▼────┐  ┌───▼──────┐                   │
│    │  Map   │  │ Dataset   │  │   MCP    │                   │
│    │ Tools  │  │ Catalog   │  │  Client  │                   │
│    │(local) │  │  (STAC)   │  │          │                   │
│    └────────┘  └──────────┘  └───┬──────┘                   │
│                                   │                           │
└───────────────────────────────────┼───────────────────────────┘
                                    │
                    ┌───────────────┼───────────────┐
                    │               │               │
              ┌─────▼─────┐  ┌─────▼─────┐  ┌─────▼─────┐
              │  Remote    │  │  Jupyter   │  │  Local    │
              │  MCP       │  │  Server    │  │  MCP      │
              │  Server    │  │  Proxy     │  │  Server   │
              └───────────┘  └───────────┘  └───────────┘

Geo-agent Module Reuse

The extension uses geo-agent as a peer — sharing the same MCP server and STAC catalog, but implementing its own catalog browsing logic locally rather than re-using geo-agent’s DatasetCatalog.

ModuleStatusNotes
MCPClientUsedWrapped in src/core/mcp.ts; may route through server proxy
createMapTools()UsedTool metadata extraction for query/filter UI
MapManager (via MapView.tsx)UsedWrapped in React component
ToolRegistryUsedPlus ToolCallRecorder hook
DatasetCatalogNot usedReplaced by src/core/mcp-catalog.ts — pure functions that fetch from the STAC root and call MCP get_collection directly
Agent / ChatUINot usedLLM loop delegated to jupyter-ai Claude persona

MapLayerConfig and ColumnInfo are defined locally in src/core/types.ts rather than re-exported from geo-agent, so the extension can evolve its type surface independently.

User Experience

Entry Point

User clicks “GeoAgent Map” in the JupyterLab launcher (or File > New > GeoAgent Map). A new main-area panel opens.

Panel Layout

Left sidebar — Catalog Browser:

Center — Map:

Right sidebar — Tabbed Panel:

Layers tab:

Query tab:

Export tab:

No-Code Guarantee

Every interaction is click-driven. The user never sees Python, JavaScript, or JSON unless they choose to export it. The notebook is not involved.

Tool Call Recording

Every GUI action maps to a named tool call, identical to what the LLM would produce in a geo-agent web app. A ToolCallRecorder wraps the ToolRegistry and intercepts every execute() call:

interface RecordedToolCall {
  id: number;           // sequential
  tool: string;         // tool name (e.g. "show_layer")
  args: object;         // tool arguments
  result?: any;         // tool return value (optional, for queries)
  timestamp: string;    // ISO 8601
}

The recorder is append-only during a session. The export tab exposes it in two formats:

  1. Tool call log (JSON) — array of RecordedToolCall, directly replayable

  2. layers-input.json — snapshot of current map state (catalog URL, collections, per-layer visibility/style/filter, view position), which captures the end state rather than the journey

The tool call log is the “reproducible notebook” equivalent for this GUI — it captures exactly what was done, in order, with arguments.

MCP Integration

Remote MCP (default)

The frontend MCPClient connects directly to a remote MCP server URL (e.g. https://duckdb-mcp.nrp-nautilus.io/mcp), same as geo-agent web apps.

Server Proxy

For JupyterHub environments that restrict outbound browser connections, the server extension exposes a proxy endpoint:

POST /jupyter-geoagent/mcp-proxy
Body: { "server_url": "https://...", "method": "tools/call", "params": {...} }

The frontend detects connectivity and falls back to the proxy automatically.

Local MCP

The server extension can optionally manage a local DuckDB MCP server process for querying the user’s own data. Configuration via JupyterLab settings or environment variables.

jupyter-ai Command Bridge

The LLM chat panel drives the map through a short chain of pre-existing JupyterLab machinery:

 jupyter-ai persona (Claude / OpenCode / Goose)
   │  (MCP tool call: execute_command)
   ▼
 jupyter_server_mcp  ──── discovers tools via the
                          `jupyter_server_mcp.tools` entrypoint
   │
   ▼
 jupyterlab_commands_toolkit  ── emits a `jupyterlab-command/v1` event
   │
   ▼
 jupyterlab-eventlistener (browser)
   │
   ▼
 app.commands.execute('geoagent:<tool>', args)
   │
   ▼
 src/commands.ts handler
   │  constructs MapManagerAdapter(controller, {onChange: refresh})
   │  calls geo-agent's createMapTools(adapter, stubCatalog, mcpClient)
   ▼
 MapViewController  → map mutations  + ToolCallRecorder entry

Key pieces:

What this enables: zero prompt-engineering per app. Any jupyter-ai persona with MCP access sees the commands via list_all_commands and can drive the map directly. Tools added upstream in boettiger-lab/geo-agent appear automatically after a jlpm install + rebuild — no per-command wiring in jupyter-geoagent.

Tools currently skipped from the createMapTools-derived set (in SKIP_TOOLS): list_datasets and get_schema (require geo-agent’s sync DatasetCatalog; jupyter-geoagent uses an MCP-backed catalog instead, and the LLM can reach MCP catalog tools directly), and set_projection (no globe/mercator toggle in MapViewController yet).

Server Extension

Lightweight Python package (jupyter_geoagent) registered as a Jupyter server extension:

No custom document type, no yjs/CRDT, no collaboration features in v1.

Package Structure

jupyter-geoagent/
├── package.json              # TypeScript deps, build scripts, JupyterLab extension metadata
├── pyproject.toml            # Python package + server extension + build config
├── tsconfig.json
├── webpack.config.js         # or a JupyterLab federated extension setup
├── README.md
├── LICENSE
│
├── src/                      # TypeScript frontend (JupyterLab extension)
│   ├── index.ts              # Plugin registration (launcher, commands, panels)
│   ├── panel.ts              # Main GeoAgent panel (Lumino MainAreaWidget)
│   ├── components/           # React components
│   │   ├── MapView.tsx       # MapLibre GL JS wrapper
│   │   ├── CatalogBrowser.tsx
│   │   ├── LayerPanel.tsx
│   │   ├── QueryPanel.tsx
│   │   └── ExportPanel.tsx
│   ├── core/                 # Wrappers around geo-agent modules
│   │   ├── types.ts          # TypeScript interfaces for geo-agent module APIs
│   │   ├── catalog.ts        # DatasetCatalog wrapper
│   │   ├── map.ts            # MapManager wrapper
│   │   ├── tools.ts          # ToolRegistry + ToolCallRecorder
│   │   └── mcp.ts            # MCPClient wrapper (with proxy fallback)
│   └── style/
│       └── index.css
│
├── jupyter_geoagent/         # Python server extension
│   ├── __init__.py           # Extension registration
│   ├── handlers.py           # MCP proxy handler
│   └── config.py             # Configurable traits
│
├── style/                    # JupyterLab CSS integration
│   └── base.css
│
└── docs/
    └── design.md             # This file

Configuration

JupyterLab settings schema (schema/plugin.json):

{
  "jupyter-geoagent:settings": {
    "type": "object",
    "properties": {
      "defaultCatalogUrl": {
        "type": "string",
        "default": "https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json",
        "description": "Default STAC catalog URL loaded when opening a new map"
      },
      "defaultTitilerUrl": {
        "type": "string",
        "default": "https://titiler.nrp-nautilus.io",
        "description": "Default TiTiler endpoint for COG rendering"
      },
      "mcpServers": {
        "type": "array",
        "default": [
          {"name": "NRP DuckDB", "url": "https://duckdb-mcp.nrp-nautilus.io/mcp", "type": "remote"}
        ],
        "description": "Available MCP servers"
      },
      "defaultBasemap": {
        "type": "string",
        "enum": ["natgeo", "satellite", "plain"],
        "default": "natgeo"
      },
      "useProxy": {
        "type": "string",
        "enum": ["auto", "always", "never"],
        "default": "auto",
        "description": "Whether to route MCP requests through the server proxy"
      }
    }
  }
}

Export Formats

Static HTML Map

A self-contained HTML file that can be opened in any browser:

layers-input.json

The standard geo-agent configuration format. A user can take this file, pair it with the geo-agent-template, and deploy a full geo-agent web app with LLM chat.

Standalone App (layers-input.json + index.html)

The Export Standalone App button downloads both files together. index.html loads geo-agent from the jsDelivr CDN (cdn.jsdelivr.net/gh/boettiger-lab/geo-agent@main/app/main.js), which reads layers-input.json at startup and renders the full map with all configured layers. Place both files in the same directory, serve over HTTP, and the app works without any additional setup.

Tool Call Log (JSON)

{
  "version": "1.0",
  "catalog": "https://...",
  "created": "2026-04-14T...",
  "calls": [
    {"id": 1, "tool": "show_layer", "args": {"layer_id": "cpad-holdings"}, "timestamp": "..."},
    {"id": 2, "tool": "set_filter", "args": {"layer_id": "cpad-holdings", "filter": ["==", ["get", "MNG_AGENCY"], "State Parks"]}, "timestamp": "..."},
    {"id": 3, "tool": "query", "args": {"sql": "SELECT MNG_AGENCY, SUM(GIS_ACRES) FROM ... GROUP BY 1"}, "result": "...", "timestamp": "..."}
  ]
}

jupyter-ai Integration

jupyter-geoagent ships with jupyter-ai v3 pre-installed. The Claude persona (powered by claude-agent-acp) runs Claude Code as an ACP subprocess with access to:

The chat panel lives in the JupyterLab sidebar. Users can ask natural-language questions about catalog data; the agent can write query results directly into notebooks.

Limitation: the Claude persona cannot yet drive the GeoAgent map panel (add layers, set styles, etc.) — it has no access to MapViewController. Wiring that up would require exposing map tools via jupyter-server-mcp.

Future Work

Technology Stack