Skip to main content
Glama
lukendatigh

google-health-mcp-server

README.md
# google-health-mcp-server

A complete, open-source single tenant [MCP](https://modelcontextprotocol.io) server for the
[Google Health API](https://developers.google.com/health) v4 -- the OAuth-based successor to the
Fitbit Web API. Every method reachable with just a user's own OAuth grant is exposed as a tool
below, including profile/settings, paired devices, raw data points, source reconciliation, and
both physical- and civil-time rollups.

Runs two ways:
- **Locally over stdio** -- for Claude Desktop / Claude Code, zero hosting required.
- **Remotely over HTTP** -- for [claude.ai](https://claude.ai) / Cowork connectors, deployable to Fly.io with the included Dockerfile.

View the full product and technical design [here](PRD-TDD.md).

## Tools

17 tools cover every non-Cloud-IAM method in the API. Full list with exact endpoint coverage: [docs/tools.md](docs/tools.md).

| Read | Write |
|---|---|
| `get_identity` | `update_profile` |
| `get_profile`, `get_irn_profile` | `update_settings` |
| `get_settings` | `create_data_point`, `update_data_point` |
| `list_paired_devices`, `get_paired_device` | `batch_delete_data_points` |
| `list_data_points`, `get_data_point` | |
| `reconcile_data_points` | |
| `roll_up_data_points`, `daily_roll_up_data_points` | |
| `export_exercise_tcx` | |

## One-time setup: getting a refresh token

This server acts on behalf of one Google account (yours). Before running it, you need an OAuth
client and a refresh token for that account:

1. Follow [Set up Google Cloud and OAuth](https://developers.google.com/health/setup) to enable
   the **Google Health API** on a Google Cloud project and create a **Web application** OAuth 2.0
   Client ID. Use `https://developers.google.com/oauthplayground` as an authorized redirect URI if
   you plan to use the OAuth Playground for the next step (or run your own short local script).
2. Add yourself as a **test user** for the OAuth consent screen, and add the
   [scopes](https://developers.google.com/health/scopes) you want this instance to use (e.g.
   `.../auth/googlehealth.activity_and_fitness.readonly`, `.../auth/googlehealth.sleep.readonly`).
3. Using the [OAuth 2.0 Playground](https://developers.google.com/oauthplayground) (gear icon →
   "Use your own OAuth credentials" → paste your client ID/secret), authorize the scopes from step
   2 against your Google account, then exchange the authorization code for tokens. Copy the
   **refresh token** it returns.
4. You now have the three values this server needs: `GOOGLE_HEALTH_CLIENT_ID`,
   `GOOGLE_HEALTH_CLIENT_SECRET`, `GOOGLE_HEALTH_REFRESH_TOKEN`.

Note: while your OAuth consent screen is in **Testing** status, refresh tokens expire after 7
days and need to be re-minted. Publish the consent screen (or request verification) for a
long-lived refresh token -- see [Token behavior during testing](https://developers.google.com/health/setup#token_behavior_during_testing).

## Quick start (local, stdio)

Requires Node.js 20+.

```bash
npx google-health-mcp-server
```

Point your MCP client (Claude Desktop, Claude Code, etc.) at it. For Claude Desktop, add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "google-health": {
      "command": "npx",
      "args": ["google-health-mcp-server"],
      "env": {
        "GOOGLE_HEALTH_CLIENT_ID": "your-client-id",
        "GOOGLE_HEALTH_CLIENT_SECRET": "your-client-secret",
        "GOOGLE_HEALTH_REFRESH_TOKEN": "your-refresh-token",
        "GOOGLE_HEALTH_READ_ONLY": "false"
      }
    }
  }
}
```

## Configuration

| Env var | Required | Default | Description |
|---|---|---|---|
| `GOOGLE_HEALTH_CLIENT_ID` | yes | -- | OAuth 2.0 client ID from your Google Cloud project. |
| `GOOGLE_HEALTH_CLIENT_SECRET` | yes | -- | OAuth 2.0 client secret paired with the client ID. Never logged, never echoed in tool output. |
| `GOOGLE_HEALTH_REFRESH_TOKEN` | yes | -- | Refresh token for the one Google account this instance acts on behalf of. Never logged, never echoed in tool output. |
| `GOOGLE_HEALTH_READ_ONLY` | no | `false` | When `true`, every mutation tool is hidden from `tools/list` entirely. Recommended for any instance you don't fully trust the client of. |
| `GOOGLE_HEALTH_API_BASE_URL` | no | `https://health.googleapis.com/v4` | Override for testing against a mock server. |
| `GOOGLE_HEALTH_TOKEN_URL` | no | `https://oauth2.googleapis.com/token` | Override for testing against a mock token endpoint. |

HTTP mode (below) needs three more: `PORT`, `PUBLIC_URL`, `MCP_HTTP_PASSWORD`. See [.env.example](.env.example) for all of them with descriptions.

## Remote (HTTP) deployment on Fly.io

The HTTP transport is gated by OAuth (required for claude.ai/Cowork connector approval). This is
**single-tenant OAuth**, and it is a separate concern from the Google OAuth grant above: it just
gates access to *your instance* behind one shared password, not per-user Google accounts. See
[docs/architecture.md](docs/architecture.md#two-independent-oauth-layers) for why and how.

1. Install [flyctl](https://fly.io/docs/flyctl/install/) and `fly auth login`.
2. `fly launch --no-deploy` from this directory (it will read `fly.toml`; rename the `app` there first if you want a specific subdomain).
3. Set secrets (never put these in `fly.toml`, which is committed to git):
   ```bash
   fly secrets set GOOGLE_HEALTH_CLIENT_ID=your-client-id
   fly secrets set GOOGLE_HEALTH_CLIENT_SECRET=your-client-secret
   fly secrets set GOOGLE_HEALTH_REFRESH_TOKEN=your-refresh-token
   fly secrets set MCP_HTTP_PASSWORD=choose-a-strong-password
   ```
4. Edit `fly.toml`'s `PUBLIC_URL` to match your actual `*.fly.dev` hostname (or custom domain), then:
   ```bash
   fly deploy
   ```
5. In claude.ai / Cowork, add a custom connector pointing at `https://<your-app>.fly.dev/mcp`. You'll be redirected to a login page on your own instance -- enter `MCP_HTTP_PASSWORD` to approve the connection.

Notes:
- Single `shared-cpu-1x` machine, no volume. Both the cached Google access token and the
  downstream OAuth session state (registered clients, tokens) live in memory and are lost on
  redeploy or restart -- you'll just need to reconnect the connector afterwards (the Google refresh
  token itself is unaffected; it's re-exchanged for a fresh access token automatically). Acceptable
  trade-off for a low-traffic personal instance; see the TDD.
- `fly.toml`'s `[http_service]` is configured to stay always-on (not scaled to zero) for exactly that reason -- a stop/start cycle would otherwise force reconnection too.

## Development

```bash
npm install
npm run dev:stdio      # run src/stdio.ts directly with tsx
npm run dev:http       # run src/http.ts directly with tsx
npm run typecheck
npm run lint            # biome check
npm run lint:fix
npm test
npm run build           # bundles dist/stdio.js and dist/http.js with tsup
```

See [docs/architecture.md](docs/architecture.md) for the repo layout and request-flow details.

## License

MIT -- see [LICENSE](LICENSE).