3.1 KiB
H2-backed MCP snapshot refresh pitfalls
Use this for Traccar/FT360-style MCP servers that read a remote H2 database by copying database.mv.db locally and querying with Java H2 Shell.
Parser pitfall: H2 header rows
H2 Shell output includes a header row like:
ID | NAME | UNIQUEID | ...
If the MCP parser returns that as data, code like int(row[0]) fails with invalid literal for int() with base 10: 'ID'.
Parser must skip header rows:
if cells[0].upper() in {"ID", "DEVICEID", "LATITUDE", "NAME"}:
continue
Also skip summary rows beginning with ( and separator rows beginning with ---.
Snapshot freshness pitfall
Do not copy the remote H2 DB only when the local file is missing. That causes stale dashboard/MCP data for hours or days. If the tool has its own short response cache, refresh the local DB snapshot on every actual query after the cache expires:
def _query_db(sql):
db_path = _scp_db() # always refresh snapshot when query runs
if db_path is None:
return []
...
A 30-second in-memory cache is enough to avoid excessive SCPs while keeping live tracking data fresh.
Contact freshness vs GPS freshness
For Traccar-like data, do not equate device online or lastupdate with a fresh GPS fix. Traccar can update device contact time when a mobile client sends a heartbeat/notification-token payload without new coordinates.
Expose both concepts separately:
| Field | Meaning |
|---|---|
last_contact_time / last_contact_age_minutes |
Device/server contact freshness (tc_devices.lastupdate) |
gps_fix_time / gps_age_minutes |
Real GPS fix age (tc_positions.fixtime, fallback to devicetime) |
is_contact_fresh |
last_contact_age_minutes <= threshold |
is_location_fresh |
gps_age_minutes <= threshold |
A valid OsmAnd GPS payload looks like:
id=612982&lat=10.417...&lon=-75.551...×tamp=...&accuracy=...
A heartbeat/notification-token payload may look like:
id=612982¬ificationToken=...
The latter can cause Traccar to keep the device online while reusing the last known coordinates. Dashboard wording should say contact live / GPS stale or equivalent, not live location.
Static dashboard export pattern
For a static ops page, generate a JSON file from the MCP/backend on a short no_agent=True cron:
/root/.hermes/scripts/ft360-export.py -> /var/www/ops/data/ft360-devices.json
schedule: every 1m
no_agent: true
If the exporter imports MCP code that depends on a venv (for example FastMCP in /opt/ops-portal/venv), either run the exporter with the venv Python or add that venv's site-packages before importing:
import site
site.addsitedir('/opt/ops-portal/venv/lib/python3.13/site-packages')
The page should fetch the JSON with cache: 'no-store' and display contact age separately from GPS fix age.
Verification
After patching, restart the MCP service and call the MCP tool, not just hermes mcp test. hermes mcp test verifies tool discovery only; a real get_devices call verifies parser and freshness.