Skip to main content
Glama
cyanheads

@cyanheads/noaa-marine-mcp-server

by cyanheads

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

Public Hosted Server: https://noaa-marine.caseyjhand.com/mcp


Overview

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.

Tools

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_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.

Resources

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.

Related MCP server: mcp-noaa-tides

Capability reference

noaa_marine_find_stations tool

  • Filter by proximity (latitude/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)

  • Returns up to limit (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)

  • CO-OPS prediction stations also carry 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 bin

  • total_found and truncated report the full match count before the limit is applied

  • Zero matches is a success with total_found: 0, carrying a notice derived from the filters that were applied and an echo of the applied search

  • A catalog that fails to load is reported as an unread source alongside the results; when every needed catalog fails, that is a typed sources_unavailable error rather than an empty search

  • Station lists are cached in-memory with a 6-hour TTL — first call after startup may be slightly slower

  • Typed incomplete_coordinates error when only one of latitude/longitude is supplied


noaa_marine_get_tide_predictions tool

  • hilo (default, high/low events) or 6min continuous curve; up to 1 year per request

  • A range whose rows fit the response budget returns whole; a longer one returns the leading rows as a page, with rows_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 error

  • 6min 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 station

  • Ten datums, matching what the CO-OPS predictions product accepts: MLLW (default, US nautical chart), MHHW, MHW, MTL, MSL, MLW, DTL, NAVD (NAVD88, where the station has a tie), STND (the station's own datum), CRD (Columbia River only)

  • A datum the station does not carry is a typed datum_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_level

  • Time zone (lst_ldt default, gmt, lst) and units (english default feet, metric meters)

  • Typed date_range_exceeded, invalid_date_range, station_not_found, no_predictions, datum_unavailable, and subordinate_no_6min errors


noaa_marine_get_water_level tool

  • interval 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 output

  • 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 page

  • Quality flags (p 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 hourly

  • Thirteen datums, matching what the CO-OPS water-level product accepts: MLLW (default, US nautical chart), MHHW, MHW, MTL, MSL, MLW, NAVD (NAVD88, where the station has a tie), STND (the station's own datum), IGLD and LWD (Great Lakes only), CRD (Columbia River only), LWI and HWI (lunitidal intervals)

  • A datum the station does not carry is a typed datum_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 returned

  • A sensor outage leaves slots with no reading; they are dropped and counted in gaps_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 absent

  • Paired tide predictions at the interval matching the observed cadence, fetched in parallel — the observed series returns either way, and predictions_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 series

  • residual_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. 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 applies

  • A range whose rows fit the response budget returns whole; a longer one returns the leading rows as a page, with rows_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 one

  • daily_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 day

  • Typed date_range_exceeded, invalid_date_range, station_not_found, no_data, datum_unavailable, great_lakes_only (daily_mean at a coastal station), and verified_data_lag errors — the last for a window CO-OPS has not verified yet, since it verifies the coarser products monthly for the prior month


noaa_marine_get_currents tool

  • MAX_SLACK (default): max flood, max ebb, and slack events only — the actionable view for passage planning

  • 6min: 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)

  • Both intervals are bounded by response size: a range whose rows fit the response budget returns whole; a longer one returns the leading rows as a page, with 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 curve

  • Units english (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 reports

  • bin 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 does

  • A station whose currents CO-OPS will not predict as discrete events returns an empty list plus CO-OPS's own wording in the notice, not an error

  • Current station IDs are alphanumeric (e.g. ACT4176), distinct from numeric tide/water-level IDs

  • Up to 1 year per request; typed date_range_exceeded, invalid_date_range, station_not_found, no_predictions, predictions_unavailable, and bin_unavailable errors


noaa_marine_get_conditions tool

  • Wave height/period/direction, wind speed/gust/direction, sea-surface and air temperature, dew point, barometric pressure

  • All values SI except tide_ft (feet) and visibility_nmi (nautical miles), both rarely populated at offshore buoys

  • Every sensor field is nullable — null 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 instant

  • NDBC writes each block of columns on its own cycle, so a block resolves from the most recent row within 90 minutes that carried it: waves report their own waves_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 station

  • Typed buoy_not_found and no_sensor_data errors


noaa_marine_get_current_profile tool

  • Depth (m), direction (degrees true, flow-toward), and speed (cm/s) per bin, shallowest first

  • Observed NDBC ADCP measurement — distinct from noaa_marine_get_currents, a CO-OPS tidal-current prediction

  • Most NDBC stations serve no ADCP profile; use find_stations with types: ["current_profile"] to discover ones that do

  • Direction or speed is null per bin when the sensor did not report that component; typed profile_not_found and no_current_data errors


noaa_marine_get_ocean_observations tool

  • Water temperature, conductivity, salinity, dissolved oxygen (% and ppm), chlorophyll, turbidity, pH, and redox potential per depth

  • Water-column counterpart to noaa_marine_get_conditions (surface weather and sea state)

  • Sensor coverage is sparse — most stations report only temperature and salinity; unreported values are null, never a fabricated zero

  • Find candidates with find_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 file


noaa-marine://station/{station_id} resource

  • Station record as application/json — name, coordinates, source, capabilities, state, the CO-OPS prediction_class (with reference_id or per-bin bins[], exactly as on noaa_marine_find_stations), and (NDBC) platform class

  • station_id comes from noaa_marine_find_stations

  • Typed station_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

  • Cached with a 6-hour TTL (cacheHint)

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:

  • In-memory station cache (6-hour TTL) for CO-OPS and NDBC station lists — discovery is fast after first startup

  • CO-OPS and NDBC integrated in a unified station model — find_stations fans out across both sources in parallel

  • NDBC fixed-width text parser normalizes MM (missing sensor data) to null, never passes it through as a string

  • Paired water-level and prediction fetches for storm-surge residual computation

  • CO-OPS application= courtesy parameter sent on every request (configurable via NOAA_APPLICATION_ID)

Agent-friendly output:

  • Datum echoed on every tide/water-level response so agents state units and reference correctly without assumptions

  • total_found on find_stations shows the count before the limit slice, so agents know whether to re-query

  • All NDBC sensor fields explicitly nullable — agents don't fabricate missing readings

  • Typed station source (coops | ndbc) plus a data-capability type and (NDBC only) a platform class — agents branch on data, not string parsing

Getting started

No API key required. Both NOAA CO-OPS and NDBC are open, keyless data sources.

Public Hosted Instance

Connect directly via Streamable HTTP — no install, no API key:

{
  "mcpServers": {
    "noaa-marine": {
      "type": "streamable-http",
      "url": "https://noaa-marine.caseyjhand.com/mcp"
    }
  }
}

Self-Hosted / Local

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

Prerequisites

  • Bun v1.4.0 or higher (or Node.js v24+).

  • No external API keys needed — NOAA CO-OPS and NDBC are fully open.

Installation

  1. Clone the repository:

git clone https://github.com/cyanheads/noaa-marine-mcp-server.git
  1. Navigate into the directory:

cd noaa-marine-mcp-server
  1. Install dependencies:

bun install
  1. Configure environment:

cp .env.example .env
# edit .env if needed (all vars optional)

Configuration

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.

Running the server

Local development

  • 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

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.

Project structure

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/

Seven 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.

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:

bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

Related MCP Connectors

Related MCP Servers