About elevation-mcp-server
elevation-mcp-server provides worldwide elevation lookup and terrain analysis for route planning, area assessment, and line-of-sight checks. It draws on USGS 3DEP and Open Topo Data, reporting dataset and resolution information with results.
Key features
- 01Look up ground height at coordinates
- 02Analyze route ascent, descent, and grades
- 03Summarize elevation grids over an area
- 04Check whether terrain blocks a sightline
- 05Assess radio Fresnel-zone clearance
Use cases
- 01Estimate elevation gain and grades on a route
- 02Compare the highest and lowest points in an area
- 03Check terrain visibility between two locations
MCP support
As an MCP server, it exposes four tools for elevation lookups, route and grid analysis, and terrain line-of-sight checks. It supports STDIO and Streamable HTTP.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://elevation.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-elevation https://elevation.caseyjhand.com/mcp{"mcpServers":{"cyanheads-elevation":{"type":"http","url":"https://elevation.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-elevation":{"type":"http","url":"https://elevation.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-elevation":{"type":"http","url":"https://elevation.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-elevation":{"serverUrl":"https://elevation.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
4| Parameter | Type | Required | Description |
|---|---|---|---|
| points | array | Yes | 1–100 points as {lat, lon} objects in decimal degrees (WGS84). latitude, longitude, and lng keys are also accepted. |
| source | enum | No | Elevation source: auto (default) uses USGS 3DEP where it has data and Open Topo Data (SRTM, with Mapzen terrain tiles where SRTM has no data) for the rest; usgs_3dep uses 3DEP only; opentopodata uses Open Topo Data only. Case is ignored and spaces or hyphens read as underscores, so USGS-3DEP and open topo data are accepted; 3dep, usgs, and epqs are also aliases for usgs_3dep.auto | usgs_3dep | opentopodata |
Details
From the project README@cyanheads/elevation-mcp-server
Look up elevation worldwide, profile route ascent/descent, grid areas, check terrain line of sight via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://elevation.caseyjhand.com/mcp
Overview
Elevation and terrain analysis anywhere on Earth, from USGS 3DEP (1–30 m across the US) and Open Topo Data worldwide. Look up spot heights, measure a route's climbs and grades, find an area's high and low points, and check whether terrain blocks a line of sight. No API key required.
Tools
| Tool | Description |
|---|---|
elevation_get_points | Ground elevation at 1–100 coordinates, in meters and feet, with the dataset and resolution behind each value |
elevation_get_profile | Sample a route at evenly spaced points and summarize distance, ascent, descent, elevation range, and steepest grades |
elevation_get_grid | Sample a node grid over a bounding box and report its highest and lowest points, mean elevation, and relief |
elevation_check_line_of_sight | Decide whether terrain blocks the sightline between two points, with earth curvature and refraction, and check first Fresnel zone clearance for radio links |
Capability reference
Every successful result includes vertical_reference on both response surfaces. Public SRTM uses EGM96; NAVD88 is typical for 3DEP in the conterminous US, but the returned raster reference is unverified and can vary elsewhere. Mapzen and self-hosted Open Topo Data references are unknown/variable. No-data-only results establish no height reference. Heights are returned without transformation; mixed datasets are not normalized to a common datum. water_surface_m needs a compatible terrain reference; zero or source: opentopodata alone does not establish compatibility.
elevation_get_points tool
points: 1–1,000{lat, lon}objects in decimal degrees (WGS84), or a single point. Only the first 100 are validated and sampled; the remainder stays unvalidatedrequested_points,processed_points, andremaining_pointscount submitted positions, including duplicates and misses.next_offsetis the index after this call's processed prefix, ornullwhen exhausted; continue withoriginalPoints.slice(next_offset), whereoriginalPointsis the array submitted in that call. Use each newly submitted array for the next slice- Each point comes back
okorno_data(a miss never fails the call), withelevation_m/elevation_ft,dataset, andresolution_m(omitted formapzen); 3DEP answers addraster_idandacquisition_date.points_with_datacounts the hits
elevation_get_profile tool
path: 2–1,000 vertices in travel order (consecutive duplicates dropped);samples: 2–250 evenly spaced points along it, endpoints included (default 100)summarycarriestotal_distance_m,ascent_m/descent_m(and feet), start, end, min, and max elevation,highest_point/lowest_point, andmax_grade_pct/min_grade_pctwith where each occurs; ascent depends on the reportedsample_interval_m. Fails asdegenerate_path(under 1 m of route),antipodal_path(add an intermediate waypoint between the identified zero-based input vertices), orno_coverage(no sample has data)include_samples(defaulttrue) returnssamples[](distance, position, elevation, grade, dataset, and resolution per sample);falseomits it and the sample table, and the rest of the result is unchanged
elevation_get_grid tool
south,west,north,eastedges in decimal degrees (a box can't cross longitude 180);rowsandcols2–25 each (default 10), withrows × colsat most 250elevations_mandcell_datasetsmatrices indexed[row][col](row 0 north, column 0 west,nullwithout data), plussummary.highest,summary.lowest,mean_elevation_m, andrelief_m. Fails asinvalid_bbox,too_many_cells, orno_coverage- Nodes are point samples with the edges included, not cell averages
elevation_check_line_of_sight tool
observerandtargetpoints, at most 1,000 km apart;observer_height_m(default 1.7) andtarget_height_m(default 0), each 0–10,000 m above ground;earth_modelflat,geometric,optical(default, κ 0.13), orradio(κ 0.25);samples3–250 (default 100); optionalwater_surface_m(-500 to 9,000) andfrequency_mhz(30–300,000)verdictisclear,blocked, orindeterminate(samples without data leave the line unconfirmed), withmin_clearance_m/min_clearance_ft, thelimiting_point, and thefirst_obstructionwhen blocked. Fails assame_endpoints(under 1 m apart),sightline_too_long(over 1,000 km apart), orendpoint_no_datafrequency_mhzaddsfresnel: asufficient,insufficient, orindeterminateverdict against the 60% free-space bar of the first Fresnel zone,min_clearance_ratio(clearance over zone radius), and the sample that limits it, which is often notlimiting_point- Terrain only: buildings and vegetation count only as far as the elevation source captures them. Where Mapzen reports sea-floor depth, clearance is measured to the sea surface; USGS 3DEP values below 0 m (bay floor in some bays, or dry land) count as received unless
water_surface_msets a water level, which then applies to every sample
Failures shared by every tool
| Reason | Code | When |
|---|---|---|
usgs_unavailable | ServiceUnavailable | USGS 3DEP did not answer or rejected the request; data.retryable says whether retrying can help |
opentopodata_unavailable | ServiceUnavailable | Open Topo Data did not answer, rejected the request, or sent an unusable response; data.retryable as above |
opentopodata_rate_limited | RateLimited | Open Topo Data kept answering HTTP 429, or asked for a wait over 8 s; data.retryAfter in seconds when it sent one of at most a day |
opentopodata_daily_limit | RateLimited | Public instance only: this server already sent 1,000 requests in the trailing 24 hours, so none was sent; data.retryAfter is the seconds until a slot frees |
opentopodata_config_rejected | ConfigurationError | The instance at OPENTOPODATA_BASE_URL answered 401, 403, or 404, redirected (self-hosted only; never followed), lacks srtm30m or mapzen, caps locations below 100, or answered from a dataset this server didn't request; the operator must fix it |
sampling_deadline_exceeded | Timeout | The 45 s sampling budget ran out (data.provider names the provider it was waiting on), or other calls' queued USGS 3DEP lookups left no time for this call's, so it sent none and data.retryAfter is the seconds until they drain |
A coverage miss is never an error: that point has no data. Any provider failure fails the whole call with no partial result.
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Elevation-specific:
sourceon every tool:auto(default) queries USGS 3DEP inside its coverage and sends 3DEP misses and everything outside it to Open Topo Data;usgs_3depandopentopodatapin one provider. Only a coverage miss falls back, never an outage- Open Topo Data answers from SRTM GL1 v3 (about 30 m, land between 60°N and 56°S), then Mapzen terrain tiles where SRTM has no tile: high latitudes, Antarctica, and ocean bathymetry
- Per-call caps of 100 points or 250 samples or grid cells, inside a 45 s sampling budget; each 3DEP point is its own request (6 concurrent, 10 per second), while Open Topo Data takes 100 points per request
- The public Open Topo Data instance is paced under its published limits: one request at a time, starts at least 1.1 s apart, at most 1,000 in any trailing 24 hours per server process
- Profiles, grids, and sightlines are computed locally from point samples, with great-circle distances and resampling and earth curvature with standard refraction
Agent-friendly output:
- Provenance on every value: each point, profile sample, grid cell, and sightline point names its
dataset(usgs_3dep,srtm30m,mapzen) and, except for Mapzen, itsresolution_m(resolution_m_rangeon profiles and grids); computed results adddatasets_usedcounts, and aSources:line credits every dataset that answered - Absent stays absent: a point or sample without data omits its elevation and a grid cell is
null, never 0; summaries report how many values had data - Notices flag what changes interpretation: results mixing 3DEP and Open Topo Data, Mapzen values below 0 m (sea-floor depths), 3DEP values below 0 m on a sightline, sample spacing coarser or finer than the source, thin clearance margins, and a clear radio path short of 60% Fresnel clearance
Getting started
Public Hosted Instance
A public instance is available at https://elevation.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"elevation-mcp-server": {
"type": "streamable-http",
"url": "https://elevation.caseyjhand.com/mcp"
}
}
}
Every caller of the hosted instance shares one Open Topo Data allowance of 1,000 requests a day. Under the default auto source it goes to the points USGS 3DEP doesn't answer; 3DEP covers the US and its territories, plus much of Canada and Mexico. For sustained use, run your own instance.
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"elevation-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/elevation-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"elevation-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/elevation-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"elevation-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/elevation-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No API key: USGS 3DEP and Open Topo Data are both keyless.
- Optional: a self-hosted Open Topo Data instance with the
srtm30mandmapzendatasets, for heavy or hosted use (see Configuration).
Installation
- Clone the repository:
git clone https://github.com/cyanheads/elevation-mcp-server.git
- Navigate into the directory:
cd elevation-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# optionally set OPENTOPODATA_BASE_URL to a self-hosted Open Topo Data instance
Configuration
| Variable | Description | Default |
|---|---|---|
OPENTOPODATA_BASE_URL | Open Topo Data instance used outside USGS 3DEP coverage, as an http or https URL. Unset or blank means the public instance; any other URL is treated as a self-hosted instance. | https://api.opentopodata.org |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. .env.example and the Docker image set stateless. | auto |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. Under jwt or oauth, each tool requires the scope tool:<tool_name>:read. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOGS_DIR | Directory for log files (Node.js only). | <app-root>/logs |
OTEL_ENABLED | Enable OpenTelemetry. | false |
Any OPENTOPODATA_BASE_URL other than the public instance is treated as self-hosted (Open Topo Data is MIT-licensed and runs in Docker): 4 concurrent requests, no rate windows. It must serve datasets named srtm30m and mapzen and accept at least 100 locations per request, or calls fail with opentopodata_config_rejected.
See .env.example for every server setting and the common framework overrides.
Known limitations
- Open Topo Data's public limits. The public instance allows 100 locations per request, 1 request per second, and 1,000 requests per day per IP address. Each server process paces itself under them, but its count is process-local and resets on restart. Several processes on one machine (a stdio server per client session), or other clients on the same address, also draw on the upstream's count, so the upstream can refuse first (
opentopodata_rate_limited). - Shared deployments. A deployment serving many users sends all their requests from one address, so on the public instance they share one 1,000-a-day allowance: about 1,000 point lookups, or 333 full 250-sample calls, and as few as 111 when retries spend requests too. Once it is spent, every call that needs Open Topo Data fails with
opentopodata_daily_limitfor up to 24 hours, including anautocall with a single 3DEP miss, so global answers on such a deployment are best-effort; pointingOPENTOPODATA_BASE_URLat its own instance (loaded withsrtm30mandmapzen) removes the cap. The server has no caller identity to ration by, so one caller's heavy calls slow USGS 3DEP and can spend the public day for everyone. - Global resolution. SRTM is a 30 m radar surface model. Mapzen's 1 arc-second grid is interpolated in places from coarser sources (GMTED2010 at 7.5 arc-seconds over parts of the high latitudes, ETOPO1 at about 1.8 km over the open ocean). Outside 3DEP coverage, profile ascent is underestimated and line of sight misses terrain narrower than the source's true resolution.
- Whole-meter values. Open Topo Data's datasets are integer rasters, and the upstream rounds the interpolated result to the nearest meter, so ascent and descent over gentle terrain include 1 m quantization steps.
- Water. SRTM reports water inside a land tile as 0 m. Beyond SRTM's tiles, Mapzen reports sea-floor depth, and points, profiles, and grids include it. 3DEP values over water depend on the raster: a hydro-flattened surface, sea level, or bathymetry. Line of sight measures clearance to 0 m over Mapzen values below 0, but uses 3DEP values below 0 m as received, since nothing in a USGS answer tells bay floor from dry land below sea level; its notice flags them, and
water_surface_mmeasures a line over water to the water instead. - Mixed models. 3DEP is a bare-earth DEM, typically NAVD88 in the conterminous US; the exact returned raster reference is unverified and can vary elsewhere. Public SRTM is a radar surface model that partly includes canopy and buildings, using EGM96. Mapzen blends regional sources; self-hosted dataset names do not verify the reference. Heights are not transformed to a common datum, and datum alone does not explain differences between providers.
- Sampling. Features narrower than the sample spacing are missed: a ridge between line-of-sight samples, a summit between grid nodes (re-grid a smaller box around
summary.highestto refine it), short climbs between profile samples. Spacing is always reported. - USGS 3DEP throughput. The point query service has no published limit and no batch endpoint: about 10 points per second at this server's pacing, hence the 250-sample cap and 45 s budget. A call whose lookups would push the queue past a running call's budget is refused at once with
sampling_deadline_exceededanddata.retryAfter, so two 250-sample calls at once run in turn: the second can be retried after about 25 s. - Acquisition dates from USGS are passed through raw in M/D/YYYY form, which sometimes carries a zero month or day; any other form is omitted.
- Antimeridian. A grid box cannot cross longitude 180, so split it into two calls. Paths and sightlines crossing it work.
- Line of sight is limited to 1,000 km, where the parabolic curvature approximation overstates the midpoint rise by up to about 10 m. Its Fresnel check covers the first zone against the 60% bar at the chosen earth model, at the sample positions only, with no diffraction loss or worst-case k factor.
Running the server
Local development
-
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start:http # or bun run start:stdio -
Run checks and tests:
bun run devcheck # Lints, formats, type-checks, and more bun run test # Runs the test suite
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point: registers the four tools, builds the server instructions from config, and starts and disposes the elevation services. |
src/config | OPENTOPODATA_BASE_URL parsing and validation with Zod. |
src/mcp-server/tools/definitions | Tool definitions (*.tool.ts). |
src/mcp-server/tools/shared | Input schemas, output schemas, and format() helpers shared by the four tools. |
src/services/elevation | ElevationSampler (routing, 3DEP coverage envelope, per-call budget), geometry, attribution, and unit helpers. |
src/services/usgs-epqs | USGS Elevation Point Query Service client. |
src/services/opentopodata | Open Topo Data client and its request pacers. |
src/services/shared | Timed fetch attempts and bounded body reads shared by both clients. |
docs/design.md | Design: tool contracts, computation, services, decisions, and limitations. |
tests/ | Unit and tool tests against a mocked upstream. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging,ctx.statefor storage - Register new tools in
allToolDefinitionsinsrc/mcp-server/tools/definitions/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Data sources and attribution
| Dataset | dataset id | Served by | Terms |
|---|---|---|---|
| USGS 3D Elevation Program (3DEP) | usgs_3dep | USGS Elevation Point Query Service | Public domain (U.S. federal government work). Credit the U.S. Geological Survey, 3D Elevation Program. |
| SRTM GL1 v3 | srtm30m | Open Topo Data | Public domain (NASA/USGS). |
| Mapzen terrain tiles v1.1 | mapzen | Open Topo Data | Requires the multi-source attribution below. |
Every successful result's Sources: line credits each dataset that answered, and a response that used any Mapzen value carries the full Mapzen attribution there. The public Open Topo Data instance publishes no terms beyond its usage limits; its server software is MIT-licensed and self-hostable.
Mapzen terrain tiles attribution, verbatim from the tilezen/joerd attribution document:
- ArcticDEM terrain data DEM(s) were created from DigitalGlobe, Inc., imagery and funded under National Science Foundation awards 1043681, 1559691, and 1542736;
- Australia terrain data © Commonwealth of Australia (Geoscience Australia) 2017;
- Austria terrain data © offene Daten Österreichs – Digitales Geländemodell (DGM) Österreich;
- Canada terrain data contains information licensed under the Open Government Licence – Canada;
- Europe terrain data produced using Copernicus data and information funded by the European Union - EU-DEM layers;
- Global ETOPO1 terrain data U.S. National Oceanic and Atmospheric Administration
- Mexico terrain data source: INEGI, Continental relief, 2016;
- New Zealand terrain data Copyright 2011 Crown copyright (c) Land Information New Zealand and the New Zealand Government (All rights reserved);
- Norway terrain data © Kartverket;
- United Kingdom terrain data © Environment Agency copyright and/or database right 2015. All rights reserved;
- United States 3DEP (formerly NED) and global GMTED2010 and SRTM terrain data courtesy of the U.S. Geological Survey.
This server is independent of the U.S. Geological Survey and of Open Topo Data, and is not endorsed by either.
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.