@cyanheads/noaa-marine-mcp-server
Find NOAA tide stations and NDBC buoys, fetch tide predictions, water levels, tidal currents, and live buoy conditions via MCP. STDIO or Streamable HTTP.
8 Tools • 1 Resource • 1 Skill
Overview
US tides, currents, water levels, and buoy observations from NOAA CO-OPS and NDBC. Find coastal stations and buoys, then fetch tide and tidal-current predictions, observed and monthly-mean water levels, and live buoy conditions, current profiles, and water-column readings.
Tools
Resources
Everything in the resource except NDBC owner is also on noaa_marine_find_stations rows, for clients that only call tools.
Agent skill
The published noaa-marine-guide skill carries the gates and conventions the tool descriptions don't: which station serves which tool, why a place-name search misses, the clock and direction differences between CO-OPS and NDBC, and what the server can't answer (forecasts, warnings, past buoy readings). MCP clients read it as skill://noaa-marine-guide/SKILL.md through the Skills extension. Plugin installs include it too.
Capability reference
noaa_marine_find_stations tool
- Filters combine:
latitude + longitude with radius_km (default 100, max 1000), query (name or ID substring on both sources; an exact ID leads unless sorting by distance), state (CO-OPS only), source (coops / ndbc / all), and types (tide, current, water_level, met, current_profile, water_quality, or the platform class buoy); limit defaults to 20, and a larger value is clamped to 200 with a notice (rejected when NOAA_CLAMP_LIMITS=false); offset pages past one page
- Each station carries
source, capabilities, type, and for NDBC a platform; CO-OPS rows add state (state_derived: true when borrowed from the nearest state-bearing station within 25 km), a tide prediction_class (reference, or subordinate with reference_id), and current bins[] giving each bin's number, depth in feet, and class
total_found counts every match across pages; truncated and next_offset appear while matches remain past the page; zero matches is a success with applied_search, a partial catalog read is flagged in sources, and total catalog failure is sources_unavailable; only one of latitude/longitude is incomplete_coordinates
noaa_marine_get_tide_predictions tool
interval hilo (default) or 6min, up to 365 days; ten datums (MLLW, MHHW, MHW, MTL, MSL, MLW, DTL, NAVD, STND, CRD), time_zone (lst_ldt, gmt, lst), and units (english feet, metric meters)
6min works at reference stations only: a subordinate station fails as subordinate_no_6min naming its reference station, and a Great Lakes station is no_predictions, pointing to noaa_marine_get_water_level
- Other failures:
invalid_date_range, date_range_exceeded, station_not_found, datum_unavailable (its hint names a datum to retry with, never the one just rejected), and upstream_throttled, a retryable CO-OPS HTTP 403 that clears in a couple of minutes
noaa_marine_get_water_level tool
interval 6min (default, 31 days), hourly or high_low (365 days), or daily_mean (3,655 days, Great Lakes only, always in LST; a coastal station is great_lakes_only); thirteen datums (MLLW, MHHW, MHW, MTL, MSL, MLW, NAVD, STND, IGLD, LWD, CRD, LWI, HWI)
- Observations carry
quality (p / v, 6min only), sigma (6min and hourly), and type (H / HH / L / LL, high_low only); slots with no reading are dropped and counted in gaps_dropped
- Paired predictions at the matching cadence (none on
daily_mean), with predictions_status (empty / unavailable) explaining an empty series; residual_summary (max_surge, max_drawdown) covers the whole matched range, on 6min and hourly only. Failures: invalid_date_range, date_range_exceeded, station_not_found, datum_unavailable, no_gauge, future_window (begin_date after the station's current date in the requested time_zone), no_data, verified_data_lag (a coarser-cadence window ending on or after the first day of the prior month, which CO-OPS may not have verified yet), and upstream_throttled
noaa_marine_get_monthly_means tool
- Up to 73,000 days per request; eleven datums (MLLW, MHHW, MHW, MTL, MSL, MLW, NAVD, STND, IGLD, LWD, CRD) in
english feet or metric meters; the first and last months returned are those begin_date and end_date fall in
- One row per month:
highest, lowest, the tidal datums (mhhw, mhw, msl, mtl, mlw, mllw, dtl), ranges (gt, mn, dhq, dlq), lunitidal intervals in hours (hwi, lwi), and the inferred code verbatim; values CO-OPS leaves blank are omitted, so a Great Lakes station carries only highest, msl, and lowest
- Failures:
invalid_date_range, date_range_exceeded, station_not_found, datum_unavailable, no_gauge, upstream_throttled, and for an empty window verified_data_lag when it ends on or after the first day of the prior month, no_data otherwise
noaa_marine_get_currents tool
interval MAX_SLACK (default: max flood, max ebb, and slack events) or 6min, up to 365 days; bin picks a depth bin from bins[] (default the shallowest); units english (knots, feet) or metric (cm/s, meters)
- Every response echoes
bin and depth; rows carry a flood / ebb / slack type, a non-negative speed, and as direction the station's mean bearing for that flow
- A bin the station lacks is
bin_unavailable listing the ones it has; a station CO-OPS won't predict as events returns an empty list with its statement in notice, and one with no published predictions is predictions_unavailable; also invalid_date_range, date_range_exceeded, station_not_found, no_predictions, and upstream_throttled
noaa_marine_get_conditions tool
- One NDBC
station_id; returns wind, waves, sea-surface and air temperature, dew point, and pressure in SI units, except tide_ft and visibility_nmi
- A 7-digit CO-OPS station ID also works when NDBC relays that gauge's met sensors under a station of its own:
station_id is then the NDBC station read, the requested ID comes back as coops_station_id, and notice names both
- Every sensor field is nullable; each column block comes from the newest row within 90 minutes that carried it, waves reporting their own
waves_observed_at and any other older block named in notice. Failures: buoy_not_found (no observation file and no relay, a relay with no file, or several relays the met flag doesn't settle), no_sensor_data
noaa_marine_get_current_profile tool
- One NDBC
station_id, found with types: ["current_profile"]; bins[] runs shallowest first with depth_m, direction_deg (true, flow-toward), and speed_cm_s, the last two null when unreported
- Observed ADCP measurements, not the CO-OPS predictions
noaa_marine_get_currents returns; failures are profile_not_found (no ADCP file) and no_current_data
noaa_marine_get_ocean_observations tool
- One NDBC
station_id, found with types: ["water_quality"], a catalog flag rather than a guarantee; readings[] holds one row per depth with temperature, conductivity, salinity, dissolved oxygen (% and ppm), chlorophyll, turbidity, pH, and redox potential
- Most stations report only temperature and salinity, and unreported values are
null; failures are observations_not_found (no .ocean file) and no_ocean_data
noaa-marine://station/{station_id} resource
station_id from noaa_marine_find_stations; returns that tool's station record as application/json, plus owner for NDBC stations, with a 6-hour cacheHint
station_not_found only when both catalogs were read; source_unavailable when one could not be
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.
CO-OPS / NDBC-specific:
- CO-OPS data and metadata APIs plus the NDBC active-stations catalog and
realtime2 feeds (.txt, .adcp, .ocean), with station catalogs cached in memory for 6 hours; CO-OPS requests carry application= from NOAA_APPLICATION_ID
- Station IDs belong to one source, with one exception: CO-OPS tide and water-level IDs are numeric (
9447130), CO-OPS current IDs alphanumeric (ACT4176), NDBC IDs mostly 5-character codes (46041) with some 7-digit numbers, and only noaa_marine_get_conditions reads a CO-OPS ID, through its NDBC relay
- CO-OPS tools take
begin_date / end_date as YYYYMMDD or YYYY-MM-DD and check the calendar and each tool's range ceiling before calling (invalid_date_range, date_range_exceeded); heights default to MLLW, a datum a station lacks is datum_unavailable with a hint naming a datum to retry with, never the rejected one, and a station that has never had a water-level gauge is no_gauge
- CO-OPS verifies
hourly, high_low, daily_mean, and monthly means once a month, for the prior month: an empty window ending on or after the first of the prior month is verified_data_lag, an earlier one no_data
- CO-OPS answers a burst of requests with HTTP 403 for about two minutes; the CO-OPS data tools report it as a retryable
upstream_throttled whose recovery names the wait
Agent-friendly output:
- Count paging on the four CO-OPS series tools: a page is
limit rows from offset, defaulting to 50 (24 for monthly means) with a larger value clamped to 1,000 and a notice; a range of at most limit rows returns whole, and a longer one adds rows_matched, rows_returned, page_offset, and next_offset
- Missing means
null or omitted, never fabricated: NDBC MM markers become null, blank CO-OPS values are dropped, and quality is left out rather than defaulted
- Discriminated fields: station
source (coops | ndbc), capability type, NDBC platform, and prediction_class, with datum and units echoed on every CO-OPS height response, so callers branch on data, not string parsing
Getting started
Connect to the hosted instance, no install:
Or run it locally:
For other clients, add this to the MCP config file:
No API key: NOAA CO-OPS and NDBC are open data sources. For Docker or a source build, see Running the server.
Configuration
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
-
Clone and install:
-
Build and run:
-
Run checks and tests:
Docker
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/noaa-marine-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/noaa-marine-mcp-server:latest"]. To build locally, docker build -t noaa-marine-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for request-scoped logging
- Register tools and resources in
src/index.ts directly (no barrels for this server)
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
- NDBC
MM values must normalize to null, not be passed through as strings
Contributing
Issues are welcome. Run checks and tests before submitting:
License
Apache-2.0 — see LICENSE for details.