Skip to main content
Glama
Novrax

fitbit-googlehealth-mcp

by Novrax

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 (HEALTH_PROVIDER=google)

Legacy backend

Fitbit Web API, still present behind HEALTH_PROVIDER=fitbit, on borrowed time

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 install

2. Create the Google Cloud project

  1. Create or pick a projecthttps://console.cloud.google.com/projectcreate

  2. Enable the Google Health APIhttps://console.cloud.google.com/apis/api/health.googleapis.com

  3. Configure the OAuth consent screenhttps://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.

  4. Add the scopeshttps://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).

  5. Create an OAuth client IDhttps://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-here

No quotes, no trailing spaces. .env is gitignored. Then:

pnpm run setup:google

Your 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.dev

6. Add to Claude

  1. On claude.ai: Settings → Connectors → Add custom connector

  2. URL: https://fitbit-googlehealth-mcp.<your-subdomain>.workers.dev/mcp/<MCP_SHARED_SECRET>

  3. Authentication: none — the secret is already in the URL path

  4. 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 (setup:google, probe:google)

.env in the repo root

You create it from .env.example. Gitignored.

The deployed Worker on Cloudflare

Cloudflare Workers Secrets

pnpm wrangler secret put GOOGLE_CLIENT_ID — encrypted at rest, never in the repo

Running the Worker locally with pnpm dev

.dev.vars in the repo root

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:google

The 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:provider

It 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

get_profile

Identity, units, timezone

list_devices

Paired devices, battery, last sync

get_daily_summary

date?

Steps, calories, distance, active minutes, resting HR

get_activity_timeseries

resource, start, end

steps / distance / calories / floors / active-minute levels

get_exercise_list

beforeDate?, limit?

Workout sessions

get_heart_rate_range

start, end

Daily resting heart rate

get_heart_rate_intraday

date, detailLevel

Down-sampled from raw samples

get_sleep

date?

Sessions with stage breakdown

get_sleep_range

start, end

get_body_log

start, end

Weight and body fat

get_food_log

date?

Food and water with macros

get_spo2

start, end

get_respiratory_rate

start, end

get_skin_temperature

start, end

Deviation from baseline

get_hrv

start, end

get_cardio_fitness

date?

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 int64 field is serialised as a string. {"count": "1250"}, not 1250.

  • Filter literals differ by time field. Civil (wall-clock) times take no Z; physical instants require one; daily types take a bare YYYY-MM-DD.

  • Ranges are closed-open. The API supports only >= and <, so an inclusive end date has to be advanced by a day.

  • sleep filters on end time only (sleep.interval.civil_end_time).

  • Rollups return rollupDataPoints, not dataPoints, and paginate by re-POSTing the body with a pageToken.

  • windowSizeDays is 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_intraday down-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-level totalFat / totalCarbohydrate fields, not nutrients[] entries.

  • Delete takes a resource name, not an id. The numeric logId in 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-rate row arrives as {dailyRestingHeartRate: {...}}. Reading the top level typechecks fine and yields undefined for every field.

  • Rollup buckets are dated by civilStartTime, not date.

  • activity-level supports neither rollup verb — the periods must be listed and summed client-side.

  • active-zone-minutes rollups carry flat sumIn<Zone>HeartZone keys, 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:

  1. The <MCP_SHARED_SECRET> at the end of the URL path must match (constant-time compare), or 401.

  2. CF-Connecting-IP must fall inside ALLOWED_CIDRS, or 403. Anthropic's published outbound range is 160.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 test

Development notes

Hosted pages

GitHub Pages serves the three URLs Google's OAuth consent screen requires:

Field on the consent screen

URL

Application home page

https://novrax.github.io/fitbit-googlehealth-mcp/

Privacy policy link

https://novrax.github.io/fitbit-googlehealth-mcp/privacy.html

Terms of service link

https://novrax.github.io/fitbit-googlehealth-mcp/terms.html

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

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Gives 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
  • A
    license
    A
    quality
    B
    maintenance
    Provides 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.
    6
    1
    MIT