Ask whether a coastal spot is good for snorkeling or tidepooling and what you'll likely see β or describe (or photograph) a creature you saw and let ReefScout identify it and check whether that species can actually be there. Then keep a personal logbook of everything you've spotted.
ReefScout is an agentic application: a Claude model decides which tools to call, chains them based on the results, and verifies its own answers against authoritative marine data. The tools are exposed through a custom MCP server and backed by free, no-API-key live ocean data.
Who it's for: casual-to-intermediate snorkelers, tidepoolers, and shore divers planning an outing and logging what they see. Β Β·Β π Submission write-up: WRITEUP.md answers the six required questions in one place.
βΆοΈ Live app: https://reefscout.onrender.com
Free tier β if it's been idle it may take ~30β60 s to wake on the first request.
- Plan a trip. "Is Saturday morning good for snorkeling at La Jolla Cove, and what might I see?" β live waves/swell/water-temp, tide timing, and the species you're realistically likely to encounter, with a clear go / caution / skip verdict.
- Identify a sighting. "I saw a small bright-orange fish with white stripes on the reef" β by text or photo β β candidate species, verified against where they actually occur, with a reference photo and conservation status.
- Keep a logbook. Sign in with Google to save your conversations and build a life-list of sightings (auto-enriched with scientific name, taxon group, conservation status, and a photo) plus a trip log. One tap from any identification adds it to your logbook.
Identifying marine life from text alone is genuinely hard: "small, mostly blue with some green" fits hundreds of species. So ReefScout is multimodal in both directions β which is unusual for a chat app, and the heart of why it works for ID:
- You send a photo. Snap the creature and attach it; the vision model identifies from the actual image β color, pattern, fin shape, habitat in frame β not a vague description. (Photos are downscaled in the browser before upload to keep it fast and cheap.)
- ReefScout sends photos back. For every candidate species it proposes, it fetches a real reference photo (iNaturalist, with photographer attribution) so you can eyeball the match β "does it look like this?" β instead of trusting a name you can't picture.
Above: a text description of a blue-green fish seen off Hawaii. ReefScout proposes the Saddle Wrasse (Thalassoma duperrey) β likely β and the Redlip Parrotfish (Scarus rubroviolaceus) β possible, each with a reference photo to compare against. That photo-in / photos-out loop is exactly how a field guide is used, and it's why ReefScout beats a text-only bot at this task.
Browser (single-file Liquid-Glass UI)
β ββ POST /chat ββββββββββββββββΊ FastAPI βββΊ Claude (Anthropic API)
β β β agent loop: read tool_use β
β β β dispatch over MCP β feed result back β repeat
β β βΌ
β β MCP server (FastMCP, stdio) ββ our 7 tools
β β β
β β βΌ
β β Live data (free, no key): Open-Meteo Β· NOAA CO-OPS Β·
β β iNaturalist Β· WoRMS
β ββ GET /species/resolve βββββββΊ FastAPI (WoRMS) β enrich a logbook sighting
β ββ GET /firebase-config.js βββΊ FastAPI (env vars) β public Firebase config
β ββ Firebase Auth + Firestore βββ directly from the browser (history + logbook)
The agent backend and the persistence layer are deliberately decoupled: the browser talks to Firestore directly (gated by security rules), so the FastAPI/agent service never holds Firebase credentials and the agent never depends on the database.
| File | Role |
|---|---|
app/ocean_data.py |
Live data layer β async functions over Open-Meteo, NOAA, iNaturalist, WoRMS, each normalized to a clean dict. No LLM logic. |
app/mcp_server.py |
Our FastMCP server exposing 7 tools (name + description + input schema each). The tools/data β agent boundary. |
app/agent.py |
The agentic core: an MCP client + a manual Claude tool-call loop that returns the answer plus a tool-call trace. |
app/prompts.py |
Versioned system prompts (v1 β v2 β v3). |
app/main.py |
FastAPI: serves the UI, /chat, /species/resolve, /firebase-config.js, /health. |
static/index.html |
The entire frontend β chat, logbook, auth β in one file, no build step. |
| Tool | Source | Does |
|---|---|---|
geocode_place |
Open-Meteo | place name β coordinates |
get_marine_conditions |
Open-Meteo Marine | wave height, swell, sea-surface temp for a date |
get_tides |
NOAA CO-OPS | high/low tides at the nearest station |
get_species_nearby |
iNaturalist | real recent sightings near a coordinate |
get_species_images |
iNaturalist | reference photos (with attribution) |
search_marine_taxa |
WoRMS | name/description β candidate species |
get_species_detail |
WoRMS | taxonomy, distribution, conservation status |
The model β not our Python β chooses which tools to call, in what order, and when to stop. Remove the LLM and replace it with a lookup table and the behavior collapses: the branching, the decision to verify an ID against location, and the synthesis all live in the model's responses. Our code only provides the tools and the loop.
This layer is client-side and fully decoupled from the agent: the browser talks to Firebase directly, so the FastAPI/agent service holds no Firebase credentials.
- Auth + data: Firebase Auth (Google sign-in) + Cloud Firestore. Each user reads/writes only
their own
users/{uid}/conversations,/sightings, and/trips, enforced byfirestore.rules. Signed-out use works but is ephemeral. - Config injection: the public Firebase web config is served at runtime by
GET /firebase-config.jsfromFIREBASE_*env vars β never committed to the repo. - Logbook enrichment: the "log a sighting" form asks only for name/place/date/notes;
GET /species/resolvethen fills in the scientific name, taxon group, conservation status, and a reference photo from WoRMS + iNaturalist (no LLM, no key). A one-tap "Add to logbook" button on any identification pre-fills it from the chat. - Photos: each sighting gets a default reference photo, or the user's own upload β downscaled client-side and stored as a compact image inside the sighting's Firestore doc (one per doc, under the 1 MB limit), so no Firebase Storage (and no billing upgrade) is required.
User: "Is Saturday morning good for snorkeling at La Jolla Cove, and what might I see?"
Tool calls the model made (shown live in the UI's trace panel):
geocode_place({"place": "La Jolla Cove"}) β no match
geocode_place({"place": "La Jolla"}) β 32.847, -117.274 (retried with a simpler name)
get_marine_conditions(32.847, -117.274, "2026-06-20")
get_tides(32.847, -117.274, "2026-06-20")
get_species_nearby(32.847, -117.274, iconic_taxa="Actinopterygii,Mollusca,Echinodermata,Cnidaria")
get_species_images("Hypsypops rubicundus") β reference photos for the
get_species_images("Hermissenda opalescens") β highlight species β fetched
get_species_images("Flabellinopsis iodinea") β unprompted
get_species_images("Octopus bimaculoides") β
Reply:
Marginal β waves are on the high side; best window is around the morning low tide at 8:26 AM.
Conditions β Saturday, June 20
Max wave height 1.26 m (4.1 ft) Wave period 13.5 s (long-period swell β sets feel bigger) Water temp ~20.6 Β°C (69 Β°F) β wetsuit recommended Tides (La Jolla Scripps station, 2.7 km away) β the 8:26 AM low is exceptionally low (~0.02 ft MLLW); aim to be in the water 7:30β9:30 AM.
What you're likely to see (719 species logged nearby): π Garibaldi (Hypsypops rubicundus), π΅ Opalescent Nudibranch, π£ Spanish Shawl, π Lesser Two-spot Octopus β each with a reference photo + attribution.
β οΈ The 1.26 m / 13.5 s swell is on the rough side. Check the lifeguard station, watch a few sets before entering, and recheck the forecast Friday evening.
Note the agentic moves: it recovered from a failed geocode on its own, chained four data tools, and fetched a photo per highlight species without being told to.
User: "I think I saw a clownfish like Nemo while snorkeling at La Jolla yesterday. Cool right?"
Tool calls:
geocode_place({"place": "La Jolla"})
search_marine_taxa({"query": "clownfish"}) β reason to a candidate, look it up
get_species_nearby(32.847, -117.274, iconic_taxa="Actinopterygii") β does it occur HERE?
get_species_detail({"aphia_id": 212783}) β range / environment check
get_species_images("Hypsypops rubicundus") β photo of the real candidate
Reply (trimmed):
Unlikely β no clownfish live in La Jolla waters. Clownfish (Amphiprion) are tropical Indo-Pacific reef fish; La Jolla is a temperate California kelp forest, and there are zero iNaturalist observations of clownfish near it.
The much more likely culprit: the Garibaldi (Hypsypops rubicundus) β La Jolla's #2 most-observed fish, a big brilliant-orange kelp-forest fish. (+ photo & attribution) Does that match what you saw β solid orange, no white stripes?
This is the distinctive behavior: the model didn't just accept the ID β it checked the claim against where the species actually occurs, refuted it with evidence, and proposed a location-appropriate alternative. Across the two examples, all 7 MCP tools are exercised.
eval/run_eval.py defines "good" as three executable dimensions β agentic
correctness (the model calls the right tools per question type, asserted on the trace, including
a negative case), grounded answers (dates resolve, verdict-first, dual units), and honesty at
the edges (an out-of-range ID is challenged; off-topic is declined with zero tool calls).
9 structured cases, 9/9 passing. An earlier run scored 7/8 β the documented failure drove
system-prompt v2. Results and run history: eval/RESULTS.md. Run it with
python -m eval.run_eval.
There are also fast integration tests: pytest (14 tests over the data layer, MCP server, and API).
git clone https://github.com/justinswork/ReefScout && cd ReefScout
python -m venv venv && . venv/Scripts/activate # Windows; use venv/bin/activate on macOS/Linux
pip install -r requirements.txt
cp .env.example .env # then add your ANTHROPIC_API_KEY
uvicorn app.main:app --reload --port 8000 # open http://localhost:8000Only ANTHROPIC_API_KEY is required to chat (all ocean data is keyless). Add the FIREBASE_*
vars to enable Google sign-in, history, and the logbook β see
docs/FIREBASE_SETUP.md.
Deploy: docs/DEPLOY.md (Render blueprint in render.yaml).
app/
main.py FastAPI: UI, /chat, /species/resolve, /firebase-config.js, /health
agent.py MCP client + Claude tool-call loop (the agentic core)
mcp_server.py FastMCP server β our 7 ocean-data tools
ocean_data.py live data layer (Open-Meteo, NOAA, iNaturalist, WoRMS)
prompts.py versioned system prompts (v1 β v2 β v3)
static/index.html single-file Liquid-Glass UI (chat, logbook, auth)
tests/ pytest: data layer, MCP server, API
eval/ run_eval.py + RESULTS.md
prompts/ PROMPT_LOG.md (prompt iteration log)
docs/ DEPLOY.md, FIREBASE_SETUP.md
firestore.rules per-user security rules
render.yaml Render deploy blueprint
overview.md Β· rubric.md Β· plan.md Β· BUILD_LOG.md
- It is not a safety authority. Conditions guidance is informational; it defers to lifeguards and local advisories, and is told to hedge when data is thin rather than fabricate.
- It does not invent data. Every number comes from a live tool call; thin data is flagged.
- ID coverage is best where the underlying databases are rich (well-studied coasts). Tides cover US coasts (NOAA); elsewhere it says so.
- User-uploaded logbook photos are stored as a single downscaled image per Firestore doc (no Firebase Storage); originals are not retained full-resolution.
Known failure modes + what I'd fix with more time are written up in WRITEUP.md Β§ 6 (e.g. iNaturalist's lack of a strict marine filter, US-only tides, in-memory rate limiting).
ReefScout is a public, unauthenticated app whose /chat endpoint spends a real API key, so the
main risk is resource abuse, not data theft. Mitigations in place: per-IP + global rate
limiting on /chat and /species/resolve, bounded request sizes, generic (non-leaking) error
messages, per-user Firestore rules with a deny-all default, HTML-escaped output, and no
server-side fetching of user-supplied URLs. The hard cost ceiling is a monthly spend limit set
on the API key. Full audit, findings, and the operational checklist: docs/SECURITY.md.
Status of each project requirement (2 pts each). β met Β· β in progress.
| # | Requirement | Status | Where / how |
|---|---|---|---|
| 1 | Deployment | β | Live at https://reefscout.onrender.com (free tier; wakes on load). Verified end-to-end. |
| 2 | Prompt engineering | β | System prompt v1 β v2 β v3 with documented rationale β prompts/PROMPT_LOG.md. |
| 3 | System prompt(s) | β | Purposeful role/scope/format prompt β app/prompts.py. |
| 4 | Grounding | β | Live conditions, tides, real sightings + authoritative WoRMS taxonomy injected via tools β app/ocean_data.py. |
| 5 | MCP tool (definition) | β | Our own FastMCP server, 7 tools with name/description/schema β app/mcp_server.py. Not a pre-built connector. |
| 6 | MCP tool (execution) | β | Manual tool-call loop reads tool_use, dispatches over MCP, feeds results back β app/agent.py. Verified live. |
| 7 | Agentic behavior | β | The model picks which tools to call, chains them, and decides when to stop (e.g. retries a failed geocode; skips the species tool when not asked). |
| 8 | Code on GitHub | β | Public repo with an incremental, meaningful commit history. |
| 9 | Build log | β | BUILD_LOG.md β concept pivots, decisions, dead ends, and bugs β plus prompts/PROMPT_LOG.md. |
| 10 | Originality | β | Marine field companion with occurrence-based ID verification, photo ID, and a naturalist logbook β not a generic chatbot. |
| 11 | Intellectual ownership | β | Architecture and trade-offs documented in overview.md, BUILD_LOG.md, and commit messages. |
| 12 | Iteration | β | Eval-driven (prompt v1 7/8 β v2 8/8 + documented bug fixes) and instructor draft feedback addressed β see BUILD_LOG.md Β§ Draft β final. |
| 13 | Evaluation | β | 9 structured cases, 9/9, with a documented earlier failure β eval/run_eval.py, eval/RESULTS.md. |
| 14 | Documentation | β | This README (architecture, setup, worked example) + overview.md + docs/. |
Python Β· FastAPI Β· Anthropic Claude (claude-sonnet-4-6) Β· Model Context Protocol (our own
FastMCP server) Β· Firebase (Auth + Firestore) Β· deployed on Render.
Live data, all free and key-less: Open-Meteo (geocoding + marine), NOAA CO-OPS (tides), iNaturalist (sightings + photos), WoRMS (taxonomy + conservation). Reference photos carry their iNaturalist contributor attribution. ReefScout is informational and not a safety authority.

