Look up elevation worldwide, profile route ascent/descent, grid areas, check terrain line of sight.
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
Ground elevation and terrain analysis from two keyless sources: USGS 3DEP across the US and its territories (plus much of Canada and Mexico), and Open Topo Data everywhere else, which answers from SRTM GL1 v3 and, where SRTM has no tile, Mapzen terrain tiles. Look up spot heights, measure a route's ascent, descent, and grades, find an area's high and low points, and check whether terrain blocks a sightline. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| 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 |
elevation_get_points toolpoints: 1–100 {lat, lon} objects in decimal degrees (WGS84)ok or no_data (a miss never fails the call), with elevation_m / elevation_ft, dataset, and resolution_m (omitted for mapzen); 3DEP answers add raster_id and acquisition_date. points_with_data counts the hitselevation_get_profile toolpath: 2–1,000 vertices in travel order (consecutive duplicates dropped); samples: 2–250 evenly spaced points along it, endpoints included (default 100)summary carries total_distance_m, ascent_m / descent_m (and feet), start, end, min, and max elevation, highest_point / lowest_point, and max_grade_pct / min_grade_pct with where each occurs; ascent depends on the reported sample_interval_m. Fails as degenerate_path (under 1 m of route) or no_coverage (no sample has data)include_samples (default true) returns samples[] (distance, position, elevation, grade, dataset, and resolution per sample); false omits it and the sample table, and the rest of the result is unchangedelevation_get_grid toolsouth, west, north, east edges in decimal degrees (a box can't cross longitude 180); rows and cols 2–25 each (default 10), with rows × cols at most 250elevations_m and cell_datasets matrices indexed [row][col] (row 0 north, column 0 west, null without data), plus summary.highest, summary.lowest, mean_elevation_m, and relief_m. Fails as invalid_bbox, too_many_cells, or no_coverageelevation_check_line_of_sight toolobserver and target points, at most 1,000 km apart; observer_height_m (default 1.7) and target_height_m (default 0), each 0–10,000 m above ground; earth_model flat, geometric, optical (default, κ 0.13), or radio (κ 0.25); samples 3–250 (default 100); optional water_surface_m (-500 to 9,000) and frequency_mhz (30–300,000)verdict is clear, blocked, or indeterminate (samples without data leave the line unconfirmed), with min_clearance_m / min_clearance_ft, the limiting_point, and the first_obstruction when blocked. Fails as same_endpoints (under 1 m apart), sightline_too_long (over 1,000 km apart), or endpoint_no_datafrequency_mhz adds fresnel: a sufficient, insufficient, or indeterminate verdict 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 not limiting_pointwater_surface_m sets a water level, which then applies to every sample| 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.
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:
source on every tool: auto (default) queries USGS 3DEP inside its coverage and sends 3DEP misses and everything outside it to Open Topo Data; usgs_3dep and opentopodata pin one provider. Only a coverage miss falls back, never an outageAgent-friendly output:
dataset (usgs_3dep, srtm30m, mapzen) and, except for Mapzen, its resolution_m (resolution_m_range on profiles and grids); computed results add datasets_used counts, and a Sources: line credits every dataset that answerednull, never 0; summaries report how many values had dataA 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.
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
srtm30m and mapzen datasets, for heavy or hosted use (see Configuration).git clone https://github.com/cyanheads/elevation-mcp-server.git
cd elevation-mcp-server
bun install
cp .env.example .env
# optionally set OPENTOPODATA_BASE_URL to a self-hosted Open Topo Data instance
| 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.
opentopodata_rate_limited).opentopodata_daily_limit for up to 24 hours, including an auto call with a single 3DEP miss, so global answers on such a deployment are best-effort; pointing OPENTOPODATA_BASE_URL at its own instance (loaded with srtm30m and mapzen) 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.water_surface_m measures a line over water to the water instead.summary.highest to refine it), short climbs between profile samples. Spacing is always reported.sampling_deadline_exceeded and data.retryAfter, so two 250-sample calls at once run in turn: the second can be retried after about 25 s.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
| 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. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storageallToolDefinitions in src/mcp-server/tools/definitions/index.ts| 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:
This server is independent of the U.S. Geological Survey and of Open Topo Data, and is not endorsed by either.
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/elevation-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-elevation-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/elevation-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/elevation-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.