CLI reference¶
The cli jar has two entry points (build.sbt):
marola.Main, the CLI:just run -- <flags>,java -jaron the assembled jar, or the image (ENTRYPOINT java -jar /app/marola.jar, default arguments--brief).marola.agent.SwimConditionsMcpServer, the MCP tool server:just mcp-server.
Both read their settings from MAROLA_* environment variables: see
Configuration reference. How to set up Ollama and run it for the first
time is Run it locally.
Modes¶
Main runs one mode per call. The first row whose flag is present wins:
| Flag | Mode | Recipe |
|---|---|---|
--report-sighting <kind> <beach> [note] |
Append a sighting to MAROLA_LOCAL_SIGHTING_STORE_PATH. kind is jellyfish, whale or pollution (any case); the note is one argument, so quote it |
|
--analyze-photo <path> |
Describe a photo with MAROLA_LOCAL_VISION_MODEL, which must be multimodal |
|
--ask "<question>" |
Answer from the knowledge corpus: retrieve with the embedder, answer with the LLM, print the passages' sources | just ask "<question>" |
--reindex |
Re-embed the corpus into MAROLA_KNOWLEDGE_INDEX_PATH (normally automatic when a file or the embed model changes) |
just knowledge-index |
--benchmark |
The benchmark | just benchmark |
--site [area-id] |
The map's boards | |
--serve-chat [port] |
The chat server | |
| none of the above | The recommendation | just run |
A flag that takes a value reads the next argument and is ignored when there is none. A
--report-sighting with an unknown kind, or with fewer than two values, does not match, and the
call falls through to the next row. The optional value of --site and --serve-chat is ignored
when it starts with --.
Recommendation (the default)¶
Tomorrow's best hour per nearby beach: up to six beaches, scored, with water quality, facilities, trails and tides where the sources have them.
| Flag | Effect |
|---|---|
--lat <deg> --lon <deg> |
Search from this point. Both or neither: one alone is ignored with a warning |
--location-url <url> |
Search from a Google Maps pin: @lat,lon, q=/query=/ll=, or a place URL's !3dlat!4dlon (Coordinates.fromMapsUrl). Expand a maps.app.goo.gl short link first (curl -sIL); an unreadable URL is ignored with a warning |
--summarize |
Add the LLM summary: a draft from recommendation_prompt.json, then a second pass from review_prompt.json that scores and corrects it (both compiled prompts on the classpath) |
--brief |
One line per beach: no detail block for the top pick, no sea lore |
--no-lore |
Drop the sea-lore paragraph (as MAROLA_SEA_LORE=off) |
The origin is the first of these that yields a point; the origin -> line names which one:
--lat/--lon.--location-url.MAROLA_ORIGIN_LAT/MAROLA_ORIGIN_LON(both or neither).- IP geolocation: ipinfo.io, ipwho.is and ip-api.com, the medoid of their answers. It is city-level, so the radius becomes at least 20 km.
- Arpoador, Rio de Janeiro, only when no IP provider answered.
The radius is MAROLA_BEACH_SEARCH_RADIUS_KM (15 km by default). With MAROLA_TRACES=mlflow the
run is one trace: marola.recommend, with bestPerBeachTomorrow, trails and one llm.<model>
span per LLM call under it.
Benchmark¶
--benchmark runs marola against a plain prompt on the questions in benchmark_questions.json
(classpath), in three arms: baseline (the plain prompt), and rag-strict and rag-general (the
corpus, with each MAROLA_ASK_FALLBACK behaviour). Retrieval uses MAROLA_ASK_MIN_SCORE. It
prints the report (coverage, citations, abstentions, latency and a verdict), writes it to
./data/benchmark-<yyyyMMdd-HHmm>.md, and logs it as an MLflow run in the experiment
<MAROLA_MLFLOW_EXPERIMENT>/benchmark when MAROLA_MLFLOW_TRACKING_URI is set. marola-ml's
benchmark gate runs this mode from the published image.
Site boards¶
--site builds the board data the map at marola.dev renders, for one area
of the areas file, or for every area when no id is given.
| Flag | Default | Effect |
|---|---|---|
--site [area-id] |
every area | Which area to build |
--areas <file> |
$PWD/site/areas.json |
The areas file |
--site-out <dir> |
$PWD/site/dist |
Where to write |
Each areas-file entry needs id ([a-z0-9-]+), name, lat, lon, radius_km,
beach_limit, tz (an IANA zone) and tiles; tiles_attribution is optional. An entry missing
a required field is dropped. The build writes <out>/data/areas.json and, per area,
<out>/data/<id>/<today>.json and <tomorrow>.json (the boards, against
cli/src/main/resources/board.schema.json) and latest.json naming both. A missing areas file or
an unknown id prints a message and writes nothing. A failed build exits 1, because a deploy would
publish whatever partial output it left.
To try it from this repo, with the test fixture's areas file:
just run -- --site floripa --areas cli/src/test/resources/site/areas.json.
The real areas file, the page and the deploy are
marola-site's, which runs the published image with
--site --areas.
Chat server¶
--serve-chat runs an HTTP server for the map's chat widget, in the foreground until Ctrl+C. The
port is the flag's value, 8787 by default. It listens on every interface, not only localhost, and
answers any origin (Access-Control-Allow-Origin: *).
| Request | Response |
|---|---|
GET /health |
200 {"status":"ok"} |
POST /ask with {"question": "..."} |
200 {"answer", "safety", "sources": [{"title", "source"}]} |
POST /ask without a string question |
400 {"error":"missing question"} |
/ask with another method |
405 {"error":"POST only"} |
/ask when retrieval or the LLM fails |
500 {"error": "<the failure>"} |
OPTIONS on either path |
204, the CORS preflight |
answer carries the emergency footer when safety is true, that is when a retrieved passage came
from the corpus's safety documents (MIP-0022). Exposing the server through a Cloudflare Tunnel and
connecting the widget is in Chat and MCP.
MCP server¶
SwimConditionsMcpServer speaks MCP over stdio as marola-swim-conditions 0.1.0. This repo's
.mcp.json registers it for Claude Code as just mcp-server; any other MCP client can launch
just mcp-server, or java -cp <the assembled jar> marola.agent.SwimConditionsMcpServer. Each
tool returns one text item holding JSON.
| Tool | Arguments | Returns |
|---|---|---|
find_nearby_beaches |
lat, lon (required), radius_km (default 15) |
The six nearest named beaches: [{name, lat, lon, distance_km}] |
get_swim_recommendation |
the same | Tomorrow's best hour per beach: [{beach_name, distance_km, hour_local, score, sea_temp_c, wind_kmh, wave_height_m, jellyfish_risk, whale_sighting_likelihood, notes, water_quality, water_quality_summary, tides: [{time, height_m, high}]}] |
get_water_quality |
the same | {source, beaches: [{beach, water_quality: {source, points: [{point, beach, location, lat, lon, latest: {sampled_on, condition, enterococci_per_100ml, rain, water_temp_c}}]}}]}, or {"error": "no water-quality provider covers this origin"} |
ask_ocean_question |
question (required) |
{answer, safety, sources: [{title, source, score}]} |
A missing or non-numeric lat/lon becomes 0, not an error. get_water_quality picks its
provider as the CLI does (MAROLA_WATER_QUALITY_PROVIDER, by default from the origin), although
its description still says Santa Catarina only.
What the servers do not read
get_swim_recommendation passes no water-quality client, so its water_quality is always
null; ask get_water_quality for it. ask_ocean_question and the chat server's /ask
answer with the strict fallback and a minimum score of 0, whatever MAROLA_ASK_FALLBACK and
MAROLA_ASK_MIN_SCORE say: only --ask and --benchmark read them.