getbased-mcp 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-mcp
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: MCP server for querying blood work data and knowledge base from getbased
5
5
  License-Expression: GPL-3.0-only
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-mcp
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: MCP server for querying blood work data and knowledge base from getbased
5
5
  License-Expression: GPL-3.0-only
6
6
  Requires-Python: >=3.10
@@ -361,6 +361,167 @@ async def getbased_section(section: str = "", profile: str = "") -> str:
361
361
  return f"[{match_key}]\n\n{sections[match_key]}"
362
362
 
363
363
 
364
+ @mcp.tool()
365
+ @_instrumented("getbased_wearables_series")
366
+ async def getbased_wearables_series(
367
+ metric: str = "",
368
+ days: int = 0,
369
+ profile: str = "",
370
+ ) -> str:
371
+ """Read the wearable daily-values series the user opted into pushing.
372
+
373
+ The user picks a window in Settings → Integrations → Agent Access:
374
+ 7, 30, or 90 days (or off). When set, the browser pushes a
375
+ `[section:wearables-series-{N}d]` block to the gateway containing
376
+ one line per metric, daily values separated by `→` (oldest to
377
+ newest), `—` for no-reading days, and the primary source in parens.
378
+
379
+ This tool extracts that series and optionally slices it.
380
+
381
+ Args:
382
+ metric: optional metric id to return only one line. Examples:
383
+ 'hrv_rmssd' (overnight HRV), 'rhr' (overnight resting HR),
384
+ 'hr_day' (daytime HR), 'sleep_score', 'readiness_score',
385
+ 'steps', 'weight'. Pass empty string for the whole matrix.
386
+ days: optional preferred window. If 0, returns whichever
387
+ window the user pushed. If 7/30/90, returns that section
388
+ specifically (404 if not pushed). The browser only pushes
389
+ ONE window at a time, so non-matching values fall back.
390
+ profile: profile id (omit for default).
391
+
392
+ Returns the section content, or a clear error if the user hasn't
393
+ enabled the toggle yet.
394
+ """
395
+ data = await _fetch_context(profile)
396
+ if "error" in data:
397
+ return f"Error: {data['error']}"
398
+ context = data.get("context", "")
399
+ if not context:
400
+ return "No context available"
401
+
402
+ sections = _parse_sections(context)
403
+ # Find the wearables-series-Nd section. Prefer requested `days`, else
404
+ # whichever the user opted into.
405
+ candidates = [k for k in sections if k.startswith("wearables-series-")]
406
+ if not candidates:
407
+ return (
408
+ "No wearable series available. The user can enable this in "
409
+ "getbased: Settings → Integrations → Agent Access → "
410
+ "'Push wearable daily series'. Pick 7, 30, or 90 days."
411
+ )
412
+
413
+ chosen = None
414
+ if days in (7, 30, 90):
415
+ target = f"wearables-series-{days}d"
416
+ chosen = next((k for k in candidates if k == target), None)
417
+ if not chosen:
418
+ available = [k.replace("wearables-series-", "").replace("d", "") for k in candidates]
419
+ return (
420
+ f"User hasn't pushed the {days}-day window. Currently "
421
+ f"available: {', '.join(available)} day(s). They can "
422
+ f"change the window in Settings → Integrations → Agent "
423
+ f"Access."
424
+ )
425
+ else:
426
+ chosen = candidates[0]
427
+
428
+ content = sections[chosen]
429
+ if not metric:
430
+ return f"[{chosen}]\n\n{content}"
431
+
432
+ # Parse one line. Lines look like:
433
+ # HRV (overnight) ms (oura): 33→35→32→…→39
434
+ metric_lower = metric.lower().strip()
435
+ matched = []
436
+ for line in content.split("\n"):
437
+ if not line or line.startswith("##"):
438
+ continue
439
+ # The metric id isn't directly in the line — labels are like
440
+ # "HRV (overnight)" / "Resting HR" / "Steps". Match by checking
441
+ # whether `metric_lower` appears in the line label OR the line
442
+ # starts with a known label-form for that metric.
443
+ head = line.split(":", 1)[0].lower()
444
+ if metric_lower in head:
445
+ matched.append(line)
446
+ continue
447
+ # Common id → label-fragment aliases. Browser emits labels via
448
+ # `${label}${unit ? ' ' + unit : ''} (${primarySource})` where
449
+ # `label` is `canon.label` followed by an optional `(${canon.sub})`.
450
+ # For `hrv_rmssd` that produces `HRV (🌙) ms (oura)` — the literal
451
+ # parens around the glyph mean substring matches like "hrv 🌙"
452
+ # FAIL. List enough fragments per id to handle all the label forms
453
+ # the canonical registry can emit.
454
+ aliases = {
455
+ # HRV overnight: label="HRV", sub="🌙" → "hrv (🌙)"
456
+ "hrv_rmssd": ["hrv (🌙)", "hrv 🌙", "hrv (overnight)", "hrv overnight"],
457
+ # HRV daytime: label="HRV", sub="☀️" → "hrv (☀️)"
458
+ "hrv_day": ["hrv (☀", "hrv ☀", "hrv (daytime)", "hrv daytime"],
459
+ # HRV SDNN (Apple Health): label="HRV", sub="SDNN" → "hrv (sdnn)"
460
+ "hrv_sdnn": ["hrv (sdnn)", "hrv sdnn"],
461
+ # Resting HR: label="Resting HR", sub="" → "resting hr"
462
+ "rhr": ["resting hr", "resting heart"],
463
+ # Heart rate daytime: label="Heart rate", sub="☀️" → "heart rate (☀️)"
464
+ "hr_day": ["heart rate (☀", "heart rate ☀", "heart rate (daytime)", "heart rate daytime"],
465
+ # Sleep score: label="Sleep", sub="score" → "sleep (score)"
466
+ "sleep_score": ["sleep (score)", "sleep score"],
467
+ "readiness_score": ["readiness (score)", "readiness score"],
468
+ "activity_score": ["activity (score)", "activity score"],
469
+ "stress_high_min": ["stress"],
470
+ "resilience_level": ["resilience"],
471
+ "cardio_age": ["cardio age"],
472
+ "strain": ["strain (day)", "strain"],
473
+ "steps": ["steps"],
474
+ "weight": ["weight"],
475
+ "bp_systolic": ["bp (syst)", "bp syst", "blood pressure systolic"],
476
+ "bp_diastolic": ["bp (dia)", "bp dia", "blood pressure diastolic"],
477
+ "spo2_avg": ["spo₂", "spo2"],
478
+ "body_temp_delta": ["body temp", "body_temp"],
479
+ "glucose_avg": ["glucose"],
480
+ # Withings full coverage (getbased PR #140 / #143). Labels are
481
+ # unsubbed for body comp, but sleep architecture carries subs
482
+ # like "Sleep total", "Sleep HR (avg) bpm", etc.
483
+ "pwv": ["pwv"],
484
+ "vascular_age": ["vascular age"],
485
+ "cardio_fitness": ["cardio fit"],
486
+ "body_fat_pct": ["body fat"],
487
+ "fat_mass_kg": ["fat mass"],
488
+ "muscle_mass_kg": ["muscle"],
489
+ "lean_mass_kg": ["lean mass"],
490
+ "bone_mass_kg": ["bone"],
491
+ "water_mass_kg": ["water"],
492
+ "visceral_fat": ["visceral fat"],
493
+ "nerve_health_score": ["nerve health"],
494
+ "body_temp": ["body temp"],
495
+ "skin_temp": ["skin temp"],
496
+ "sleep_total_min": ["sleep total"],
497
+ "sleep_deep_min": ["deep sleep"],
498
+ "sleep_light_min": ["light sleep"],
499
+ "sleep_rem_min": ["rem sleep"],
500
+ "sleep_awake_min": ["awake (in bed)", "awake in bed"],
501
+ "sleep_hr_avg": ["sleep hr (avg)", "sleep hr"],
502
+ "sleep_breathing_rate": ["breathing (sleep)", "breathing"],
503
+ "sleep_snoring_min": ["snoring"],
504
+ "sleep_breath_disturb": ["apnea (level)", "apnea"],
505
+ }
506
+ for alias_id, label_forms in aliases.items():
507
+ if alias_id == metric_lower and any(lf in head for lf in label_forms):
508
+ matched.append(line)
509
+ break
510
+
511
+ if not matched:
512
+ # Surface the available metric labels so the agent can retry.
513
+ labels = []
514
+ for line in content.split("\n"):
515
+ if line and not line.startswith("##") and ":" in line:
516
+ labels.append(line.split(":", 1)[0].strip())
517
+ return (
518
+ f"Metric '{metric}' not found in [{chosen}]. "
519
+ f"Available labels: {' · '.join(labels)}"
520
+ )
521
+
522
+ return f"[{chosen}: {metric}]\n\n" + "\n".join(matched)
523
+
524
+
364
525
  @mcp.tool()
365
526
  @_instrumented("getbased_list_profiles")
366
527
  async def getbased_list_profiles() -> str:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "getbased-mcp"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "MCP server for querying blood work data and knowledge base from getbased"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-only"
File without changes
File without changes
File without changes