Ocean MCP

Ocean MCP

Enables LLMs to work with Copernicus Marine ocean data through semantic tools for dataset discovery, metadata retrieval, recommendation, validation, point time-series extraction, area statistics, subsetting, and computation without exposing raw API plumbing.

Category
Visit Server

README

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.

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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured