Developer API Beta

Free read-only access to Dublin Bay observations & marine forecasts

๐Ÿ‘‹ What you get

Dublin Bay buoy readings + marine forecasts, on tap. Same dataset, two surfaces:

  • MCP endpoint โ€” for Claude Code, Cursor, or any LLM agent. 8 curated marine-data verbs; the server holds the key for you. Jump to MCP →
  • PostgREST API โ€” for apps, scripts, dashboards. REST + RPCs with the anon JWT. Details below.

Backed by 27k+ hourly buoy readings since 2023-01-01, 400k+ forecast rows across ECMWF IFS and EWAM (refreshed every 30 min), and 5 RPCs for the heavy queries. No sign-up for read access.

๐Ÿค– Use it as an MCP server

Point your MCP-capable client at https://mcp.dublinbaybuoy.com/ (streamable-HTTP transport) and you get 8 named marine-data tools. No signup, no token, no config surface โ€” the server holds the anon key for you.

Claude Code / Claude Desktop / Cursor / Windsurf

Add to ~/.claude.json (or a project-scoped .mcp.json โ€” same shape for Cursor, Windsurf, and other MCP-capable IDEs):

{
  "mcpServers": {
    "dublin-bay": {
      "type": "http",
      "url": "https://mcp.dublinbaybuoy.com/"
    }
  }
}

Or one-liner on the Claude Code CLI:

claude mcp add --transport http dublin-bay https://mcp.dublinbaybuoy.com/

Restart the client โ€” tools appear as mcp__dublin-bay__*.

Python MCP SDK

from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession

async with streamablehttp_client("https://mcp.dublinbaybuoy.com/") as (r, w, _):
    async with ClientSession(r, w) as s:
        await s.initialize()
        tools = await s.list_tools()
        result = await s.call_tool("get_current_conditions", {})
        print(result)

Tools exposed

ToolReturns
describe_dataMetadata: coverage, units, upstream sources, attribution. Call first to orient an LLM.
get_current_conditionsLatest hourly buoy row โ€” wind, gusts, waves, water temp, is_gale.
get_recent_readings(hours=24)Hourly series; auto-downsampled to daily means past 720 h.
get_summary(days=7)Means for wind / gust / wave / water temp over the window.
get_wind_rose(days=30)16-bucket compass histogram + prevailing direction.
get_recordsAll-time extremes since 2023-01.
get_forecast(variable, model, lead_hours)Forecast curves from 0 / 24 / 72 / 120 / 168 h ago โ€” visualise drift.
get_forecast_skill(variable, model, days, bucket_hours)MAE / RMSE / bias per lead bucket.

Forecast variables: wind_speed_10m, wind_gusts_10m, wind_direction_10m, wave_height, wave_period, wave_direction, sea_surface_temperature. Models: ecmwf_ifs025, ewam.

Try these prompts

  • “What's the wind doing in Dublin Bay right now?” โ†’ get_current_conditions
  • “How accurate is ECMWF's 72-hour wind forecast for Dublin Bay over the last month?” โ†’ get_forecast_skill
  • “What direction does the wind usually blow from here in winter?” โ†’ get_wind_rose

Why curated verbs, not raw SQL?

LLMs burn tokens and get creative with SQL. Named verbs like get_wind_rose(days=30) are cheaper, safer, and self-documenting. The server holds the anon key, so clients never rotate tokens. Read-only by design โ€” RLS on every table, SECURITY DEFINER on every RPC, no write paths exposed.

Self-host it

The source is ~200 lines of FastMCP + httpx, trivially portable to any Supabase project:

cd dublin-bay-bot/mcp
uv pip install --system -r requirements.txt
SUPABASE_URL=https://api.dublinbaybuoy.com \
SUPABASE_ANON_KEY=<anon key> \
PORT=8080 \
python server.py
This endpoint is read-only and anonymous. Need write access, raw SQL, or schema introspection? The underlying Supabase MCP at api.dublinbaybuoy.com/mcp is available with a service-role key โ€” email [email protected].

๐Ÿ”‘ Base URL & key

All endpoints are served from:

https://api.dublinbaybuoy.com/rest/v1/

Pass the public anon key on every request. It’s safe to embed in client code โ€” RLS limits it to SELECT on public tables:

apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NDU3MzAxLCJleHAiOjE5MzQxMzczMDF9.4ee1fobnsvjyRO14xKTi3rNhBv-UTeQRlQHMIp5Oqmw
The anon key is public by design, but heavy traffic will earn you a per-key quota. Email [email protected] before you build something big โ€” a dedicated Kong consumer takes two minutes.

