Skip to main content
Glama

aicesat — cross-mission altimetry

An MCP server for Claude Desktop that pulls real ICESat-2 ATL03 photons and ICESat/GLAS GLAH06 shots over a chosen area, renders them as a 3D point cloud (deck.gl), and — on a toggle — applies an ITRF2014 + epoch co-registration (plate motion, ITRF2014-PMM / NOAM) with pyproj, updating the co-located Δh statistics. Started from docs/cross-mission-altimetry-mcp-spec.md, which carries the full design and rationale.

The co-registration removes plate motion between epochs. It does not remove ice flow, GIA, geoid/tide, firn compaction, or the vertical datum, and the widget says so on every answer. GLAS heights are converted TOPEX/Poseidon → WGS84 ellipsoid using the product's d_deltaEllip, with the saturation correction d_satElevCorr applied; that is recorded in every comparability block.

Setup

uv sync          # Python 3.13
uv run pytest    # offline unit tests

Earthdata Login: fetching NASA data needs a token; browsing the existing lake does not. The server resolves a token in this order: EARTHDATA_TOKEN (env) → a token file at ~/.edl/token.prod (override the path with AICESAT_EDL_FILE) → ~/.netrc. Generate a token at https://urs.earthdata.nasa.gov (User Profile → Generate Token). The server never writes to stdout (stdio MCP transport); logs go to stderr.

Related MCP server: mcp-opentopography

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json, using the absolute path to your checkout. Put your Earthdata token in the env block — this is machine-independent (no token file needed) and the recommended way for a new user to configure the server:

{
  "mcpServers": {
    "aicesat": {
      "command": "/opt/homebrew/bin/uv",
      "args": ["--directory", "/ABSOLUTE/PATH/TO/aicesat", "run", "aicesat-server"],
      "env": { "EARTHDATA_TOKEN": "<your Earthdata Login token>" }
    }
  }
}

If you keep your token in a file instead, drop the EARTHDATA_TOKEN line and either place it at ~/.edl/token.prod or add "AICESAT_EDL_FILE": "/path/to/your/token" to env. Restart Claude Desktop. The unified UI renders inline as an MCP App; the server also serves it at http://127.0.0.1:8765/ (port via AICESAT_PORT) for use in a browser.

Getting an Earthdata Login token (step by step)

  1. Create a free Earthdata Login account if you don't have one: https://urs.earthdata.nasa.gov → Register.

  2. Sign in, then open your profile (top-right) → the Generate Token tab → Generate Token. Copy the long token string (a JWT that starts with eyJ…). Tokens last ~60 days — regenerate when one stops working.

  3. Paste it into the Claude Desktop config shown above, as the value of EARTHDATA_TOKEN in the server's env block. The config file is at ~/Library/Application Support/Claude/claude_desktop_config.json (in Claude Desktop: Settings → Developer → Edit Config).

  4. Restart Claude Desktop. The Lake view works without a token; building a scene / checking coverage will now authenticate. If it can't, the UI shows an error banner naming what to fix.

A ~/.netrc with your Earthdata username/password also works, but a token in env is the most reliable and is what we recommend for sharing the server with a colleague.

