hybridlog

io.github.Imgrund由 GitHub 帳號 Imgrund 發布v0.1.13更新於 Oct 24, 2026

Reads your own Garmin mirror: readiness, training load, muscle freshness, and read-only SQL.

已驗證Streamable HTTP可網頁執行資料庫位置與生活資料與分析

概覽

AI 產生的概覽

一個自架的 MCP 伺服器,把你自己的 Garmin 資料鏡像到你掌控的資料庫,讓助理讀取準備度、訓練負荷、肌肉恢復狀態,並執行唯讀 SQL 查詢。

可以做什麼
  • 一次呼叫取得健康總覽、各身體系統的洞察、肌肉恢復狀態與訓練負荷指標
  • 依動作類別檢視力量進展,以及逐圈的比賽分段資料
  • 深入檢視單次活動,包括各心率區間時長、圈、組與心率曲線
  • 描述鏡像資料庫的資料表結構,並執行有列數上限的唯讀 SQL 查詢
  • 從 Garmin 重新整理最近兩天資料、記錄或刪除症狀,並把回饋存成長期準則
  • 執行每週回顧提示
適用情境
適合用 Garmin 訓練、希望助理回答手錶和儀表板無法回答的問題的人,例如本週與上週相比如何、今天是否該休息、某處不適對明天代表什麼。由於沒有託管執行個體,適合願意自行部署的使用者。
執行需求
需要自架部署:Docker(或手動安裝)搭配 PostgreSQL、佇列 worker 與排程器,以及已發布的容器映像。必須提供同一個 hybridlog 安裝的 APP_KEY 和指向其資料庫的 DATABASE_URL。填充鏡像需要登入 Garmin 帳號(電子郵件、密碼、MFA 驗證碼);示範模式不需要 Garmin 帳號。遠端用戶端透過 streamable HTTP 並使用儀表板登入的 OAuth 連線。
安裝前請注意
它會把完整的健康紀錄交給語言模型,讀取是透過僅限本人 schema 的唯讀角色進行。症狀是聊天唯一可寫入的資料,且寫入應用程式自身的 schema。抓取器使用 Garmin Connect 的非官方 Web API,Garmin 可能隨時變更或關閉;本專案與 Garmin 無關聯。計算出的準備度、HRV 區間與負荷比率是訓練參考,不是醫療結論。選用的天氣與通知功能會向伺服器外部發出請求。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 hybridlog,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

hybridlog

[Tests] [License: MIT] [Release] [MCP registry]

Self-hosted MCP server on your own Garmin data. It mirrors everything Garmin answers into a database you control, computes the models the watch does not (training load, muscle freshness, readiness in context), and hands all of it to a language model over stdio or HTTP, so the things Garmin cannot measure can be dictated into the chat instead of typed into a form. A dashboard comes with it: the body map, the training load, and the login the connector authenticates against. There is no hosted instance, and that is the point: it is a health record, and the model reads it through a role that can only read.

Running, lifting or both on the same day: the models do not care which. Sessions that alternate running with station work get a lap-by-lap breakdown of their own, which is the shape of a HYROX race.

There is a demo to walk through before installing anything: sign in as [email protected] with the password demo-demo-demo. It runs on generated data and puts itself back every night.

[A muscle map with per-zone load and its findings beside it, one tab away from the training-load view]

Every number in these three is generated by fetcher/seed_demo.py. The interface is English and ships a German translation; it follows the browser language unless the profile says otherwise.

Read this before you rely on it. The fetcher talks to Garmin Connect's unofficial web API. Garmin can change or close it without notice. This project is not affiliated with or endorsed by Garmin. Nothing it computes is a medical statement: readiness, HRV bands, load ratios and the coach texts are training aids, not diagnoses. It is a personal project published in the hope it is useful, with no promise of support or a stable interface.

What it is

  • MCP server: thirteen tools and one prompt over laravel/mcp, in two transports. Local (stdio) for Claude Code and Claude Desktop, hosted (streamable HTTP with OAuth 2.1, PKCE and dynamic client registration) for claude.ai, ChatGPT, Mistral's Le Chat (renamed Vibe), LM Studio and anything else that speaks it.
  • Dashboard: Laravel + Alpine + Chart.js. It draws the body map and the training load, and it is also the login the connector authenticates against and the place where you set what the chat may see.
  • Fetcher: Python (fetcher/), pulls daily metrics and activities via python-garminconnect into one athlete's schema of a PostgreSQL database, which both of the above read through a connection that may only read it, and only that athlete's. Each answer is also kept whole beside the columns read out of it, so a field nobody has built a column for is still there to be asked about, and a day Garmin Connect no longer serves is still in the mirror.

