Files
hermes-skills/skills/devops/mcp-servers/references/h2-remote-db-query.md
T

5.8 KiB
Raw Blame History

Remote H2 Database Query via SSH

When an MCP server needs to read from an H2 database on a remote host (e.g., Traccar on app2), use this pattern:

Architecture

MCP tool call
  → SSH into remote host (key-based, no password prompt)
  → scp the .mv.db file to a local temp file
  → Java H2 Shell query (org.h2.tools.Shell)
  → Parse pipe-delimited output
  → Delete temp file
  → Return structured JSON

Why this pattern

  • H2 databases lock. You cannot query a live H2 database from two processes simultaneously. Copying with scp creates a read-only snapshot — the copy succeeds even while the DB is in use, and you query the copy.
  • Java is needed. H2's file format is proprietary. Python libraries like jaydebeapi exist but are brittle; the H2 Shell jar is self-contained and reliable.
  • 30-second cache. The SCP + Java query takes 3-5 seconds. Caching avoids this overhead on repeated calls.

Prerequisites

On the local host:

apt-get install -y default-jre-headless
# Copy H2 jar from the remote host or the container
scp root@remote:/tmp/h2.jar /tmp/h2.jar
# Test: java -cp /tmp/h2.jar org.h2.tools.Shell --help

On the remote host (the H2 jar must exist):

# If inside a Docker container:
docker cp container_name:/opt/app/lib/h2-*.jar /tmp/h2.jar

Implementation Template

import subprocess, tempfile, os, time

HOST = "152.53.xxx.xxx"
DB_PATH = "/root/docker/app/data/database"
CACHE_TTL = 30

_cache = {"data": None, "ts": 0}

def query_devices():
    if _cache["data"] and (time.time() - _cache["ts"]) < CACHE_TTL:
        return _cache["data"]

    tmp_db = tempfile.mktemp(suffix=".mv.db")
    # 1. Copy DB snapshot from remote
    subprocess.run(
        ["scp", "-i", "/root/.ssh/itpp-infra", "-o", "StrictHostKeyChecking=no",
         f"root@{HOST}:{DB_PATH}.mv.db", tmp_db],
        capture_output=True, timeout=15, check=True
    )

    # 2. Query with Java H2 Shell
    base = tmp_db.replace(".mv.db", "")
    result = subprocess.run(
        ["java", "-cp", "/tmp/h2.jar", "org.h2.tools.Shell",
         "-url", f"jdbc:h2:{base}", "-user", "sa", "-password", "",
         "-sql", "SELECT id, name, latitude, longitude FROM tc_devices;"],
        capture_output=True, text=True, timeout=20
    )
    os.unlink(tmp_db)

    # 3. Parse pipe-delimited output (H2 Shell format)
    # Output: "ID | NAME | LATITUDE | LONGITUDE\n1  | foo  | 10.4     | -75.5"
    rows = []
    for line in result.stdout.strip().split('\n'):
        parts = [p.strip() for p in line.split('|')]
        if len(parts) >= 4 and parts[0].strip().isdigit():
            rows.append({
                "id": int(parts[0]),
                "name": parts[1] if parts[1] != "null" else None,
            })

    data = {"rows": rows, "count": len(rows)}
    _cache["data"] = data
    _cache["ts"] = time.time()
    return data

Traccar Schema Reference (GPS Tracking)

These are the live table schemas used by Traccar 6.x. All queries go through the H2 database at /root/docker/traccar/traccar-data/database.mv.db on app2 (152.53.39.202).

tc_devices

Column Type Notes
id INT Primary key
name VARCHAR User-assigned device name
uniqueid VARCHAR Device identifier (IMEI, serial)
lastupdate TIMESTAMP Last position received
status VARCHAR "online", "offline", or "unknown"

tc_positions

Column Type Notes
id BIGINT Auto-increment
deviceid INT FK → tc_devices.id
latitude DOUBLE Decimal degrees
longitude DOUBLE Decimal degrees
speed DOUBLE Knots (× 1.15078 for mph)
devicetime TIMESTAMP Device-reported time

Common Queries

-- All devices with latest position (one query)
SELECT d.id, d.name, d.uniqueid, d.lastupdate, d.status,
       p.latitude, p.longitude, p.speed, p.devicetime
FROM tc_devices d
LEFT JOIN tc_positions p ON p.id = (
    SELECT MAX(id) FROM tc_positions WHERE deviceid = d.id
)
ORDER BY d.lastupdate DESC;

-- Position history for one device
SELECT latitude, longitude, speed, devicetime
FROM tc_positions WHERE deviceid = 1
ORDER BY devicetime ASC;

-- Stats: distance (haversine), max speed, active time
SELECT COUNT(*) as positions,
       MAX(speed) * 1.15078 as max_speed_mph
FROM tc_positions
WHERE deviceid = 1
  AND devicetime >= NOW() - INTERVAL '24' HOUR;

FastAPI Proxy Integration

When the MCP server is slow (3-5s per SCP+Java query), serve data through a FastAPI proxy with 30-second caching. The ops portal at /opt/ops-portal/server.py uses this pattern:

# /api/ft360/status endpoint proxies Traccar data
@app.get("/api/ft360/status")
async def traccar_status():
    return _get_traccar_data()  # cached 30s, SCP+Java underneath

The JavaScript dashboard fetches from /api/ft360/status every 30 seconds — same TTL as the cache. No authentication needed; the proxy handles the SSH and Java complexity.

  • Java must be on PATH. Install default-jre-headless before using. The java binary in Docker containers (e.g., /opt/traccar/jre/bin/java) isn't on PATH by default.
  • H2 jar must be on the same host and compatible. Copy it from the container or remote host — don't download a different version from Maven Central.
  • H2 Shell output format. The first line is column headers (ID | NAME | ...), subsequent lines are data. Filter with .isdigit() on the first column.
  • SCP key must work. The SSH key path must be absolute, and StrictHostKeyChecking=no avoids prompt-freezing on first connection.
  • Temp file cleanup. Always os.unlink(tmp_db) in a finally block or immediately after the query. A 176KB file won't cause issues if left, but it's sloppy.
  • Cache invalidation. If you stop+start the remote container (restarting the DB), the cache may return stale data until the TTL expires. For write operations, skip the cache.