Tools: survey_coverage, elevation_change and show_timeseries (the place-first ladder: coverage, change, one cell's record), open_ui, list_regions, list_scenes, check_coverage, show_photons (region, bbox, or polygon), add_glas, coregister, find_timeseries_candidates, lake_status, lake_load_cells, job_status. By default (the demo profile) only the three ladder tools are exposed, with the demo UI (how the UWG demo was recorded). AICESAT_PROFILE=full exposes every tool above and the full UI.

Asking for a time series

find_timeseries_candidates ranks the places in an already-built scene where an elevation time series can actually be measured, and show_timeseries shows one of them. Both open the Time series view inline.

The search bins every mission's points into H3 cells and fixed time windows, keeps cells seen in three or more windows, and fits one local reference plane per cell so surface slope is removed rather than mistaken for change. Each cell gets a deterministic 0–1 confidence dominated by within-cell roughness — the real failure mode, where two missions sample different sub-cell relief and fake a trend. why names the top two limiters in plain language.

The candidate list is summarised for the caller (no per-cell series, no geometry) and reports n_candidates_total alongside what it returned, so a truncated answer still says how much it left behind; the view shows every cell. trend_cm_yr is the least-squares rate through the window medians and is uncorrected for inter-campaign / inter-sensor bias and for GIA — quote it with that caveat, as params.notes states on every answer.

UI

One self-contained page (built from src/aicesat/ui/* + vendored deck.gl / h3-js by scripts/build_ui.py, pure Python; the server rebuilds it on start when sources change), served both inline in Claude Desktop and at /:

  • Explore — a 3-D globe (Natural Earth basemap, no flat projection). Navigate anywhere, draw a box or polygon, check coverage, and build a scene as a background job; scene footprints and the loaded H3 cells are shown on the globe.

  • Lake — the persistent Parquet lake as an H3 grid: per-cell stats on hover, a storage limit that auto-evicts the least-recently-used cells, and background loading or eviction of selected cells.

  • Scene — the 3-D viewer: ICESat-2 and GLAS points draped on a DEM, an Adjustments panel of correction toggles, and the co-located Δh histograms. The true plate-motion shift is sub-pixel at scene scale, so the clouds do not visibly move — the effect is read from the Δh panel, not an exaggerated visual. Panels collapse and close.

  • Time series (#ts/<scene_id>) — the same candidate list and chart the Scene panel carries, given the whole page and no 3-D. It needs only the scene metadata and one search call, so unlike the Scene view it renders completely inside Claude Desktop, where the point cloud's push stream cannot follow (tools/call is request/response). Selecting a different cell is a local redraw: the view already holds every candidate's series.

The UI talks to a transport-neutral API (api.py) exposed two ways: the localhost /api/* routes for the browser, and visibility:["app"] MCP tools the host proxies for the inline app. scripts/e2e_apps.py checks the MCP-App wiring.

Architecture

The ATL03 path does not open HDF5 at query time. Per granule, an index build (index.py) records each chunk's byte range, filter pipeline, and the H3 cells it touches. Queries resolve a bbox/polygon to cells, fetch only the needed chunks by HTTPS range request (EDL bearer token → presigned URL), decode them without an HDF5 library, and materialize photons into a hive-partitioned Parquet lake (lake.py) with per-row provenance and co-registered coordinates. DuckDB answers over the lake (api.py); a coverage table records what is materialized so repeat queries fetch nothing. The earlier earthaccess.open + h5py path is kept as atl03.extract_legacy for comparison. scripts/bench_access.py compares the access methods; see the spec (Appendix C) for the approach.

Scene geometry is latitude-aware (scene.frame_crs): polar-stereographic near the poles, a per-scene azimuthal-equidistant projection elsewhere, so scenes render anywhere on Earth.

DEM and imagery

The scene surface is a real DEM chosen by region, always on WGS84-ellipsoid heights (the same vertical reference as ATL03), read by window over /vsicurl/ and cached: ArcticDEM v4.1 (Arctic, EPSG:3413) and REMA v2.0 (Antarctic, EPSG:3031) — both already ellipsoidal — and Copernicus GLO-30 elsewhere, whose EGM2008 orthometric heights are converted to ellipsoidal by adding the NGA egm08 geoid undulation (h = H + N). There is no photon-interpolated fallback: a scene shows a DEM only where one covers it. Scene imagery is Sentinel-2 cloudless (EOX, CC BY-NC-SA 4.0), draped on the surface. The Explore/Lake globe basemap is Natural Earth 50 m land (public domain). Attributions appear on screen.

Scripts

uv run scripts/check_coverage.py --region egig_west_flank   # granule counts by month / laser campaign
uv run scripts/ingest.py egig_west_flank                    # index + byte-range ingest + lake query
uv run scripts/build_index.py --region egig_west_flank      # offline index pre-build (amortized off the query path)
uv run scripts/make_scene.py egig_west_flank --glas --coreg # full pipeline for a region
uv run scripts/serve.py                                     # widget server only, for local testing
uv run scripts/bench_access.py                              # access-method comparison

Data (data/) is gitignored; delete it to force a re-fetch.

How Δh is measured

For each GLAS shot, the ICESat-2 surface height at the footprint centre comes from a local along-track linear fit of the signal photons within the co-location radius. A disc median is an order statistic and cannot resolve the sub-cm slope effect; the fit is continuous in position, so it can. Only the along-beam component of the plate-motion shift is observable on a single beam, and the per-pair artifact panel keeps the mm-level vertical part of the frame step separate from the slope effect. Every answer carries the unresolved list and states "plate motion applied", never "the missions agree".

Available Tools

24 tools
add_glasC

Slice 2: add ICESat/GLAS GLAH06 40 Hz shots (2003-2009 campaigns) to an existing scene, in native coordinates (ITRF2008; heights converted TOPEX/Poseidon -> WGS84 ellipsoid). Returns provenance by campaign.

Inputs: scene_id (required), time_window, max_granules.

check_coverageB

How many ATL03 (ICESat-2) and GLAH06 (ICESat/GLAS) granules touch a region, by month / laser campaign. Give either a region name (see list_regions) or an explicit bbox [W, S, E, N]. No data is fetched.

Inputs: bbox, region, glas_window, atl03_window.

coregisterB

Slice 3: run the ITRF2014 + epoch co-registration (plate motion, ITRF2014-PMM NOAM) on both missions in a scene, co-locate GLAS shots with ICESat-2 photons, and compute delta-h statistics in native and co-registered coordinates. Live pyproj on first call, cached after. Returns the comparability block. exaggeration <= 0 picks a display exaggeration automatically (~3% of scene span); it is always labelled on screen.

Inputs: scene_id (required), common_epoch, exaggeration, colocation_radius_m.

job_statusC

Status and log of a background build job (scene or cell load).

Inputs: job_id (required).

lake_load_cellsB

Materialize H3 (res 6) cells into the lake in the background (cell ids as decimal strings); returns a job id.

Inputs: cells (required), time_window, max_granules.

lake_statusA

Parquet lake summary: cells, files, rows, bytes, storage limit and usage, recent evictions.

Inputs: no named parameters.

list_regionsB

Named candidate Greenland demo regions (bbox = west, south, east, north) with validation notes.

Inputs: no named parameters.

list_scenesA

Scenes built so far (newest first) with status ready | loading | error, area, series present, and widget URL.

Inputs: no named parameters. Returns: result

open_uiC

URL of the unified UI: Explore (imagery map, draw a box or polygon on Sentinel-2 imagery, coverage check, build scenes, open the 3-D viewer) and Lake (H3 grid with per-cell stats, storage limit, background loading, eviction).

Inputs: view.

show_photonsA

Slice 1: extract real ICESat-2 ATL03 land-ice signal photons (strong beams, medium+high confidence) over an area and create a 3D scene with an imagery base layer. Area = region name, bbox [W,S,E,N], or polygon [[lon,lat],...]. Uses the H3 chunk index + byte-range reads + Parquet lake: first touch of an area fetches only the chunks it needs, later calls hit the lake. Returns the widget URL to open plus extraction/access provenance.

Inputs: bbox, region, polygon, question, +2 more.

ui_benchD

Inputs: no named parameters.

ui_coregisterD

Inputs: scene_id (required).

ui_coverageD

Inputs: bbox, polygon.

ui_extractD

Inputs: bbox, polygon, question, with_glas, +2 more.

ui_jobD

Inputs: job_id (required).

ui_jobsD

Inputs: no named parameters.

ui_lake_cellsD

Inputs: stats.

ui_lake_evictD

Inputs: cells (required).

ui_lake_loadD

Inputs: cells (required), max_granules.

ui_lake_settingsD

Inputs: max_bytes.

ui_lake_summaryD

Inputs: no named parameters.

ui_regionsD

Inputs: no named parameters.

ui_scene_partD

Inputs: scene_id (required), part, chunk, stride.

ui_scenesD

Inputs: no named parameters.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    C2/5.0

    Scored across 24 tools

    Disambiguation2/5

    The substantive tools are distinct, but the large block of ui_* tools have empty descriptions and overlapping names such as ui_job vs ui_jobs, ui_coverage vs check_coverage, and ui_lake_cells vs ui_lake_summary. An agent cannot reliably distinguish UI shortcuts from actual data operations, so several tool boundaries are unclear.

    Naming Consistency3/5

    All names use lower_snake_case and are readable, but conventions vary between verb-first names like show_photons and check_coverage, noun-style names like lake_status and job_status, and the large ui_* prefix group. The ui_* group is internally consistent but the overall set lacks a single predictable naming pattern.

    Tool Count3/5

    With 24 tools, the server sits at the heavy end of the acceptable range. Many ui_* tools are simple navigation shortcuts that could plausibly be consolidated, while the real operational core is closer to 10 tools, making the overall surface feel larger than its actual scope.

    Completeness3/5

    The core scientific workflow is covered: coverage checking, scene creation, adding GLAS data, co-registration, scene listing, lake loading, and job status. However, there are notable gaps such as no scene deletion or update, no job listing/cancellation, and no programmatic lake eviction, with the ui_* links only partially compensating for those missing operations.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers