The boat-agent stack here runs on a prime directive: if there's something usable out there, improve it; build our own only as a last resort. So when we needed a SignalK MCP server, the honest first move wasn't to write one — it was to evaluate the one that already exists.
for complex multi-step work.
We read it, ran the numbers against our own agent, and kept our discrete-named-tool makes the spoken form a first-class field:
{
"path": "environment.wind.speedApparent",
"value": 8.5,
"display": "16.5 knots",
"unit": "knots",
"timestamp": "2026-05-18T00:00:00Z"
}
{
"bank": "house",
"soc_fraction": 0.68,
"voltage": 12.84,
"current": -8.2,
"display": "68 percent, 12.8 volts, 8.2 amps discharging",
"timestamp": "2026-05-14T18:00:00Z"
}
The rules behind that display: spelled-out units ("knots", never "kn"), spelled-out compass points ("North-East", never "NE"), cardinal-name lat/lon, no °T suffix a TTS engine reads as letters, no ISO timestamp narrated digit by digit. Position is the instructive case — the raw {latitude, longitude} dict stays in value for programmatic use, but the agent speaks display, never the raw pair.
execute_code returns whatever the agent's script returns — raw SignalK. That pushes all of this formatting onto the agent, which is exactly the layer that fails on a small model. We've written before about why . Go read VesselSense too — different target, good work.
SOCIAL SHARE CARD GENERATOR