Skip to main content
Glama

Ocean MCP

An MCP server that lets an LLM work with Copernicus Marine ocean data through semantic, ocean-data-shaped tools — not a 1:1 wrapper around the Copernicus Marine Toolbox Python API.

"Find daily sea-surface temperature around Marseille for summer 2025 and calculate the monthly averages."

Why a semantic layer, not a thin wrapper

The Copernicus Marine Toolbox is a general-purpose data-access library: describe(), subset(), open_dataset(), read_dataframe(). Exposing those 1:1 as MCP tools would hand the LLM low-level plumbing (raw dataset IDs, service names, coordinate-selection strategies) and force it to reconstruct scientific judgment the tools should already encode — e.g. that a request needs the nearest grid cell, that a 2D dataset has no depth axis, or that a full catalogue crawl takes ~7 minutes and can't run inside a single tool call.

Instead, this server sits a domain layer between the LLM and the Toolbox:

MCP Layer (mcp_tools/)         — thin: parse input, call one service, shape output
    |
    v
Ocean Service Layer (services/) — all business logic
    |
    +--> Dataset Discovery   (services/catalogue.py)
    +--> Metadata            (services/metadata.py)
    +--> Validation          (services/validation.py)
    +--> Data Access         (services/extraction.py)
    |
    v
Copernicus Marine Toolbox (copernicus/client.py — the only module that imports it)

