Initial: Mesh-Skills, Configs, Scripts
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: coros-mcp-integration
|
||||
description: "Use when: COROS-Sportuhr-Daten in PersoEnt anbinden."
|
||||
version: 1.0.0
|
||||
author: mehlzoerwer
|
||||
platforms: [linux]
|
||||
models:
|
||||
default: mimo-v2-flash
|
||||
license: MIT
|
||||
related_skills: [content-creator, pocketbase-deployment]
|
||||
---
|
||||
|
||||
# COROS-MCP — Sportuhr-Daten in PersoEnt
|
||||
|
||||
Nutze diesen Skill, wenn PersoEnt (oder ein anderes Projekt) **echte COROS-Sportuhr-Gesundheitsdaten** anbinden soll: Schlaf, HRV, Stress, Ruhe-Herzfrequenz, Training, Erholung, VO2max.
|
||||
|
||||
## Status (2026-08-10)
|
||||
- **Installiert** global: `npm install -g coros-mcp` (OFFIZIELLES Paket von `coros_openplatform`, ~0.1.1)
|
||||
- **Eingeloggt** — Token gecacht in `/root/.coros-mcp-skill-gateway-ts/<region>/token.json`
|
||||
- **22 MCP-Tools** verfügbar, Endpoint `mcp.coros.com` (Europa: `mcpeu.coros.com`)
|
||||
|
||||
## CLI-Syntax (WICHTIG — Flags, nicht Positional)
|
||||
|
||||
Das Paket nutzt Flags, NICHT Positionsargumente:
|
||||
```bash
|
||||
coros-mcp list-tools # alle Tools + Schema
|
||||
coros-mcp describe-tool --tool <NAME> # Schema eines Tools
|
||||
coros-mcp call-tool --tool <NAME> --arguments-json '{}' # Tool aufrufen
|
||||
```
|
||||
- `call-tool --tool X queryDailyHealthData` (ohne `--tool`) → **Fehler "unknown argument"**
|
||||
- `--args '{}'` / `--name X` / `--tool` mit Positional → **alles falsch** (richtig: `--tool NAME --arguments-json JSON`)
|
||||
|
||||
## Login-Flow (einmalig, nur Nutzerkonto)
|
||||
```bash
|
||||
coros-mcp login-start # erzeugt Browser-Login-Link (mcpeu.coros.com/cli/login/...)
|
||||
# Nutzer oeffnet Link + meldet sich mit COROS-Konto an
|
||||
coros-mcp login-finish # bestaetigt Session -> 'login ok'
|
||||
coros-mcp login-status # 'no pending login session' = sauber/fertig
|
||||
```
|
||||
Login-Link ist region-/sessiongebunden. Andere COROS-Region → `--issuer`/`--mcp-url` anpassen.
|
||||
|
||||
## Wichtigste Tools (Daten fuer PersoEnt-Gesundheitsdetektiv)
|
||||
|
||||
| Tool | Daten |
|
||||
|------|-------|
|
||||
| `queryDailyHealthData` | 7 Tage: Schritte, Kalorien, Stress, Schlaf (Tief/Leicht/REM), Schlaf-HR |
|
||||
| `querySleepHrv` | taegl. HRV + Baseline + Normbereich |
|
||||
| `queryRestingHeartRate` / `queryAvgHeartRate` | Ruhe-/durchschnittl. HF |
|
||||
| `queryStressLevel` / `queryStressTimeSeries` | Stress (avg/roh) |
|
||||
| `querySportRecords` | Workout-Protokoll (+ labelId, sportType) |
|
||||
| `getActivityDetail` / `queryActivityLapData` | Activity-Details inkl. HR |
|
||||
| `queryRecoveryStatus` | Erholung % + Level |
|
||||
| `queryFitnessAssessmentOverview` | VO2max, Threshold-Pace |
|
||||
| `querySleepData` | Schlafscore, Dauer, Stufen |
|
||||
| `queryDevices` / `queryUserInfo` | Geraete / Profil |
|
||||
|
||||
## Integration in PersoEnt (Architektur-Vorgabe)
|
||||
- **Server-seitig auf VPS** (Master): Poller ruft `coros-mcp call-tool` auf → Daten nach **PocketBase** (`health_records`-ahnliche Struktur) → PersoEnt-Dashboard/Detektiv liest von dort.
|
||||
- HRV + Schlaf + Stress + Erholung = ideale **Kausal-Merkmale** fuer den Gesundheitsdetektiv (besser als Google-Health-Fit).
|
||||
- Gleiche Basis-Assets (ein COROS-Konto) fuer mehrere Kanaele/Personas.
|
||||
|
||||
## Edge Cases
|
||||
- **Daten brauchen echten Login** — ohne `login-finish` kein Token, `call-tool` schlaegt fehl.
|
||||
- Zeitfenster-Tools (`queryCustomActivityLapData`) brauchen `startTimestamp`/`endTimestamp` in Unix-Sekunden.
|
||||
- Sport-Records-Details brauchen `labelId` + `sportType` aus der Records-Liste.
|
||||
|
||||
## Verifizierte Daten-Shapes
|
||||
Siehe `references/coros-mcp-data-shapes.md` — echte Antwort-Formate (queryDailyHealthData = ein String mit `--- JJJJMMTT ---`-Tag-Abschnitten, Sport-Records mit labelId/sportType), Token-Sicherheit (root-only Cache + age-Backup, NIE die Klartext-token.json verschluesseln) und der PersoEnt-Write-Hinweis (kein datum-Feld).
|
||||
|
||||
## 🔥 Robustes Parsen der 7-Tage-Helth-Antwort (verifiziert 2026-08-15)
|
||||
|
||||
`queryDailyHealthData` liefert EINEN String mit 7 `--- 20260815 ---`-Tagbloecken. Naive Extraktion (erstes "Steps" finden) ist FEHLERHAFT — es erwischt das Datum oder einen alten Tag. Robust:
|
||||
- **Letzten Tages-Block nehmen**, nicht den ersten: `re.split(r'--- (\d{8}) ---', text)` → nimm das letzte (date, body)-Paar.
|
||||
- **`Stress: Avg 22`** hat "Avg" zwischen Label und Zahl → Regex `re.escape(label) + r'[^\d]*?([\d.,]+)'` (Ueberspringt Zwischenwoerter).
|
||||
- **Puls zuverlaessig aus `Sleep HR: Avg 51 bpm`** im Health-Block (AVG HR-Call `queryAvgHeartRate` hat anderes/leeres Block-Format — nicht darauf stuetzen).
|
||||
- **Schlaf** = `Total: 7h 46min` → `(\d+)h (\d+)min` → 7.77h.
|
||||
- COROS-Werte kommen mit **Tausender-Komma** (z.B. `9,304`) → `,` entfernen.
|
||||
- Nach jedem Fix: **real gegen die Live-Antwort testen** (echten `call-tool`-String parsen), nie von Schema raten — Daten-Shapes weichen ab.
|
||||
@@ -0,0 +1,95 @@
|
||||
# COROS-MCP — Historie/Backfill + Parser-Pitfalls (verifiziert 2026-08-18)
|
||||
|
||||
Ergänzt zu `coros-mcp-data-shapes.md`. Alle Befunde sind reale `call-tool`-Antworten, nicht geraten.
|
||||
|
||||
## Health-Historie: days-Parameter + Server-Limit
|
||||
|
||||
`queryDailyHealthData` akzeptiert im Schema einen `days`-Parameter (integer, default 7).
|
||||
|
||||
```bash
|
||||
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{"days":30}'
|
||||
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{"days":365}'
|
||||
```
|
||||
|
||||
**WICHTIG (verifiziert 2026-08-18):** Egal ob `days:30`, `days:90` oder `days:365` — die COROS-API liefert **maximal ~53 Health-Tage** zurück (z.B. nur `2026-05-16 .. 2026-08-15`, 53 Blöcke). Das ist ein **SERVER-Limit von COROS**, kein Parser- oder Client-Fehler. Mehr Health-Historie gibt es über diese API nicht; für längere Geschichte bleibt nur der Sport/FIT-Pfad.
|
||||
|
||||
Auch bei "alles was geht" lohnt kein Versuch über 53 hinaus zu holen — wiederhole `sportBackfill` dafür nicht.
|
||||
|
||||
## Sport-Historie: querySportRecords mit Datumsbereich
|
||||
|
||||
`querySportRecords` akzeptiert (anders als Health) **`startDate`/`endDate` im Format `yyyyMMdd`** (kein `days`) und `sportTypeCodes`-Array:
|
||||
|
||||
```bash
|
||||
coros-mcp call-tool --tool querySportRecords --arguments-json \
|
||||
'{"startDate":"20250501","endDate":"20260815","sportTypeCodes":[65535]}'
|
||||
```
|
||||
|
||||
- `sportTypeCodes: [65535]` = alle Sportarten (sonst numerische Codes, nie Namen).
|
||||
- **Sport-Historie reicht deutlich weiter zurück als Health** (1 Jahr+). 20 Records über 15 Monate bestätigt.
|
||||
- Antwort enthält je Workout: `N. <Sportart> — YYYY-MM-DD`, `Time Window: startTimestamp=.. | endTimestamp=..`, `Duration`, `Avg HR: <n> bpm`, `Calories: <n> kcal`, `Distance: <x> km`, `LabelId: <Zahl>`, `SportType: <code>`.
|
||||
|
||||
## 🔥 PITFALL: COROS-`content[0].text` enthält LITERALES `\n`, KEINE echten Newlines
|
||||
|
||||
(Wichtigster Fund — ohne Normalisierung matcht **nichts**.)
|
||||
|
||||
`querySportRecords` (und teils andere) liefern im Text **literale 2-Zeichen-Sequenz `\n` (ein Backslash + n)**, keine echten Zeilenumbrüche. `splitlines()` / `repr()` in einem Launch zeigen das nicht zuverlässig.
|
||||
|
||||
**Debug-Befund (2026-08-18):**
|
||||
```python
|
||||
text = d['content'][0]['text']
|
||||
'\n' in text # False (kein echter Newline)
|
||||
'\\n' in text # True (literales Backslash-n vorhanden)
|
||||
```
|
||||
|
||||
**Fix — normalisiere VOR jedem Zeilen-Parsing:**
|
||||
```python
|
||||
text = text.replace('\\n', '\n')
|
||||
lines = text.splitlines() # erst jetzt funktioniert splitlines()
|
||||
```
|
||||
Übersehen → alle Regex gegen `splitlines()` finden 0 Zeilen (kein Fehler, nur 0 Treffer). Achte auf die richtige Backslash-Tiefe: Ersetze das **1-Backslash-`\n`-Literal**, nicht doppel.
|
||||
|
||||
## ⏱️ Dauer-Format ist uneinheitlich: HH:MM:SS vs MM:SS vs SS
|
||||
|
||||
COROS-`Duration:`-Werte kommen **gemischt**:
|
||||
- `1:00:14` → 1h = **60 min** (3 Segmente)
|
||||
- `2:03:36` → 2h03m = **123 min** (3 Segmente)
|
||||
- `51:35` → **51 min** (2 Segmente — NICHT 51std35!)
|
||||
- `0:22` → 22 Sek → 0 min (2 Segmente)
|
||||
|
||||
**Fehlerhafte Regex** `(\d+):(\d+)(?::(\d+))?` macht `51:35` fälschlich zu `51*60+35 = 3095 min` (absurd 63h). **Robust:**
|
||||
```python
|
||||
m = re.search(r'Duration:\s*([\d:]+)', block)
|
||||
seg = [int(x) for x in m.group(1).split(':')]
|
||||
if len(seg) == 3: # HH:MM:SS
|
||||
dauer_min = seg[0]*60 + seg[1]
|
||||
elif len(seg) == 2: # MM:SS
|
||||
dauer_min = seg[0]
|
||||
else: # SS
|
||||
dauer_min = seg[0] // 60
|
||||
```
|
||||
|
||||
## Sport-Daten → SEPARATE Tabelle (nicht health_records)
|
||||
|
||||
Ein Tag kann **mehrere Workouts** haben → passt NICHT in `health_records` (1 Zeile/Tag). Eigene Tabelle:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS sport_records (
|
||||
label_id TEXT PRIMARY KEY, -- COROS-LabelId je Workout (natürlicher PK)
|
||||
datum TEXT NOT NULL, -- yyyyMMdd
|
||||
sportart TEXT NOT NULL,
|
||||
sport_type_code INTEGER, -- COROS SportType-Code
|
||||
dauer_min INTEGER,
|
||||
avg_hr INTEGER,
|
||||
kalorien INTEGER,
|
||||
distanz_km REAL,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sport_datum ON sport_records(datum);
|
||||
```
|
||||
|
||||
**Idempotent:** `INSERT OR IGNORE ... ON CONFLICT(label_id) DO NOTHING` — Workouts sind nach Import unveränderlich, kein UPDATE nötig. (Direkter DB-Write, da `health_records`-API createRule leer = nur Superuser schreibt; siehe `pocketbase-deployment`.)
|
||||
|
||||
## Reihenfolge bei Backfill in bestehende DB
|
||||
|
||||
1. Wenn schon fehlerhafte Records mit dem alten Dauer-Parser geschrieben wurden → Flushing die Tabelle (`DELETE FROM sport_records`) oder per label_id neu überschreiben, DANN Neu-Load mit korrektem Parser.
|
||||
2. Immer vorher DB-Backup (Backup-Regel): `cp /opt/pocketbase/pb_data/data.db data.db.bak-<ts>`.
|
||||
@@ -0,0 +1,51 @@
|
||||
# COROS-MCP — verifizierte Tool-Calls + Daten-Shapes (2026-08-15)
|
||||
|
||||
Proof-of-Concept mit echtem Login, alle Werte real. Endpoint Europa `mcpeu.coros.com`, Token-Cache `/root/.coros-mcp-skill-gateway-ts/<region>/token.json`.
|
||||
|
||||
## Zugriffshinweis (Token-Sicherheit, Team-Auflage)
|
||||
- Token-Datei root-only (`chmod 600`), Cache `chmod 700`.
|
||||
- Zusaetzlich age-verschluesseltes Backup: `age-keygen -o /root/.coros-age-key.txt; age -r <PUBKEY> -o /root/.coros-token.json.age <token.json>`.
|
||||
- WICHTIG: **NICHT die Klartext-`token.json` verschluesseln/ersetzen** — die `coros-mcp`-CLI LIESST sie selbst (Klartext nötig fuer den Betrieb). Verschluesselung nur als SEPARATE Sicherungskopie ablegen.
|
||||
|
||||
## Korrekte Aufruf-Syntax (verifiziert)
|
||||
```bash
|
||||
# Achtung: Flags! Nicht positional. KEIN --args/--name/--tool-positional.
|
||||
coros-mcp list-tools
|
||||
coros-mcp describe-tool --tool queryDailyHealthData
|
||||
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{}'
|
||||
```
|
||||
|
||||
## Echte Ergebnis-Shapes (Anhaltspunkt fuer Parser)
|
||||
|
||||
### queryDailyHealthData ({} args) — Text, 7 Tage
|
||||
```
|
||||
"Daily Health Data — Last 7 days | Resting HR: 52 bpm | HRV Baseline: 42 ms
|
||||
--- 20260815 ---
|
||||
Steps: 981 | Calories: 47 kcal | Exercise: 0 min
|
||||
Stress: Avg 22
|
||||
Sleep Summary:
|
||||
Total: 7h 46min | Deep: 1h 57min | Light: 4h 17min | REM: 1h 23min | Awake: 9 min
|
||||
```
|
||||
Antwort-Container: `{"content":[{"type":"text","text":"<string>"}],"isError":false}` — **Daten sind EIN String**, mit `\n`-Zeilen, pro Tag Abschnitt `--- JJJJMMTT ---`. Parser: nach Datums-Marker + Label splitten.
|
||||
|
||||
### querySportRecords ({}) — 1+ Records
|
||||
```
|
||||
"Sport Records — DATE to DATE (N records)
|
||||
1. Indoor Cycling — 2026-08-11
|
||||
Location: Indoor Rad
|
||||
Time Window: startTimestamp=1786472136 | endTimestamp=1786475750
|
||||
Duration: 1:00:14 | Avg HR: 114 bpm | Calories: 629 kcal
|
||||
LabelId: 479635354870120452 | SportType: 201"
|
||||
```
|
||||
Sauber parsebar via Regex auf `LabelId:` + `SportType:` (fuer Activity-Details noetig).
|
||||
|
||||
### querySleepHrv ({}), queryRecoveryStatus ({}), queryFitnessAssessmentOverview ({})
|
||||
- HRV: pro Tag `HRV Avg: 55 ms — Normal`, `Baseline: 61 ms`
|
||||
- Recovery: `Recovery: 100%`, `Level: Heavy training allowed`
|
||||
- Fitness: `Threshold Pace: 4:45 /km`
|
||||
|
||||
### queryUserInfo ({})
|
||||
`Height: 173.0 cm | Weight: 100.6 kg | Birthday: 1972-02-21 (Age: 54) | Gender: Male | Nickname: Thorsten`
|
||||
|
||||
## PersoEnt-Write-Hinweis (strukturierte Gesundheitsdaten)
|
||||
COROS liefert KEIN Datums-Feld in der Antwort — das Datum steckt im `--- JJJJMMTT ---`-Marker. PersoEnt `health_records`-Collection hat KEIN `datum`-Feld; Datum geht verloren, wenn man es nicht als String in einem Text-Feld oder via Record-`created` abbildet. (siehe pocketbase-deployment: leere createRule → Superuser-Token noetig.)
|
||||
Reference in New Issue
Block a user