
Connects Claude to NOAA's CO-OPS and NDBC APIs for real-time marine data across 3,450+ tide stations, 4,430+ current stations, and 1,354+ offshore buoys. You get five tools: station discovery by location or name, tide predictions with eight datum options (MLLW, MHHW, MSL, etc.), observed water levels paired with predictions for storm surge calculation, tidal current forecasts in max/slack or 6-minute intervals, and live buoy conditions including wave height, wind, and sea-surface temperature. Useful for passage planning, weather analysis, or any workflow that needs authoritative US coastal and offshore observations. Built on the mcp-ts-core framework with in-memory station caching and support for both stdio and streamable HTTP transports.
Sponsors
NOAA_APPLICATION_IDdefault: noaa-marine-mcp-serverCourtesy identifier sent as application= on CO-OPS requests. Defaults to noaa-marine-mcp-server.
MCP_LOG_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.
Find NOAA tide stations and NDBC buoys, fetch tide predictions, water levels, tidal currents, and live buoy conditions via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://noaa-marine.caseyjhand.com/mcp
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.
| Tool | Description |
|---|---|
noaa_marine_find_stations | Find CO-OPS tide, water-level, and current stations and NDBC buoys by location, name, ID, state, or capability |
noaa_marine_get_tide_predictions | High/low tide predictions or a 6-minute curve for a CO-OPS tide station |
noaa_marine_get_water_level | Observed water level at four cadences, paired with predictions and a storm-surge residual |
noaa_marine_get_monthly_means | Verified monthly tidal datums and extremes for a CO-OPS water-level station |
noaa_marine_get_currents | CO-OPS tidal-current predictions: max flood, max ebb, and slack events, or a 6-minute curve |
noaa_marine_get_conditions | Live NDBC buoy conditions: waves, wind, sea-surface and air temperature, pressure |
noaa_marine_get_current_profile | Observed ocean-current depth profile from an NDBC ADCP buoy |
noaa_marine_get_ocean_observations | Sub-surface water-column readings (temperature, salinity, oxygen, and more) from an NDBC station |
| Resource | Description |
|---|---|
noaa-marine://station/{station_id} | Metadata for a CO-OPS or NDBC station: name, coordinates, source, capabilities, prediction class, platform |
Everything in the resource except NDBC owner is also on noaa_marine_find_stations rows, for clients that only call tools.
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.
noaa_marine_find_stations toollatitude + 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 pagesource, 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 classtotal_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_coordinatesnoaa_marine_get_tide_predictions toolinterval 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_levelinvalid_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 minutesnoaa_marine_get_water_level toolinterval 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)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_droppeddaily_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_throttlednoaa_marine_get_monthly_means toolenglish feet or metric meters; the first and last months returned are those begin_date and end_date fall inhighest, 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 lowestinvalid_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 otherwisenoaa_marine_get_currents toolinterval 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)bin and depth; rows carry a flood / ebb / slack type, a non-negative speed, and as direction the station's mean bearing for that flowbin_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_throttlednoaa_marine_get_conditions toolstation_id; returns wind, waves, sea-surface and air temperature, dew point, and pressure in SI units, except tide_ft and visibility_nmistation_id is then the NDBC station read, the requested ID comes back as coops_station_id, and notice names bothwaves_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_datanoaa_marine_get_current_profile toolstation_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 unreportednoaa_marine_get_currents returns; failures are profile_not_found (no ADCP file) and no_current_datanoaa_marine_get_ocean_observations toolstation_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 potentialnull; failures are observations_not_found (no .ocean file) and no_ocean_datanoaa-marine://station/{station_id} resourcestation_id from noaa_marine_find_stations; returns that tool's station record as application/json, plus owner for NDBC stations, with a 6-hour cacheHintstation_not_found only when both catalogs were read; source_unavailable when one could not beBuilt 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:
realtime2 feeds (.txt, .adcp, .ocean), with station catalogs cached in memory for 6 hours; CO-OPS requests carry application= from NOAA_APPLICATION_ID9447130), 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 relaybegin_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_gaugehourly, 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_dataupstream_throttled whose recovery names the waitAgent-friendly output:
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_offsetnull or omitted, never fabricated: NDBC MM markers become null, blank CO-OPS values are dropped, and quality is left out rather than defaultedsource (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 parsingConnect to the hosted instance, no install:
claude mcp add --transport http noaa-marine-mcp-server https://noaa-marine.caseyjhand.com/mcp
Or run it locally:
claude mcp add noaa-marine-mcp-server -- npx -y @cyanheads/noaa-marine-mcp-server
For other clients, add this to the MCP config file:
{
"mcpServers": {
"noaa-marine-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/noaa-marine-mcp-server"]
}
}
}
No API key: NOAA CO-OPS and NDBC are open data sources. For Docker or a source build, see Running the server.
| Variable | Description | Default |
|---|---|---|
NOAA_APPLICATION_ID | Courtesy identifier sent as application= on CO-OPS requests. | noaa-marine-mcp-server |
NOAA_CLAMP_LIMITS | Clamp a limit above its cap (1,000 series rows, 200 stations) to the cap with a notice. false enforces each cap as a schema maximum. | true |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_HTTP_HOST | HTTP bind address. | 127.0.0.1 |
MCP_SESSION_MODE | HTTP session mode: stateless, stateful, or auto. | stateless |
MCP_AUTH_MODE | Authentication: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result (redacted by key name only), capped by LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES. | false |
OTEL_ENABLED | Enable OpenTelemetry. | false |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP base URL; traces and metrics export to /v1/traces and /v1/metrics under it. | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Opt-in OTLP log export endpoint; the base URL never enables it. | — |
See .env.example for the full list of optional overrides.
Requires Bun v1.4.0 or higher (or Node.js v24+).
Clone and install:
git clone https://github.com/cyanheads/noaa-marine-mcp-server.git
cd noaa-marine-mcp-server && bun install
cp .env.example .env # all vars optional
Build and run:
bun run rebuild
bun run start:stdio
# or
bun run start:http
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
docker run --rm -p 3010:3010 ghcr.io/cyanheads/noaa-marine-mcp-server:latest
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.
| Path | Purpose |
|---|---|
src/index.ts | createApp() entry point: registers the tools and resource, initializes services. |
src/config/ | NOAA_APPLICATION_ID and NOAA_CLAMP_LIMITS env var parsing with Zod. |
src/services/coops/ | CO-OPS client: station catalog cache, data fetch, error classification, date-range validation, row paging, state resolution. |
src/services/ndbc/ | NDBC service: active-stations XML and realtime2 feed parsers. |
src/services/geo.ts | Haversine distance for proximity search and state resolution. |
src/mcp-server/tools/definitions/ | Eight tool definitions (*.tool.ts). |
src/mcp-server/resources/definitions/ | Station metadata resource (noaa-marine-station.resource.ts). |
tests/ | Vitest tests mirroring src/. |
docs/ | Design doc and directory tree. |
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped loggingsrc/index.ts directly (no barrels for this server)MM values must normalize to null, not be passed through as stringsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
@cyanheads/noaa-marine-mcp-server