What no hosted instance means in practice: the Garmin session sits in a schema of your own database that no reader role can reach, so the SQL a model writes cannot ask for it, and no operator but you holds it. Past Garmin itself, two optional things reach out at all: the weather, which takes a pair of coordinates you fill in yourself, and notifications, where the push service is woken by an empty POST and never carries what it was about.

The tools

Thirteen tools and one prompt. The right-hand column is the switch at /connect that gates each one; everything is on by default.

ToolWhat it answersNeeds
get-health-summary-toolthe current picture in one call: readiness, sleep, load, data freshnessRead health data
get-insights-toolthe app's own verdict per body system, with the recommendation and the early illness patternRead health data, Read body metrics
get-muscle-map-toolper-zone freshness, weekly volume per zone, what to train todayRead health data
get-training-load-toolCTL/ATL/TSB, the acute:chronic ratio, the weekly stimulus splitRead health data
get-strength-progress-toolweek by week per exercise category: reps, tonnage where it was recorded, top weights, what has not movedRead health data
get-race-splits-toolone session lap by lap: running vs. station work, pace per lap, how far the pace driftedRead health data
get-activity-detail-toolone session in depth: zone minutes and shares against the athlete's zone floors, laps, sets, the heart-rate curve, and how it compares with earlier sessions of its kindRead health data
describe-schema-toolthe mirror's tables and columns, so the model can write its own queryRead health data, Read body metrics
query-health-data-tooleverything else, as one read-only SELECT with a 500-row capRead health data, Read body metrics
refresh-data-toolsyncs today and yesterday from Garmin (the header button's job, narrowed to two days) and waits for itStart a fetch
log-symptom-toola strain mentioned in passing, as a marker on the body mapLog how you feel
delete-symptom-tooltakes one off again once it has healedLog how you feel
give-feedback-toola correction that becomes a standing guideline for the connectorProcess feedback
weekly-report (prompt)drives the Sunday review; the report is the conversation's answer and is stored nowhereRead health data

Most of them answer a question the app was built around. Two do not: describe-schema hands the model the mirror's own tables with the comments that say what each column means, and query-health-data runs the SELECT it writes from that. It is the difference between a set of reports and a database somebody can think in, and it is what makes keeping the raw answers worth the disk: a field nobody has promoted to a column is still one question away.

Reading is the whole of it, with one documented exception. Symptoms are the only thing the chat may write, and they go to the app's own schema, never into the Garmin mirror. Free-form SQL runs through app/Garmin/ReadOnlyGarminQuery: a single SELECT or WITH, a keyword blocklist, a read-only transaction, a row cap, on a connection switched into a role that holds SELECT on one athlete's schema and nothing else.

Quickstart (Docker, demo data, no Garmin account)

bash
git clone https://github.com/Imgrund/hybridlog.gitcd hybridlogcp .env.example .envdocker compose up -d

That brings up PostgreSQL, the dashboard, a queue worker and a scheduler. The published image covers amd64 and arm64, so the first start downloads rather than compiles. After a git pull, plain up -d keeps running the image it already has: the new code arrives only with --build.

Then fill the mirror with 120 days of plausible data, create the account you log in with, and open the dashboard:

bash
docker compose exec app /opt/fetcher/bin/python fetcher/seed_demo.pydocker compose exec app php artisan app:create-user [email protected] --adminopen http://localhost:8080

There is no sign-up page, on purpose: a login nobody can register at has no surface to attack. For anybody but yourself, hand over a link instead of a password:

bash
docker compose exec app php artisan app:invite [email protected] --name="Them"

It prints a one-time link, good for seven days (--days), on which they set their own password. --admin marks the installation owner, who is the account the local stdio transport acts for. Every account keeps its own profile, permissions, symptom log, Garmin sign-in, notifications and mirror, and sees none of anybody else's.

DEMO_MODE=true turns an installation into a shop window instead: one shared account, everything that would reach out of it closed (the Garmin sign-in above all), and php artisan demo:reset putting it back nightly.

A shop window is also the one place worth counting visitors in, and UMAMI_SCRIPT_URL plus UMAMI_WEBSITE_ID render the script tag of an Umami instance you host yourself: page views without a cookie and without an identifier that follows anyone off the site. Both lines are needed, and with either empty, which is the default, nothing is rendered at all and no page requests a host but this one. It is not tied to DEMO_MODE, so a private installation stays free of outside scripts by simply leaving them alone.

Connect an AI

The server reads the same mirror the dashboard draws from, so a chat and the page never disagree. What the page cannot do is answer a question it was not built for: how this week stands against the one before, whether today is a rest day, what a niggle means for tomorrow.

[!IMPORTANT] This hands a language model your health record. It reads through a role that may only read, and only your own schema, but it reads all of it. Which parts is yours to set at /connect, per switch, and the switches take effect on the next tool call rather than the next reconnect.

One address, the same for every client:

https://<your-domain>/mcp/garmin

Claude, ChatGPT, Langdock, Le Chat and LM Studio all take it and run OAuth against the dashboard's own login, so no client ever sees a password and /connect can cut any of them off again. Claude Code and Claude Desktop can skip the deployment entirely and talk to the repository over stdio. docs/connect-ai.md has the steps per client.

Whichever way it connects, it gets the same thirteen tools and the same switches.

Things worth asking, once it is connected:

How did my training week compare with the one before?

Am I ready for a hard session, or do I need a rest day?

Which muscles are fresh, and what should I train today?

Where did my pace go in Saturday's race, and how much of the clock was station work?

My left knee hurt on the box jumps.

Setup with your own Garmin account

Sign in under Garmin in the account menu, or at /connect/garmin: email, password, and the MFA code if Garmin asks for one. What is stored is an OAuth token pair, never the password. The sign-in runs on a queue worker, so one has to be up.

A first sign-in fills the mirror by itself: a ninety-day backfill on the queue, roughly a quarter of an hour, with the page filling in as the history lands. Ninety days is about the minimum for the models to say anything, since the HRV baseline needs three weeks of nights and the load ratios a rolling six weeks. From then on the scheduler fetches three times a day and the Fetch from Garmin button fetches on demand.

Every command works on one athlete (--tenant <user id>, the owner where left out). Details, backfills and the manual login are in docs/install.md; how to record so the data is worth reading is in docs/recording.md.

Documentation

Architecture notes

  • Derived metrics (CTL/ATL/TSB, ACWR fallback, muscle freshness with a ~28 h half-life self-calibrated against 90-day history) are computed on the fly in app/Garmin/.
  • The garmin connection is separate from Laravel's default one. It points at the same database, but is aimed at one athlete's schema per query: App\Garmin\Mirror sets search_path to garmin_t{id} and switches into that tenant's read-only role before the first statement.
  • The interface language is the one thing the athlete still enters, on /profile, stored in the database rather than in .env: it belongs to the athlete rather than to the deployment.
  • Colors follow a validated dataviz palette (light + dark via prefers-color-scheme), roles defined in resources/css/app.css.

Contributing

Releases are tagged, and CHANGELOG.md says what changed in each one. CONTRIBUTING.md says how to run the suite, what the tests will hold you to, and which changes are deliberately out of scope. Conduct is covered in CODE_OF_CONDUCT.md. Security problems do not belong in an issue: SECURITY.md has the private path.

License

MIT, see LICENSE. The muscle polygons in resources/data/body-polygons.json come from the body-highlighter project family and are MIT-licensed as well.

Two bundled assets carry their own terms. The 3D figure resources/models/body-zones.glb is an adaptation of BodyParts3D (© The Database Center for Life Science) and stays under CC Attribution-Share Alike 2.1 Japan; resources/models/CREDITS.md carries the full credit and what the license binds. The Mulish and Oswald fonts in resources/fonts/ are under the SIL Open Font License 1.1, whose texts ship next to them.

來源:README.md,提交 958a8ca

工具

0
SourceWeft 尚未讀取此伺服器的工具。

版本歷史

1
  1. v0.1.13最新Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通必需25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式必需10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分