โšก Quick start

Pick a language — every example is a single standalone copy-paste. No env vars, no setup.

Latest reading

curl -sS "https://api.dublinbaybuoy.com/rest/v1/readings?select=*&order=timestamp.desc&limit=1" \
  -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NDU3MzAxLCJleHAiOjE5MzQxMzczMDF9.4ee1fobnsvjyRO14xKTi3rNhBv-UTeQRlQHMIp5Oqmw" | jq
import httpx

ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NDU3MzAxLCJleHAiOjE5MzQxMzczMDF9.4ee1fobnsvjyRO14xKTi3rNhBv-UTeQRlQHMIp5Oqmw"

r = httpx.get(
    "https://api.dublinbaybuoy.com/rest/v1/readings?select=*&order=timestamp.desc&limit=1",
    headers={"apikey": ANON_KEY},
)
print(r.json())
// Paste into your browser's DevTools console, or a Node 18+ REPL.
const ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NDU3MzAxLCJleHAiOjE5MzQxMzczMDF9.4ee1fobnsvjyRO14xKTi3rNhBv-UTeQRlQHMIp5Oqmw";

const r = await fetch(
  "https://api.dublinbaybuoy.com/rest/v1/readings?select=*&order=timestamp.desc&limit=1",
  { headers: { apikey: ANON_KEY } },
);
console.log(await r.json());

Last 24 hourly readings

curl -sS "https://api.dublinbaybuoy.com/rest/v1/readings?select=timestamp,avg_wind,gust_speed,wave_height&order=timestamp.desc&limit=24" \
  -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NDU3MzAxLCJleHAiOjE5MzQxMzczMDF9.4ee1fobnsvjyRO14xKTi3rNhBv-UTeQRlQHMIp5Oqmw" | jq
r = httpx.get(
    "https://api.dublinbaybuoy.com/rest/v1/readings?select=timestamp,avg_wind,gust_speed,wave_height&order=timestamp.desc&limit=24",
    headers={"apikey": ANON_KEY},
)
print(r.json())
const r = await fetch(
  "https://api.dublinbaybuoy.com/rest/v1/readings?select=timestamp,avg_wind,gust_speed,wave_height&order=timestamp.desc&limit=24",
  { headers: { apikey: ANON_KEY } },
);
console.log(await r.json());

Wind rose — last 30 days

curl -sS -X POST "https://api.dublinbaybuoy.com/rest/v1/rpc/rpc_wind_rose" \
  -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NDU3MzAxLCJleHAiOjE5MzQxMzczMDF9.4ee1fobnsvjyRO14xKTi3rNhBv-UTeQRlQHMIp5Oqmw" \
  -H "Content-Type: application/json" \
  -d '{"days": 30}' | jq
r = httpx.post(
    "https://api.dublinbaybuoy.com/rest/v1/rpc/rpc_wind_rose",
    headers={"apikey": ANON_KEY},
    json={"days": 30},
)
print(r.json())
const r = await fetch(
  "https://api.dublinbaybuoy.com/rest/v1/rpc/rpc_wind_rose",
  {
    method: "POST",
    headers: { apikey: ANON_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ days: 30 }),
  },
);
console.log(await r.json());

Latest ECMWF wind-speed forecast curve

curl -sS -X POST "https://api.dublinbaybuoy.com/rest/v1/rpc/rpc_forecast_drift_snapshots" \
  -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NDU3MzAxLCJleHAiOjE5MzQxMzczMDF9.4ee1fobnsvjyRO14xKTi3rNhBv-UTeQRlQHMIp5Oqmw" \
  -H "Content-Type: application/json" \
  -d '{"variable_name": "wind_speed_10m", "model_name": "ecmwf_ifs025"}' | jq
r = httpx.post(
    "https://api.dublinbaybuoy.com/rest/v1/rpc/rpc_forecast_drift_snapshots",
    headers={"apikey": ANON_KEY},
    json={"variable_name": "wind_speed_10m", "model_name": "ecmwf_ifs025"},
)
print(r.json())
const r = await fetch(
  "https://api.dublinbaybuoy.com/rest/v1/rpc/rpc_forecast_drift_snapshots",
  {
    method: "POST",
    headers: { apikey: ANON_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      variable_name: "wind_speed_10m",
      model_name: "ecmwf_ifs025",
    }),
  },
);
console.log(await r.json());

๐Ÿ—‚๏ธ Tables

Anonymous SELECT is allowed on these tables. Use standard PostgREST query syntax โ€” select, order, limit, range filters like timestamp=gte.2026-01-01.

