Files

158 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```bash
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):
```bash
# If inside a Docker container:
docker cp container_name:/opt/app/lib/h2-*.jar /tmp/h2.jar
```
## Implementation Template
```python
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
```sql
-- 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:
```python
# /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.