Swellread
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Swellreadis it worth driving to Trestles today, and which hours?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Swellread
Read the water before you paddle out.
One explainable, sealed verdict per surf break — from real swell, wind and verified tide predictions.
Live app · GitHub · API · Agent · Issues
I built this for someone who drives two hours to a reef pass and is disappointed by a forecast they cannot read. Swellread answers one question honestly: given real swell, wind and verified tide, is it worth driving to this break today, and at which hours? Every number that moves the verdict is shown, and every plan you keep is sealed so you can prove later what the water was.
The control above is the whole product in one gesture. Peel speed is shallow-water celerity, c = √(g·depth) — so dragging the tide re-cuts the seabed, the peeling section re-solves, and the shared engine re-runs on the server and returns a new seal. Same 1.66 m swell: 12 km/h on a low tide, 21 km/h on a high one. Unridable versus the best thing on the coast.
✨ Features
What you get | Why it matters |
A verdict for every hour, not one number for the day |
|
Six weighted factors, each showing its arithmetic | Swell power, peel speed, wind quality, tide window, direction match, period cleanliness. A factor with no real data is dropped and labelled, never quietly scored at zero. |
Hard gates that override the score | Below 0.35 m of swell the score is zero, because there is nothing to break on. Below a reef's minimum safe depth it is clamped to 0.08, because that is a hazard rather than a score. |
A tide drag that really re-runs the engine | The signature interaction. It POSTs a what-if to |
A playable WebGL wave | A Gerstner surface built from the same height, period and heading as the rest of the app, breaking where H/d = 0.78, peeling at the engine's own speed. Steer with A/D, pump with W, kick out with Space. |
Sealed plans you can hand to someone else | Every session freezes its conditions and verdict, and appends a SHA-384 link to a per-entity chain. Export a printable brief, a share route, or the JSON. |
An MCP agent that can read and write | Eight typed tools over JSON-RPC 2.0. Four mutate through the exact same service layer as the web UI, and |
An open-weights model that never leaves your device |
|
Honest degradation | Where no NOAA tide station is in range, the tide factor is dropped and the remaining weights are renormalised — and the page says so. Where every upstream is down, a sealed dated sample is shown and labelled |
Related MCP server: Nexxupp MCP Server
🚀 Quickstart
git clone https://github.com/aniruddhaadak80/swellread
cd swellread
npm ci
npm run devThat is the whole setup. Zero required environment variables. With nothing configured, Swellread runs
on an embedded PGlite database at ./.data/pglite, seeds its break catalogue idempotently on first run,
and talks to the real NOAA and Open-Meteo APIs.
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm test # vitest — 137 deterministic tests
npm run build # next build
npm run check # all three gates in sequenceProduction environment variables
Variable | Required | Purpose |
| yes in production | A hosted Postgres connection string (Neon, Vercel Postgres, Supabase — anything speaking |
| no | Absolute URL for canonical tags, OpenGraph and the sitemap. Vercel sets this itself. |
| no | Deliberately permits the embedded store in production. Set it only for preview deploys; data will not survive a redeploy. |
Without DATABASE_URL in production, Swellread refuses to start rather than silently accepting writes
that vanish on the next cold start. See .env.example.
🔌 API
Everything the interface can do, over plain HTTP. All responses are JSON; errors are always
{ "error": { "code", "message" } }.
Read a break's live conditions
curl -s "https://swellread.vercel.app/api/conditions?breakId=pago-pago&hour=8" | head -c 400{
"status": "live", // "live" | "mixed" | "fallback" — never hidden
"snapshot": {
"date": "2026-10-03",
"hours": [ /* 24 normalised hourly rows */ ],
"sources": [
{ "id": "open-meteo-marine", "status": "live", "stale": false, "url": "https://open-meteo.com/…" },
{ "id": "noaa-coops", "status": "live", "stale": false, "url": "https://tidesandcurrents.noaa.gov/…" }
]
},
"points": [ { "localLabel": "08:00", "score": 0.68, "band": "worth-the-drive", "tideM": 0.17 } ]
}The engine, and the tide what-if
# the real verdict for one hour
curl -s "https://swellread.vercel.app/api/engine?breakId=pago-pago&hour=8"
# "what if the tide were 2.4 m?" — never writes anything back
curl -s -X POST "https://swellread.vercel.app/api/engine" \
-H "content-type: application/json" \
-d '{"breakId":"pago-pago","hour":8,"tideM":2.4}'{
"whatIf": { "tideM": 2.4 },
"engine": {
"version": "swellread-engine/2026.10.1",
"score": 0.68,
"band": "worth-the-drive",
"factors": [
{
"key": "peelSpeed", "available": true, "raw": 21.1, "rawLabel": "21.1 km/h",
"weight": 0.2, "effectiveWeight": 0.2, "contribution": 0.1806,
"evidence": "c = sqrt(g · depth) = sqrt(9.81 · 4.60 m) = 21.1 km/h over a 4.60 m take-off…"
}
],
"gates": [],
"derived": { "breakHeightM": 0.9, "peelSpeedKmh": 21.1, "powerKwM": 14.1, "depthAtBreakM": 4.6 },
"window": { "bestStart": "…", "goodHours": ["…"] },
"seal": "700589816acc…"
}
}A mutation, then read it back
BASE=https://swellread.vercel.app
# 1. create (the response carries the first audit seal)
ID=$(curl -s -X POST "$BASE/api/sessions" \
-H "content-type: application/json" \
-d '{"breakId":"pago-pago","plannedFor":"2026-10-03T18:00:00.000Z","status":"committed","idempotencyKey":"demo-1"}' \
| sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
echo "created $ID"
# 2. read back
curl -s "$BASE/api/sessions/$ID" | head -c 300
# 3. update — bumps the version and appends a sealed event
curl -s -X PATCH "$BASE/api/sessions/$ID" \
-H "content-type: application/json" -d '{"call":"in","confidence":4}'
# 4. log a ride from the WebGL lab
curl -s -X POST "$BASE/api/sessions/$ID/rides" \
-H "content-type: application/json" \
-d '{"durationSec":210,"longestRideSec":42,"topSpeedKmh":28.4,"completed":true}'
# 5. replay the integrity chain
curl -s "$BASE/api/integrity/$ID" | head -c 300
# 6. export the brief as a file
curl -s "$BASE/api/brief/$ID?format=json" -o brief.json
# 7. soft delete — keeps a tombstone so the chain still replays
curl -s -X DELETE "$BASE/api/sessions/$ID"Health
curl -s https://swellread.vercel.app/api/health{
"ok": true,
"store": { "kind": "neon", "productionStore": true, "label": "Hosted Postgres (Neon serverless driver)" },
"persistence": { "probe": "SELECT 1 succeeded; sessions table holds 12 row(s)" }
}The probe is a real query, not a static object. See verifying a deployment.
🧠 Agent interface
Live JSON-RPC 2.0 over HTTP at /api/mcp. GET on that URL returns a discovery document; POST
accepts a single message or a batch. A published manifest lives at public/mcp.json.
curl -s https://swellread.vercel.app/api/mcp \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'{
"jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2025-06-18",
"serverInfo": { "name": "Swellread", "engineVersion": "swellread-engine/2026.10.1" },
"capabilities": { "tools": { "listChanged": false } }
}
}Tool | Kind | What it does |
| read | The catalogue, including which tide station (if any) is in range. |
| read | Normalised 24-hour slice with full attribution. |
| analysis | The sealed verdict, every factor, gates and the day's window. |
| read | Recomputes a session's chain and names the first broken link. |
| mutating | Freezes conditions + verdict. Idempotent on |
| mutating | Records an in/out call and confidence. |
| mutating | Same write path the lab's save button uses. |
| mutating | Soft delete, tombstone retained. |
Configuring an MCP client:
{
"mcpServers": {
"swellread": {
"type": "http",
"url": "https://swellread.vercel.app/api/mcp"
}
}
}Every mutating tool is scoped to the anonymous HTTP-only cookie of the browser it was called from. An agent cannot read or delete another rider's session, because it has no way to name one. Try it yourself in the in-page agent console — one click per tool, with the exact request and response shown.
📁 Project map
User routes
Route | What it is for |
| Today's real verdict for the hero break, the tide ribbon, and the three jobs this does. |
| The 14-break catalogue, filterable by what each one can and cannot tell you. |
| Dynamic. Live 24-hour slice, the factor table with its arithmetic, and the tide-drag cross-section. |
| Your workspace. Status filter and paging live in the URL, so a filtered view is a shareable link. |
| Dynamic. Frozen conditions, decide/annotate/delete, the on-device feel match, rides, and the full audit chain. |
| The WebGL wave lab. Open from a session to attach rides to it. |
| Live MCP JSON-RPC console with one-click calls for all eight tools. |
| Build the handover brief, with a real JSON download. |
| Units, the bundled model, ownership, the store, and the security model. |
| Replay any chain from its id. |
| Every formula, weight, gate and boundary case, written down. |
| Dynamic. The stable handover route. Anyone with the link can read it. |
API routes
Route | Methods | Responsibility |
| GET | Real persistence probe against the configured store. |
| GET | The catalogue, no upstream call. |
| GET | Break + live slice + engine verdict + the day's scores. |
| GET | Normalised conditions with attribution and honest status. |
| GET, POST | The verdict for a real hour; POST takes a what-if tide. |
| GET, POST | List and create. Idempotency supported. |
| GET, PATCH, DELETE | Read, update, soft delete. |
| POST | Log a ride. |
| GET | Recompute and verify the chain. |
| GET | The exportable brief. |
| GET, POST | MCP discovery document and JSON-RPC endpoint. |
Library
Path | Responsibility |
| The only place the arithmetic lives. Called by pages, REST and MCP alike. |
| Tide interpolation, extrema detection, trend, depth. |
| Clamp, angle deltas, smoothstep, defensive number parsing. |
| Canonical JSON and the SHA-384 seal chain. |
| Marine + forecast, with pure normalisers that are unit-tested against captured real payloads. |
| Tide predictions. Handles both NOAA response shapes. |
| The sealed offline sample. Never presented as live. |
| The curated catalogue, with tide stations only where NOAA publishes them. |
| Store selection, schema, seeding, and the two-driver executor. |
| Sessions, rides and audit chains. One statement per mutation. |
| The service layer the UI, REST and MCP all share. |
| In-browser transformers.js with the committed model. |
| Tools, schemas, and JSON-RPC error mapping. |
🏗 Architecture
graph LR
Browser["Visitor"]:::live
Pages["Server pages<br/>landing, breaks, sessions"]:::ai
Client["Client islands<br/>tide drag, lab, console"]:::live
API["REST routes"]:::agent
MCP["MCP JSON-RPC"]:::agent
Service["Service layer<br/>one write path"]:::ai
Engine["Engine<br/>scoreHour"]:::ai
DB["Postgres<br/>Neon in prod"]:::risk
Feed["NOAA + Open-Meteo<br/>keyless"]:::warn
Audit["Audit chains<br/>SHA-384"]:::agent
Model["MiniLM<br/>in the tab"]:::ai
Browser --> Pages
Browser --> Client
Client --> API
Browser --> MCP
API --> Service
MCP --> Service
Service --> Engine
Service --> DB
Service --> Audit
Engine --> Feed
Service --> Feed
Browser --> Model
Engine -.->|seal| Audit
API --> Service
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef warn fill:#fbbf24,stroke:#b45309,color:#451a03
classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519The point of the middle column: the UI, the REST routes and the MCP tools cannot diverge, because none of them talk to the database directly.
User journey
graph TB
Land["Open today"]:::live
Pick["Pick a break and an hour"]:::live
Drag["Drag the tide<br/>see the peel change"]:::ai
Commit["Commit the window"]:::agent
Inspect["Inspect the frozen<br/>snapshot and factors"]:::ai
Ride["Ride it in the lab<br/>log the ride"]:::live
Recall["Feel match<br/>find earlier sessions"]:::ai
Brief["Export the brief"]:::agent
Share["Hand it to a friend"]:::agent
Delete["Delete, keeping<br/>a replayable tombstone"]:::risk
Land --> Pick --> Drag --> Commit --> Inspect
Inspect --> Ride --> Recall --> Brief --> Share
Inspect --> Delete
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519🌊 Data provenance and the fallback
graph TB
NOAA["NOAA CO-OPS<br/>predictions, MLLW"]:::warn
Meteo["Open-Meteo<br/>marine + forecast"]:::warn
Norm["Normalise<br/>nested hourly arrays"]:::ai
Join["Join on the UTC hour"]:::ai
Tally["Hourly slice<br/>+ tide rate"]:::ai
Status["Status per source<br/>live or fallback"]:::agent
Used["Snapshot used<br/>by pages and MCP"]:::live
Sample["Sealed sample<br/>dated, synthetic"]:::risk
Drop["Drop the factor,<br/>renormalise"]:::risk
Engine["Engine"]:::ai
NOAA --> Norm
Meteo --> Norm
Norm --> Join --> Tally --> Status --> Used
Norm -.->|all upstreams failed| Sample --> Used
Join -.->|no station in range| Drop --> Engine
Used --> Engine
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef warn fill:#fbbf24,stroke:#b45309,color:#451a03
classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519Two keyless public feeds, both attributed on every page that shows their numbers:
Source | Used for | Access | Licence |
Hourly tide predictions, MLLW datum |
| U.S. Government public domain | |
Swell height, period, bearing; wind-wave | No key | CC BY 4.0 | |
Wind speed and bearing, air temperature, cloud, rain | No key | CC BY 4.0 |
The honest parts, which matter more than the features:
product=water_levelis the observed product and stops at "now". The app deliberately usesproduct=predictionsfor forecasts, because a surf forecast needs the future.Both Open-Meteo endpoints nest their arrays under
hourly. Reading the wrong level fails silently — the request succeeds, the arrays are missing, and you serve your offline sample while believing you are live. That bug existed in this app; the checked-in verifier caught it. There are now tests against captured real response shapes so it cannot come back.NOAA's two products do not share a response shape either: the observed one returns
data, the forecast one returnspredictions. The normaliser accepts both.Where a break has no NOAA station in range — 6 of the 14 here, including Kovalam and Arugam Bay — the tide factor is dropped, the remaining weights are renormalised, and the attribution row says "No NOAA CO-OPS station is in range for this break". It never invents a tide, and it never fabricates one in the offline sample either.
When every upstream is unreachable, a sealed, dated, synthetic profile is shown and the snapshot is labelled
fallback. It is never presented as today's water, and it never replaces anything you created.Break coordinates describe real coastlines. Peel orientation, reef slope and take-off depth are this project's own editorial estimates and are labelled as estimates wherever they appear.
🧮 The engine
graph TB
In["Hour + break"]:::live
Phys["Green's law<br/>Hb = Hs0·(γ·tanβ)^¼"]:::ai
Cap["Cap by water<br/>min Hb, γ·depth"]:::ai
Peel["Peel speed<br/>c = √(g·depth)"]:::ai
Pow["Power<br/>⅟₁₆ρgHs²Tp"]:::ai
Wind["Wind alignment<br/>× speed envelope"]:::ai
Dir["Direction match"]:::ai
Per["Period cleanliness"]:::ai
Renorm["Renormalise over<br/>available factors"]:::ai
Sum["Weighted score"]:::ai
Gates["Hard gates<br/>flat, exposed, blown out"]:::risk
Band["Band + seal"]:::agent
Window["Best window"]:::agent
In --> Phys --> Cap --> Peel
In --> Pow
In --> Wind
In --> Dir
In --> Per
Peel --> Renorm
Pow --> Renorm
Wind --> Renorm
Dir --> Renorm
Per --> Renorm
Renorm --> Sum --> Gates --> Band --> Window
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519Nominal weights: swell power 0.22, peel speed 0.20, wind quality 0.20, tide window 0.18, direction match 0.12, period cleanliness 0.08 — summing to exactly 1, then renormalised over whatever has real data. Bands: ≥0.70 get in, ≥0.50 worth the drive, ≥0.30 marginal, else stay home.
Two formulas carry most of the weight, and both are real physics rather than tuning knobs:
Green's law on the reef face.
Hb = Hs0 · (γ·tanβ)^¼with γ = 0.78, the solitary-wave breaking index. Capped by the water you actually have, because a wave in water shallower thanHb/γhas already broken further out and is closing rather than standing up.Shallow-water celerity.
c = √(g·depth). This is the entire reason tide matters, and the reason the signature interaction exists.
Every result is sealed: seal = SHA-384(genesis ‖ canonicalJson(result body)), so the verdict you were
shown can be recomputed later. See /method for the full derivation
and every boundary case.
🔐 Integrity: the seal chain
graph TB
Create["session.create<br/>seq 1, prev = genesis"]:::agent
Update["session.decide<br/>seq 2, prev = seal 1"]:::agent
Ride["ride.log<br/>seq 3"]:::agent
Kill["session.delete<br/>tombstone kept"]:::risk
Can["canonicalJson<br/>sorted keys"]:::ai
Seal["seal_n = SHA-384<br/>prevSeal ‖ body"]:::ai
Chain["Append-only chain"]:::agent
Replay["Replay endpoint<br/>first broken link"]:::ai
Verify["verify_integrity<br/>via MCP too"]:::agent
Create --> Update --> Ride --> Kill
Create --> Can --> Seal
Update --> Can
Ride --> Can
Kill --> Can
Seal --> Chain
Chain --> Replay --> Verify
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519seal_n = SHA-384( UTF-8(prevSeal) || canonicalJson(event_n) )
genesis = 96 zerosCanonical JSON sorts object keys recursively by UTF-16 code unit and preserves array order, so the same
event always produces the same bytes whichever client wrote it. seal and prevSeal are stored beside
the body, never inside it, so rewriting a stored seal cannot be laundered by re-hashing.
Every mutation is one SQL statement with the entity change and the audit insert gated on the same
target CTE, so the row and its seal land together or not at all. Deleting a session is a soft delete that
keeps a tombstone, so the history stays replayable forever.
🕹 The WebGL wave lab
graph LR
Real["Real Hs, Tp, direction"]:::live
Break["Depth from tide"]:::live
Gerstner["Gerstner sum<br/>4 components"]:::ai
Shoal["Shoal where shallow"]:::ai
Break2["Break where H/d ≥ 0.78"]:::ai
Foam["Foam behind the peel"]:::warn
Peel3["Peel at c = √(g·depth)"]:::ai
You["You: steer, pump, kick out"]:::agent
Ride["Ride result"]:::agent
Save["POST /rides"]:::agent
Real --> Gerstner
Break --> Gerstner
Gerstner --> Shoal --> Break2 --> Foam
Break2 --> Peel3
Peel3 --> You --> Ride --> Save
You --> Gerstner
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef warn fill:#fbbf24,stroke:#b45309,color:#451a03The seabed profile in the shader is the same shape as the cross-section on the break page, so the wave only breaks where the water is genuinely shallow enough. Keyboard first: A/D steer up and down the face, W pumps, S stalls, Space kicks out. Get ahead of the section and it closes out; fall behind and you are in the foam.
It is a Gerstner surface, not a CFD simulation, and the riding is a score rather than a physics engine with a real surfer in it. Take the water model seriously and the gameplay lightly.
🤖 An agent writing a plan
graph TB
Agent["Any MCP client"]:::agent
Init["initialize"]:::agent
List["tools/list"]:::agent
Read["analyse_break<br/>read + seal"]:::ai
Create["create_session<br/>idempotent"]:::ai
Service2["Same service layer<br/>as the web UI"]:::agent
Row["Row + audit link"]:::agent
Read2["GET /sessions/:id"]:::live
Verify["verify_integrity"]:::agent
Clean["delete_session<br/>tombstone"]:::risk
Agent --> Init --> List --> Read
Read --> Create --> Service2 --> Row
Service2 --> Read2
Row --> Read2
Read2 --> Verify
Read2 --> Clean
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519Try it in the agent console — one click per tool, with the exact JSON-RPC request and response on screen, including the error codes.
🔒 Security model
No accounts, no secrets. The only production environment variable is a database connection string. There is no API key anywhere in this application, which is also why the model runs in your browser.
Anonymous ownership is an unguessable id in an HTTP-only, same-site cookie, minted by middleware. Every read, update and delete is scoped to it, and there is no code path that returns another rider's row — covered by tests, not by inspection.
Input is parsed with a schema before it reaches the service layer. Strings are length-capped, enums are closed sets, ids are pattern-checked, and every SQL statement is parameterised. There is no string interpolation of user input into a query anywhere in the repository.
Errors are a stable
{ code, message }envelope. Stack traces, driver messages and environment variables are never returned to a client.Abuse controls are in-memory sliding windows: 20 session creates/min, 40 updates/min, 30 rides/min, 20 deletes/min, 120 JSON-RPC calls/min per client. On serverless each isolate keeps its own map, so this is a speed bump, not a hard limit — put a platform WAF or a hosted limiter in front of the write routes for a real guarantee. This is stated plainly rather than implied.
Upstreams are exactly two fixed HTTPS hosts, a 9-second timeout and one retry. Payloads are normalised and re-validated in TypeScript, and no upstream response is rendered as HTML.
Full detail in SECURITY.md.
🚀 Deployment
graph LR
Push["Push to main"]:::live
CI["GitHub Actions<br/>typecheck, lint, test, build"]:::agent
Vercel["Vercel production"]:::agent
Env["DATABASE_URL"]:::warn
Neon["Hosted Postgres"]:::ai
Live["Public alias"]:::live
Verify["verify-live.mjs"]:::ai
Smoke["browser-smoke.mjs"]:::ai
Push --> CI --> Vercel
Env --> Vercel
Neon --> Env
Vercel --> Live
Live --> Verify
Live --> Smoke
Verify --> Push
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef warn fill:#fbbf24,stroke:#b45309,color:#451a03Verifying a deployment
Two checked-in scripts, both of which use real HTTP and no secrets:
# 36 assertions: health, live data, CRUD, idempotency, engine, MCP, integrity,
# delete, the repository link in nav and footer, and every primary route.
SWELLREAD_BASE_URL=https://swellread.vercel.app npm run verify
# 14 assertions through real visible controls: the tide drag, committing a
# session, deciding, the brief, the agent console, delete, keyboard focus,
# the WebGL ride loop, mobile, reduced motion — and zero console errors.
SWELLREAD_BASE_URL=https://swellread.vercel.app node scripts/browser-smoke.mjsThe browser script needs a Chromium: npx playwright install chromium.
🧭 Roadmap
Now — shipped
graph TB
A["Keyless NOAA + Open-Meteo"]:::live
B["Six-factor explainable engine"]:::ai
C["Tide drag + cross-section"]:::live
D["WebGL ride lab"]:::live
E["Sealed sessions + brief"]:::agent
F["MCP with 8 tools"]:::agent
G["In-browser MiniLM"]:::ai
H["137 tests + 2 verifiers"]:::agent
A --> B --> C --> D
B --> E --> F
E --> G
E --> H
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22Live verdicts per break and per hour, with the tide ribbon as the spine of every page.
The six-factor engine, hard gates, and a SHA-384 seal on every result.
The tide-drag cross-section and a playable WebGL wave built from the same data.
Sealed sessions, a printable brief, a share route, and a JSON download.
Eight MCP tools, four of them mutating, over the same service layer.
An open-weights embedding model committed to this repository and run in the tab.
137 tests, a 36-assertion live verifier, and a 14-assertion browser journey.
Next — the obvious things
graph TB
A["Wider break coverage<br/>more tide stations"]:::live
B["Riders you can trust<br/>per-user feel model"]:::ai
C["Saved watchlists<br/>alert me when it hits"]:::agent
D["Buoy observations<br/>blend measured with modelled"]:::warn
E["WebGPU renderer<br/>for bigger meshes"]:::ai
F["Export the brief as a<br/>signed image card"]:::agent
A --> C
B --> C
D --> A
E --> D
C --> F
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef warn fill:#fbbf24,stroke:#b45309,color:#451a03More coastlines, more tide stations. The honest gap right now is NOAA's Pacific coverage, not the code. A second keyless tide source would widen it.
A feel model per rider. Once you have twenty sessions with vectors, the "which conditions do you actually like" question becomes a ranking rather than a guess.
Watchlists that tell you when it hits, so the decision arrives before the drive, not after it.
Buoy observations blended with the model, so a real measurement can outvote a forecast.
A WebGPU renderer, to push mesh resolution and get the reef into the frame honestly.
Later — the honest long game
graph TB
A["Instrument the reef<br/>crowd and hazard context"]:::risk
B["Regional ensembles<br/>probability, not a verdict"]:::ai
C["Pass to pass<br/>session hand-over"]:::live
D["Open dataset<br/>of sealed sessions"]:::agent
E["Local-first offline mode<br/>sync when you surface"]:::ai
A --> B
C --> D
B --> D
D --> E
classDef live fill:#22d3ee,stroke:#0e7490,color:#083344
classDef ai fill:#a78bfa,stroke:#6d28d9,color:#2e1065
classDef agent fill:#34d399,stroke:#047857,color:#022c22
classDef risk fill:#fb7185,stroke:#be123c,color:#4c0519Crowd and hazard context — the part of the decision this app deliberately does not make, and the part it most obviously should not make alone.
Regional ensembles: probability of a window rather than a single verdict, because a single number about the ocean is always slightly dishonest.
Pass to pass: handing a session to whoever is paddling out next, which is what the seal chain was quietly built for.
An open dataset of sealed sessions, so the feel model can be learned outside this app.
Local-first offline mode, because a forecast tool that needs a network is a forecast tool that fails in exactly the places the forecast matters.
⚠️ Safety
Swellread describes the surface of the ocean. It does not know what is happening underneath it, and it
cannot see your ability, your fitness, the state of the reef, the current, or a rips. It is a planning aid
for deciding when, never a safety guarantee, and the app says so on every page that shows a verdict. The
reef-exposed gate exists because running out of water over a reef is a hazard rather than a low score.
📄 Attribution
Wave and wind data from Open-Meteo (CC BY 4.0). Tide predictions from
NOAA CO-OPS (U.S. Government public domain). Sentence-embedding
weights are all-MiniLM-L6-v2 (Apache-2.0),
committed under public/models/. Built with Next.js,
three.js, PGlite,
transformers.js and
Vercel.
🤝 Contributing
Please read CONTRIBUTING.md — the short version is that a factor change needs the physics and the tests, a data source needs attribution and a bounded timeout, and a new break only gets a tide station if NOAA genuinely publishes one.
Issues and pull requests are welcome: github.com/aniruddhaadak80/swellread
📄 Licence
MIT — see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Context and control for any AI. Agent identity, scoped access, and proof across tools.
Gateway between LLM agents and world data through eight tools and a bundled endpoint catalog.
AI Agent Utility Layer for agent safety, web monitoring, crypto research and tool discovery.
Web search, point-in-time crypto market data, verified strategy track records and 160+ agent tools.
461
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables language models to run command-line tools and access calendars, Gitea repositories, notifications, and weather through typed HTTP endpoints.1MIT

Nexxupp MCP Serverofficial
AlicenseBqualityCmaintenanceEnables AI agents to store and search private memory, explore and edit workspace stations, databases, tasks, and charts, and schedule calendar events through natural language. Supports local stdio and hosted HTTP modes with per-user OAuth scoping of tools.22MIT- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to discover and create agents, durably submit messages with verifiable replies, and search source-linked local memory. Exposes eight authenticated tools with owner-approved OAuth, transcript-backed verification, and explicit control over history import and embedding.MIT
- AlicenseNot gradedqualityCmaintenanceEnables agents to query live NOAA/USGS space-weather and earthquake feeds, compute deterministic storm-risk assessments with hash-chained integrity seals, and verify data authenticity via JSON-RPC tools.MIT