mcp_tools/* never talks to copernicusmarine directly and contains no business logic — it validates input via Pydantic, calls one service function, and returns one schema. All science and correctness logic (which service to prefer, whether a variable exists, whether a date falls in range) lives in services/*, which is fully testable without network access via a fake CopernicusClient.

Related MCP server: Copernicus Earth Observation MCP Server

What was verified before building this (not assumed)

  • describe() without a product_id/dataset_id scope crawls the entire ~1,260-dataset catalogue and takes several minutes. contains=[...] does not speed this up — it still walks the whole catalogue and filters after. This is why search_ocean_datasets never calls describe() live: it searches a local JSON index built offline by scripts/refresh_catalogue_cache.py.

  • Datasets often expose multiple zarr services with identical variables/coordinates but different chunkingarco-geo-series (optimized for spatial slabs) vs. arco-time-series (optimized for point/time-series access). extract_point_timeseries explicitly prefers arco-time-series rather than trusting auto-selection, and reports which service it used.

  • Time coordinate values are not uniformly typed across the catalogue: gridded/model datasets use epoch milliseconds, but in-situ observation datasets use ISO 8601 strings directly (coordinate_unit == "ISO8601"). time_utils.coerce_time_value_to_iso handles both.

  • read_dataframe() returns time/latitude/longitude as a MultiIndex, not columns. services/extraction.py normalizes this before use.

  • copernicusmarine reads COPERNICUSMARINE_SERVICE_USERNAME/PASSWORD into module-level constants at import time, not lazily per call. This means .env must be loaded before anything imports copernicusmarine — see the ordering in server.py and tests/conftest.py.

  • 2D (surface-only) datasets have no depth coordinate at all, which is how validate_data_request distinguishes "depth not applicable to this dataset" from "depth out of range". 3D (depth-resolved) datasets, conversely, silently return every depth level if none is requested — extract_point_timeseries/extract_area_statistics now require an explicit depth whenever has_depth is true, rather than averaging across levels no one asked for.

  • A depth coordinate's minimum_value/maximum_value are sometimes both None even though the axis has real bounds — some ocean model products report depth as a discrete list of levels (coordinate.values) instead of a continuous range. coordinate_utils.coordinate_min_max falls back to min(values)/max(values).

  • subset()/open_dataset() need coordinates_selection_method="nearest" explicitly (matching what read_dataframe() already used) — with the default "inside", a depth/bbox request that doesn't land exactly on a grid point can match nothing and silently return all-NaN data instead of an error.

  • xr.Dataset.resample(time=freq).map(fn) does not reduce the time dimension for you — each group passed to fn still has its own multi-step "time" axis inside it. extract_area_statistics initially reduced only latitude/longitude per group, so "monthly" aggregation silently produced one row per input day instead of one per month. services/area_statistics.py now also reduces over time inside each resample group.

  • ResponseSubset (from subset(), verified via dry_run=True) fields: file_path, output_directory, filename, file_size/data_transfer_size (MB), variables, coordinates_extent (a list of GeographicalExtent/TimeExtent objects with minimum/maximum/coordinate_id), status, message, file_status, file_names.

  • open_dataset()'s returned xr.Dataset uses plain dims {time, latitude, longitude} with no MultiIndex surprise (unlike read_dataframe()) — but at least one real product decodes its time coordinate to a valid datetime64 dtype with semantically wrong values (clustered near the 1970 epoch). A dtype check alone doesn't catch this; extract_area_statistics also sanity-checks the decoded range against the requested date range before aggregating.

Tools

Tool

Purpose

search_ocean_datasets

Rank candidate datasets from the local catalogue cache against a free-text query, optional variables, region, and date range.

get_dataset_metadata

Normalized metadata for one dataset: variables (units, standard names), spatial/temporal coverage, depth availability, provider, and which service to prefer for point vs. area extraction. Prefers a live scoped lookup (seconds), falls back to the cache.

recommend_dataset

Recommends the single most appropriate dataset for a scientific request — re-verifies coverage against live metadata and prefers gap-filled L4 analysis products over raw L3/L3S swath products, rather than trusting a variable-name match alone. Reports concrete reasoning, limitations, and up to two alternatives with why they weren't picked.

validate_data_request

Checks a proposed extraction before running it — dataset/variable existence (with closest-match suggestions), coordinate/bbox validity, date coverage, depth applicability — and returns {valid, errors, warnings, estimated_output_size_mb} rather than raising, so an agent can self-correct.

extract_point_timeseries

Validates, then extracts a time series at the nearest grid cell to a point. Reports requested vs. actual coordinates, the distance between them, units, and missing-value counts — nothing is silent.

extract_area_statistics

Spatial mean/min/max/std/percentile over a bounding box, optionally aggregated over time (daily/monthly/yearly). Mean/std are weighted by cos(latitude) to account for grid-cell area shrinking toward the poles; percentile is unweighted, and that limitation is stated in the response, not hidden.

subset_ocean_data

Semantic wrapper over subset(): extracts a bounding box/time/depth range to a local NetCDF or CSV file and returns metadata about it (path, size, coverage) — never the file's contents.

compute_ocean_statistics

Pure computation (mean/min/max/std/percentile/anomaly/trend/correlation) over values already returned by a prior tool call — no re-fetching, no server-side result store.

Known limitation: free-text relevance ranking

search_ocean_datasets/recommend_dataset score candidates with keyword/phrase matching over title, keywords, and description — no domain ontology. On a real query for "chlorophyll concentration," it recommended a dinoflagellate-biomass model product over the dataset literally named bgc-chl, because the two tied on score and the tie-break fell on catalogue order. The tie itself is defensible (both are real, related biogeochemistry variables), but the ranking is naive; a synonym/variable-alias table would be the natural next improvement.

Setup

Requires Python ≥3.11 and uv.

uv sync

Credentials

Copy .env.example to .env and fill in your real Copernicus Marine credentials.

Credentials are read only via COPERNICUSMARINE_SERVICE_USERNAME/COPERNICUSMARINE_SERVICE_PASSWORD, by the Toolbox itself. This project never reads, logs, or returns their values — config.py only checks they're present, and errors surfaced to the LLM (errors.pyfastmcp.exceptions.ToolError) never include secret values or raw stack traces.

Build the catalogue cache

search_ocean_datasets reads a local index rather than crawling the catalogue live. Build it once (takes several minutes):

uv run python scripts/refresh_catalogue_cache.py

Re-run periodically to refresh (get_dataset_metadata's response reports catalogue_snapshot_date when it falls back to the cache, so staleness is always visible).

Run the server

uv run python -m ocean_mcp.server

Claude Code / MCP client configuration

Add to your MCP client config (e.g. .mcp.json):

{
  "mcpServers": {
    "ocean-mcp": {
      "command": "uv",
      "args": ["run", "python", "-m", "ocean_mcp.server"],
      "cwd": "/absolute/path/to/mcp-toolbox",
      "env": {
        "COPERNICUSMARINE_SERVICE_USERNAME": "your-username",
        "COPERNICUSMARINE_SERVICE_PASSWORD": "your-password"
      }
    }
  }
}

(Credentials can instead be left out of the config and picked up from .env/the environment where the server process runs — whichever keeps them furthest from source control in your setup.)

Example queries

  • "Find daily sea surface temperature datasets covering the Mediterranean."

  • "Get metadata for cmems_obs-sst_med_phy_my_l3s_P1D-m — what variables and depth range does it have?"

  • "Recommend a dataset for daily sea surface temperature around Marseille in summer 2025."

  • "Validate a request for thetao at 43.2965, 5.3698 between June and September 2025."

  • "Give me daily SST at 43.2965, 5.3698 for the first week of June 2025."

  • "Calculate the monthly average sea surface temperature over the Gulf of Lion for Q1 2025."

  • "Subset SST over the Gulf of Lion for June 1–3, 2025 to a NetCDF file."

  • "What's the trend in those monthly SST values?"

Development

uv run pytest tests/unit tests/integration   # no network required
uv run ruff check .
uv run mypy src

Live tests hit the real Copernicus Marine service and require real credentials plus an explicit opt-in:

RUN_LIVE_COPERNICUS_TESTS=1 uv run pytest tests/live -m live

Test layout

  • tests/unit/ — services and schemas against a fake CopernicusClient (tests/fakes.py, built from live-verified model shapes), no network.

  • tests/integration/ — full mcp_tools call paths via fastmcp.Client in-memory, checking schema shape and error translation (ToolError).

  • tests/live/ — real describe()/read_dataframe()/open_dataset()/subset() calls, skipped unless opted in.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Access oceanographic and environmental data from 63+ ERDDAP servers worldwide through natural language queries. Search datasets, retrieve metadata, and download scientific data for climate research, marine biology, and coastal management.
    18
  • A
    license
    B
    quality
    D
    maintenance
    Provides tools to search, download, and manage satellite imagery from all Copernicus Sentinel missions via the Copernicus Data Space ecosystem. It enables advanced geospatial queries, temporal coverage analysis, and automated data management for Earth observation tasks.
    13
    3
    LGPL 3.0
  • A
    license
    A
    quality
    A
    maintenance
    Enables natural language interaction with rasdaman multidimensional databases by translating tool calls into WCS/WCPS queries. It allows users to list coverages, retrieve metadata, and execute complex queries on datacubes through an LLM.
    6
    7
    MIT

View all related MCP servers

Related MCP Connectors

  • Gateway between LLM agents and world data through eight tools and a bundled endpoint catalog.

  • Bounded tools for rendering, extraction, RAG, enrichment, local discovery and review analysis.

  • Interact with climate metrics via Riskthinking.AI's CDT Express API in supported AI chat experiences

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ishimwe5555/mcp-toolbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server