fitbit-googlehealth-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@fitbit-googlehealth-mcplog my lunch from this photo and estimate calories"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
fitbit-googlehealth-mcp
A Model Context Protocol (MCP) server for your Google Health data (Fitbit's successor). Reads your health metrics and writes food, weight, activity and sleep logs. TypeScript, deployed to Cloudflare Workers, connected to Claude Desktop / mobile / claude.ai as a custom connector.
Built for single-user personal use: fork it and run it on your own Google Cloud project and Cloudflare account.
Status — read this first
The legacy Fitbit Web API (api.fitbit.com) is being decommissioned in September 2026, and Google is no longer issuing new Fitbit developer accounts. This server therefore targets the Google Health API (health.googleapis.com/v4) by default.
Default backend | Google Health API v4 ( |
Legacy backend | Fitbit Web API, still present behind |
Written against | v4 discovery document, revision 20260909 |
Verified against live Google Health data on 2026-09-10: all 17 read methods return real
values from a Fitbit Air. pnpm run verify:provider re-runs that check against your own
account, and pnpm run probe:google dumps the raw API shapes if you need to debug a
specific data type.
Related MCP server: google-health-mcp
What it does
Read (16 tools) — activity and steps, heart rate (daily + intraday), sleep with stages, weight and body fat, food and water logs, SpO2, respiratory rate, skin temperature, HRV, VO2 max, paired devices.
Write (7 tools) — food, water, weight, body fat, activity and sleep logs.
Delete (6 tools) — remove individual entries.
Meal presets (4 tools) — reusable nutrition profiles stored in Workers KV.
⭐
log_meal_photo— attach a meal photo in Claude, Claude estimates the nutrition visually, and the items are written to your food log in one call.
Prerequisites
A Google account holding your health data (a Fitbit account merged into Google).
A Google Cloud project with the Google Health API enabled — free.
A Cloudflare account — the free plan is enough.
A Claude account — custom connectors must be added from claude.ai on the web, then sync to mobile.
Node.js 20+ and pnpm 9+ locally.
You do not need a Fitbit developer account. If you already made one, it is only useful for the legacy HEALTH_PROVIDER=fitbit path, which stops working this month.
Setup
1. Clone and install
git clone <your-fork-url>
cd fitbit-googlehealth-mcp
pnpm install2. Create the Google Cloud project
Create or pick a project — https://console.cloud.google.com/projectcreate
Enable the Google Health API — https://console.cloud.google.com/apis/api/health.googleapis.com
Configure the OAuth consent screen — https://console.cloud.google.com/auth/audience
User type: External
Add your own Google account under Test users
Publish the app so its status is "In production". This matters: while the app sits in Testing, Google expires refresh tokens after 7 days, and the Worker will break every week. Publishing does not require Google's security review — that is only needed above 100 users.
Add the scopes — https://console.cloud.google.com/auth/scopes — search "Google Health API" and add read and write for activity & fitness, health metrics & measurements, sleep and nutrition, plus profile and settings (read).
Create an OAuth client ID — https://console.cloud.google.com/apis/credentials
Application type: Desktop app
Copy the Client ID and Client secret
3. Authorize
Copy the template and paste your two values into it:
cp .env.example .env # macOS / Linux / Git Bash
copy .env.example .env # Windows cmd / PowerShell# .env
GOOGLE_CLIENT_ID=1234567890-abc123.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-secret-hereNo quotes, no trailing spaces. .env is gitignored. Then:
pnpm run setup:googleYour browser opens Google's consent screen. Approve it, and the script prints the exact wrangler commands for the next step.
The script reads real environment variables first, so these work too — the syntax just differs per shell:
# PowerShell
$env:GOOGLE_CLIENT_ID = "..."
$env:GOOGLE_CLIENT_SECRET = "..."# bash / zsh / Git Bash
export GOOGLE_CLIENT_ID=...
export GOOGLE_CLIENT_SECRET=...:: Windows cmd
set GOOGLE_CLIENT_ID=...
set GOOGLE_CLIENT_SECRET=...Consent is collected here, in a real browser, on purpose: Google blocks OAuth inside embedded WebViews (disallowed_useragent), which is what Claude mobile would use.
4. Push to Cloudflare
cp wrangler.toml.example wrangler.toml
# then check TIMEZONE in wrangler.toml — it decides what "today" means for
# every tool with an optional date. Ships as "Europe/London".
pnpm wrangler kv namespace create TOKENS
pnpm wrangler kv namespace create CACHE
# paste the returned ids into wrangler.toml
pnpm wrangler secret put GOOGLE_CLIENT_ID
pnpm wrangler secret put GOOGLE_CLIENT_SECRET
openssl rand -hex 32 | pnpm wrangler secret put MCP_SHARED_SECRET
# tokens — copy the exact commands printed by setup:google (--remote matters)
pnpm wrangler kv key put --remote --binding=TOKENS google_refresh_token '<paste>'
pnpm wrangler kv key put --remote --binding=TOKENS google_access_token '<paste>'
pnpm wrangler kv key put --remote --binding=TOKENS google_expires_at '<paste>'5. Deploy
pnpm deploy
# → https://fitbit-googlehealth-mcp.<your-subdomain>.workers.dev6. Add to Claude
On claude.ai: Settings → Connectors → Add custom connector
URL:
https://fitbit-googlehealth-mcp.<your-subdomain>.workers.dev/mcp/<MCP_SHARED_SECRET>Authentication: none — the secret is already in the URL path
Save; it syncs to Claude Desktop and mobile automatically
New connectors cannot be added from Claude mobile — use the web.
Where secrets live
Three separate places, for three separate purposes. This trips people up, so:
Purpose | Where | How it gets there |
Running the local helper scripts ( |
| You create it from |
The deployed Worker on Cloudflare | Cloudflare Workers Secrets |
|
Running the Worker locally with |
| You create it. Gitignored. |
The deployed Worker never reads .env — Cloudflare doesn't upload it. If you skip
wrangler secret put, the Worker deploys fine and then fails at runtime with a
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are not set error.
Your Google refresh token is not in any of these. It lives in the Workers KV TOKENS
namespace, put there by the wrangler kv key put commands in step 4.
Verifying
# easiest: paste the access token setup:google printed into .env as
# GOOGLE_ACCESS_TOKEN=ya29...
pnpm run probe:google
# or pass it inline (bash / Git Bash)
GOOGLE_ACCESS_TOKEN=ya29... pnpm run probe:google
# or against the token the deployed Worker is using
GOOGLE_ACCESS_TOKEN=$(pnpm wrangler kv key get --remote --binding=TOKENS google_access_token) \
pnpm run probe:googleThe probe is read-only. For each endpoint it prints ✓ with the value fields that came back, · if reachable but empty, or ✗ with the API's error. A 403 means that scope was not granted — add it on the Data Access page and re-run setup:google.
To check the provider itself rather than the raw API — that every read method returns
sensible values, not undefined from a wrong field path:
pnpm run verify:providerIt calls all 17 read methods against your live account and prints a preview of each
result. Read-only; it never writes or deletes. Access tokens last about an hour, so
refresh GOOGLE_ACCESS_TOKEN in .env if it starts returning 401.
Tools
Read (16)
Tool | Arguments | Notes |
| — | Identity, units, timezone |
| — | Paired devices, battery, last sync |
|
| Steps, calories, distance, active minutes, resting HR |
|
| steps / distance / calories / floors / active-minute levels |
|
| Workout sessions |
|
| Daily resting heart rate |
|
| Down-sampled from raw samples |
|
| Sessions with stage breakdown |
|
| |
|
| Weight and body fat |
|
| Food and water with macros |
|
| |
|
| |
|
| Deviation from baseline |
|
| |
|
| VO2 max |
Write (7)
log_food · log_meal_photo · log_water · log_weight · log_body_fat · log_activity · log_sleep
Delete (6)
delete_food_log · delete_water_log · delete_weight_log · delete_body_fat_log · delete_activity_log · delete_sleep_log
Meal presets (4)
save_meal_preset · list_meal_presets · log_preset · delete_meal_preset
33 tools total. Every optional date falls back to today.
Architecture
Claude mobile / Desktop / Web
│ (public URL, Streamable HTTP)
▼
Anthropic Cloud (outbound CIDR 160.79.104.0/21)
│
▼
Cloudflare Workers /mcp/<SECRET>
├─ guard middleware (SECRET + CIDR allowlist)
├─ @hono/mcp Streamable HTTP transport
└─ McpServer
├─ HealthProvider interface
│ ├─ GoogleHealthProvider ← default
│ │ ├─ Google OAuth refresh (Workers KV: TOKENS)
│ │ └─ GoogleHealthClient (pagination, 401/429 retry)
│ └─ FitbitProvider ← legacy, sunsetting
└─ tools/read/*, tools/write/*
└─ getCached → Workers KV: CACHE (TTL 1h)Images never reach the server: Claude analyses the photo and passes structured items[].
Notes on the Google Health API
Things that differ from Fitbit and cost time if you hit them cold:
Every
int64field is serialised as a string.{"count": "1250"}, not1250.Filter literals differ by time field. Civil (wall-clock) times take no
Z; physical instants require one; daily types take a bareYYYY-MM-DD.Ranges are closed-open. The API supports only
>=and<, so an inclusive end date has to be advanced by a day.sleepfilters on end time only (sleep.interval.civil_end_time).Rollups return
rollupDataPoints, notdataPoints, and paginate by re-POSTing the body with apageToken.windowSizeDaysis documented as optional but is required — omitting it returns HTTP 400.Rollup ranges are capped: 14 days for heart rate, total calories, active minutes and calories-in-HR-zone; 90 days for everything else.
List pages cap at 25 rows for sleep and exercise, 10000 elsewhere.
No intraday detail levels. Google exposes raw ~5-second samples;
get_heart_rate_intradaydown-samples client-side.Skin temperature is absolute °C plus a baseline; Fitbit reported only the deviation, so this server derives it.
Nutrient enum is
SUGAR, singular. Fat and carbohydrate are top-leveltotalFat/totalCarbohydratefields, notnutrients[]entries.Delete takes a resource name, not an id. The numeric
logIdin these tools is a stable hash of that name, resolved by scanning the last 35 days.A DataPoint nests its values under a camelCase key named for the data type, so a
daily-resting-heart-raterow arrives as{dailyRestingHeartRate: {...}}. Reading the top level typechecks fine and yieldsundefinedfor every field.Rollup buckets are dated by
civilStartTime, notdate.activity-levelsupports neither rollup verb — the periods must be listed and summed client-side.active-zone-minutesrollups carry flatsumIn<Zone>HeartZonekeys, not an array of zone objects, and no zone bounds.Instant-valued types are filtered in true UTC, so selecting a local day means converting local midnight to UTC first — not using UTC midnight.
Security
Single-user design, two layers:
The
<MCP_SHARED_SECRET>at the end of the URL path must match (constant-time compare), or 401.CF-Connecting-IPmust fall insideALLOWED_CIDRS, or 403. Anthropic's published outbound range is160.79.104.0/21.
MCP_SHARED_SECRET lives in Workers Secrets, never in code. Rotating it is wrangler secret put plus updating the URL in claude.ai; your Google tokens are unaffected.
Threat model: if the secret leaks and the attacker can reach you from inside Anthropic's CIDR, they can read your health data and write false entries. They cannot take over the Google account — the refresh token stays in the Worker.
Local development
echo 'MCP_SHARED_SECRET=dev-secret' > .dev.vars
pnpm dev
pnpm lint
pnpm typecheck
pnpm testDevelopment notes
docs/research.md— original design research (Japanese), including the Fitbit-era API findingsdocs/journal.md— development log (Japanese)scripts/probe-google-health.ts— ground-truth probe against the live APIscripts/diagnose-food-log.ts— legacy Fitbit food-log reproducer
Hosted pages
GitHub Pages serves the three URLs Google's OAuth consent screen requires:
Field on the consent screen | URL |
Application home page |
|
Privacy policy link |
|
Terms of service link |
|
Add github.io under Authorized domains on the same screen. Sources are in
docs/.
Credits
Derived from tachibanayu24/fitbit-googlehealth-mcp (MIT), which implemented the original Fitbit Web API server. The Google Health API provider, the timezone handling and the OAuth bootstrap for Google are additions.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Connect your health, fitness, nutrition, sleep, and wearable data to your AI assistant.
Read wearables and lab health data — sleep, activity, workouts, timeseries, lab tests and orders.
Garmin data in Claude & ChatGPT via the Garmin Health API. OAuth sign-in, no password sharing.
WHOOP recovery, strain, sleep and workouts in Claude via official WHOOP OAuth. Free, open source.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables Claude to access and query Oura Ring health data including sleep, activity, readiness, heart rate, and more via the Oura API.MIT
- AlicenseNot gradedqualityCmaintenanceGives Claude read access to wearable health data from Fitbit or Wear OS devices via the Google Health API, exposing tools for metrics like steps, heart rate, sleep, and workouts, plus a computed recovery score.MIT
- AlicenseAqualityBmaintenanceProvides Claude with real-time access to local health data including sleep, recovery, strain, and workouts from WHOOP and Apple Health, enabling informed context-aware interactions.61MIT
- AlicenseNot gradedqualityBmaintenanceEnables Claude to act as a personal health coach by connecting to Garmin wearable data and Notion workspace for automated calorie tracking, photo food logging, and coaching insights.MIT