TableRowsContents
readings~27kHourly buoy observations โ€” avg_wind, gust_speed, wind_dir, wave_height, wave_period, water_temp, is_gale
forecast_log~400kRolling forecast snapshots โ€” fetched_at_utc, target_at_utc, model, variable, value, lead_hours. 30-day TTL.
ais_readings~400Raw SignalK measurements from the buoy’s AIS meteo broadcasts

Units

  • Wind & gusts โ€” knots (buoy and forecast agree)
  • Direction โ€” degrees true (meteorological: from)
  • Waves โ€” metres / seconds
  • Temperature โ€” ยฐC
  • All timestamps โ€” UTC

๐Ÿงฎ RPCs

Server-side functions for the heavy stuff. Call with POST /rest/v1/rpc/<name> and a JSON body of arguments.

rpc_summary(days int)

Min/mean/max for every variable over the window. One-call dashboard feed.

rpc_wind_rose(days int default null)

Wind-direction histogram in 16 buckets of 22.5ยฐ, zero-padded. Suitable for polar charts. Omit days for the full archive.

rpc_readings_downsampled(hours int)

Bucketed averages for long time-series charts. Hourly buckets for up to 30 days (hours โ‰ค 720); daily buckets beyond that.

rpc_forecast_drift(target_hours int)

Each model’s single best forecast for a target lead-time (e.g. “what does each model say about the weather 72 h from now?”).

rpc_forecast_drift_snapshots(variable_name, model_name, requested_leads int[])

Multiple forecast runs at progressively older leads, each with its full hourly curve. Powers the Mini App’s drift visualisation โ€” plot how the forecast for the coming week has evolved.

rpc_forecast_skill(variable_name, model_name default null, window_days default 30) Beta

Joins forecast_log against readings at target_at_utc and returns MAE, RMSE and bias per 6-hour lead bucket. Direction variables use short-arc circular error. See the spec section below.

๐Ÿ“ˆ Forecast-skill endpoint (spec)

Answers: how well does ECMWF predict Dublin Bay wind 24 h out? 72 h? A week?

Request

POST /rest/v1/rpc/rpc_forecast_skill
{
  "variable_name":    "wind_speed_10m",  // or wind_direction_10m, wave_height, ...
  "model_name":       "ecmwf_ifs025",    // optional โ€” null = both models
  "window_days":      30,                // lookback, default 30
  "lead_bucket_hours": 6                 // bucket width, default 6
}

Response

[
  {"model":"ecmwf_ifs025","variable":"wind_speed_10m","lead_bucket":0,  "n":168,"mae":1.02,"bias":-0.11,"rmse":1.38},
  {"model":"ecmwf_ifs025","variable":"wind_speed_10m","lead_bucket":24, "n":167,"mae":1.84,"bias":-0.22,"rmse":2.41},
  {"model":"ecmwf_ifs025","variable":"wind_speed_10m","lead_bucket":72, "n":165,"mae":3.12,"bias":+0.04,"rmse":3.95},
  ...
]

How it’s computed

  • Every forecast row in the window whose target_at_utc matches an observed readings.timestamp becomes a (forecast, observed) pair.
  • Pairs are grouped by lead_bucket = floor(lead_hours / lead_bucket_hours) * lead_bucket_hours.
  • MAE = mean of |forecast โˆ’ observed|. For direction variables, short-arc distance: min(|fโˆ’o|, 360โˆ’|fโˆ’o|).
  • Bias = mean signed error. For direction, signed short-arc (NULL is returned instead when circular-bias is meaningless).
  • RMSE = โˆš(mean of errยฒ) using the same error definition as MAE.
  • Anon-readable, stable, 30-day rolling lookback default. Full spec in supabase/migrations/20260423140000_rpc_forecast_skill.sql.

Variable โ†” observation mapping

forecast variablereadings column
wind_speed_10mavg_wind
wind_gusts_10mgust_speed
wind_direction_10mwind_dir
wave_heightwave_height
wave_periodwave_period
sea_surface_temperaturewater_temp
wave_directionno buoy counterpart โ€” not yet supported

โš–๏ธ Fair use & attribution

  • Soft limit: ~60 requests/min on the anonymous key. Burst is fine; sustained load gets noticed.
  • Please credit: “Data from Dublin Bay Buoy (dublinbaybuoy.com), sourced from Irish Lights MetOcean and Open-Meteo”.
  • Data is provided as-is. Not for navigation.
  • Questions, quota requests, or issues โ€” [email protected].