Find NOAA tide stations and NDBC buoys, fetch tide predictions, currents, and live conditions.
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 tide, current, and buoy data from NOAA CO-OPS and NDBC. Find tide, water-level, and current stations plus NDBC buoys, then fetch tide predictions, observed water levels, tidal currents, and live buoy conditions from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|---|
noaa_marine_find_stations | Find CO-OPS tide/water-level/current stations and NDBC buoys by location, name, state, or data 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 a 6-minute, hourly, high/low, or daily-mean cadence, paired with predictions and a storm-surge residual summary. |
noaa_marine_get_monthly_means | Verified monthly tidal datums and extremes for a CO-OPS water-level station — the record for sea-level and tidal-range trends. |
noaa_marine_get_currents | CO-OPS tidal current predictions — max flood/ebb/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 observations (temperature, salinity, oxygen, and more) from an NDBC station. |
| Resource | Description |
|---|---|
noaa-marine://station/{station_id} | Metadata for a CO-OPS or NDBC station by ID: name, coordinates, source, data capabilities, and — for NDBC — physical platform class. |
All resource data is also reachable via tools — use noaa_marine_find_stations to discover station IDs before accessing the resource.
noaa_marine_find_stations toollatitude/longitude + radius_km, default 100 km, max 1000 km), name/ID substring (matched against both sources; an exact ID match sorts first), US state/territory (CO-OPS only), source (coops/ndbc/all), or types: data capabilities (tide, current, water_level, met, current_profile, water_quality) or NDBC platform class (buoy)state_derived: true; that state can be wrong on waters shared across a state or national border. A station with no such neighbor shows its own non-code catalog value (e.g. FM) if it publishes one, which no state filter matcheslimit (default 20, max 200) unified stations with source, coordinates, distance, data capabilities, and — for NDBC — physical platform class (buoy, fixed, oilrig, dart, tao, usv, other)prediction_class, a third axis beside capability and platform: a tide station is reference (serving hilo and the 6-minute curve) or subordinate (hilo only, with reference_id naming where its offsets come from), while a current station reports its class per depth bin in bins[] alongside each bin's number and catalog depth in feet — the bin numbers noaa_marine_get_currents takes as bintotal_found and truncated report the full match count before the limit is appliedtotal_found: 0, carrying a notice derived from the filters that were applied and an echo of the applied searchsources_unavailable error rather than an empty search, whose recovery names the couple-of-minutes wait when the CO-OPS catalog was throttledincomplete_coordinates error when only one of latitude/longitude is suppliednoaa_marine_get_tide_predictions toolhilo (default, high/low events) or 6min continuous curve; up to 1 year per request, with begin_date/end_date as YYYYMMDD or YYYY-MM-DDrows_matched, rows_returned, page_offset, and next_offset on both consumption surfaces. Walk it with offset; limit lowers a page and never raises it past the byte bound, and an offset past the last row is an empty page rather than an error6min is served by reference stations only — a subordinate station's high and low events are offsets from a reference station and it has no 6-minute curve, so the request is refused before the upstream call as a typed subordinate_no_6min naming hilo and that reference stationdatum_unavailable naming the planes it does, not a report that the station ID was wrong; a Great Lakes station, which publishes no prediction series at any datum, is no_predictions pointing at noaa_marine_get_water_levellst_ldt default, gmt, lst) and units (english default feet, metric meters)date_range_exceeded, invalid_date_range, station_not_found, no_predictions, datum_unavailable, subordinate_no_6min, and upstream_throttled errors — the last a retryable RateLimited for the HTTP 403 CO-OPS answers a burst of requests with, whose recovery says to wait a couple of minutesnoaa_marine_get_water_level toolinterval selects the cadence: 6min (default) the full curve, hourly hourly heights, high_low the observed high and low waters with their H/HH/L/LL classification, daily_mean the daily mean at Great Lakes stations only. The interval is echoed in the outputbegin_date/end_date as YYYYMMDD or YYYY-MM-DD. Per-interval CO-OPS range ceilings, rejected locally before the call: 31 days for 6min, 365 for hourly and high_low, 3,655 for daily_mean. A coarser cadence is not automatically a smaller response — a year of hourly rows outweighs a month of 6-minute ones — so the ceiling bounds the request and the response budget bounds the pagep preliminary, v verified) on 6min only: CO-OPS sends no flag with the coarser products, and quality is omitted rather than defaulted to preliminary, which would label verified data unverified. Sensor sigma on 6min and hourlydatum_unavailable whose recovery names the planes that do read it — STND, IGLD, LWD at a Great Lakes station, MLLW/STND where an NAVD88 tie is missing — rather than sending the caller back to re-verify an ID noaa_marine_find_stations just returnedgaps_dropped, so rows_matched always counts only the slots that carried a value and continuous coverage across the range is only implied when that count is absentpredictions_status says whether an empty prediction series means CO-OPS has none or the fetch failed. Not fetched at all on daily_mean, which has no paired seriesresidual_summary (max surge, max drawdown) only when both series are present, computed from the finite observed/predicted pairs across the whole matched series rather than the returned page. Each side clamps at zero, so a window that stayed above prediction reports max_drawdown: 0 and one that stayed below reports max_surge: 0. Reported on 6min and hourly only — observed high and low waters do not occur at the predicted extreme times, so a high_low join would rest on a small fraction of the events, and daily_mean has no paired series at all; the notice says which appliesrows_matched, rows_returned, page_offset, and next_offset on both consumption surfaces. Observations carry the offset and the paired predictions follow by time window, so a page's two series always describe one span even after gap rows shorten the observed onedaily_mean is requested in local standard time whatever time_zone was passed — CO-OPS serves that product in LST only and silently shifts any other zone by a daydate_range_exceeded, invalid_date_range, station_not_found, no_data, datum_unavailable, great_lakes_only (daily_mean at a coastal station), verified_data_lag, and upstream_throttled errors — verified_data_lag for an hourly, high_low, or daily_mean window ending on or after the first day of the prior month, which CO-OPS may not have verified yet since it verifies the coarser products monthly for the prior month (an earlier window it answers the same way is no_data, since waiting cannot help it), and upstream_throttled for the HTTP 403 CO-OPS answers a burst of requests with (a 403 on the paired prediction fetch alone leaves predictions_status: "unavailable" instead, with a notice naming the wait)noaa_marine_get_monthly_means toolmonthly_mean product: the month's highest and lowest water, its tidal datums (mhhw, mhw, msl, mtl, mlw, mllw, dtl), ranges (gt, mn, dhq, dlq), lunitidal intervals in hours (hwi, lwi), and CO-OPS's inferred code passed through verbatim — a code (0, 1, 11 observed), not a booleanenglish feet or metric meters. The ranges (gt, mn, dhq, dlq) are differences between two planes, so they read the same under every datum. A value CO-OPS publishes as an empty string is omitted, never zeroed — a Great Lakes station at IGLD carries only highest, msl, and lowestbegin_date/end_date as YYYYMMDD or YYYY-MM-DD, spanning up to 73,000 days (the ceiling CO-OPS names), rejected locally before the call. The months the two dates fall in are the first and last returned; CO-OPS answers one month past end_date, and that month is droppedrows_matched, rows_returned, page_offset, and next_offset. Walk it with offset; an offset past the last month is an empty page rather than an errorinvalid_date_range, date_range_exceeded, station_not_found, datum_unavailable, verified_data_lag, no_data, and upstream_throttled errors. CO-OPS answers an unpublished window with one sentence whatever the cause, so a window ending on or after the first day of the prior month is verified_data_lag and any earlier one is no_datanoaa_marine_get_currents toolMAX_SLACK (default): max flood, max ebb, and slack events only — the actionable view for passage planning6min: continuous current curve, each row carrying its own flood/ebb/slack sense plus the station mean flood or ebb bearing that sense implies (a station constant, not an instantaneous heading)rows_matched, rows_returned, page_offset, and next_offset on both surfaces. offset and limit walk whichever series the interval selects — the max/slack events or the 6-minute curveenglish (knots for speed, feet for the echoed depth, default) or metric (cm/s for speed, meters for depth) — CO-OPS publishes metric current speed in cm/s, the unit noaa_marine_get_current_profile also reportsbin selects one of a station's depth bins; omit it for the CO-OPS default, the shallowest. The bin CO-OPS answered with and its depth are echoed on every response, and a bin the station does not publish is a typed bin_unavailable naming the bins it doesACT4176), distinct from numeric tide/water-level IDsbegin_date/end_date as YYYYMMDD or YYYY-MM-DD; typed date_range_exceeded, invalid_date_range, station_not_found, no_predictions, predictions_unavailable, bin_unavailable, and upstream_throttled (the HTTP 403 CO-OPS answers a burst of requests with) errorsnoaa_marine_get_conditions tooltide_ft (feet) and visibility_nmi (nautical miles), both rarely populated at offshore buoysnull when the buoy did not report, never a fabricated value. latitude/longitude are null for a station absent from the NDBC catalog, and observed_at is always a valid instantwaves_observed_at, and any other block read from an earlier row is named with its measurement time in the response notice. Row cadence runs 5–60 minutes depending on the stationbuoy_not_found and no_sensor_data errorsnoaa_marine_get_current_profile toolnoaa_marine_get_currents, a CO-OPS tidal-current predictionfind_stations with types: ["current_profile"] to discover ones that donull per bin when the sensor did not report that component; typed profile_not_found and no_current_data errorsnoaa_marine_get_ocean_observations toolnoaa_marine_get_conditions (surface weather and sea state)null, never a fabricated zerofind_stations using source="ndbc" and types: ["water_quality"], NDBC's own water-quality catalog flag — a strong hint, not a guarantee, so expect observations_not_found on a flagged station serving no .ocean filenoaa-marine://station/{station_id} resourceapplication/json — name, coordinates, source, capabilities, state (with state_derived, resolved as on noaa_marine_find_stations), the CO-OPS prediction_class (with reference_id or per-bin bins[], exactly as on noaa_marine_find_stations), and (NDBC) platform classstation_id comes from noaa_marine_find_stationsstation_not_found when both catalogs were read and neither carries the ID, and source_unavailable when a catalog could not be read — a station only the unread catalog carries is never reported as nonexistent. When the unread CO-OPS catalog was throttled (HTTP 403), the recovery names the couple-of-minutes waitcacheHint)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:
find_stations fans out across both sources in parallelMM (missing sensor data) to null, never passes it through as a stringapplication= courtesy parameter sent on every request (configurable via NOAA_APPLICATION_ID)Agent-friendly output:
total_found on find_stations shows the count before the limit slice, so agents know whether to re-querysource (coops | ndbc) plus a data-capability type and (NDBC only) a platform class — agents branch on data, not string parsingNo API key required. Both NOAA CO-OPS and NDBC are open, keyless data sources.
Connect directly via Streamable HTTP — no install, no API key:
{
"mcpServers": {
"noaa-marine": {
"type": "streamable-http",
"url": "https://noaa-marine.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file:
{
"mcpServers": {
"noaa-marine": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/noaa-marine-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"noaa-marine": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/noaa-marine-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"noaa-marine": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/noaa-marine-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
git clone https://github.com/cyanheads/noaa-marine-mcp-server.git
cd noaa-marine-mcp-server
bun install
cp .env.example .env
# edit .env if needed (all vars optional)
| Variable | Description | Default |
|---|---|---|
NOAA_APPLICATION_ID | Courtesy identifier sent as application= on CO-OPS requests. | noaa-marine-mcp-server |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless. The server declares stateless in src/index.ts; set this to override. | stateless |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
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 build -t noaa-marine-mcp-server .
docker run --rm -p 3010:3010 noaa-marine-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/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 tools, resource, and initializes services. |
src/config/ | NOAA_APPLICATION_ID env var parsing with Zod. |
src/services/coops/ | CO-OPS Tides & Currents API client: station list cache, data fetch, error detection. |
src/services/ndbc/ | NDBC buoy service: active stations XML parser, realtime text parser. |
src/mcp-server/tools/ | Eight tool definitions (*.tool.ts). |
src/mcp-server/resources/ | 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.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/noaa-marine-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-noaa-marine-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/noaa-marine-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/noaa-marine-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.