google-health-mcp
by metacurb
README.md
# Google Health MCP
Local, read-only access to recorded workouts through Google Health API v4. Node.js 22.14+ and TypeScript. Three runtime dependencies: the official MCP SDK, Zod and an XML parser.
Each user sets up their own Google Cloud project and OAuth client, then signs in with the Google account that holds their workout data.
## Requirements
- Node.js 22.14 or later, npm and Git.
- A Google account with recorded workouts available through Google Health API.
- Permission to create a Google Cloud project and configure its OAuth client.
- A local desktop browser for sign-in and an MCP client that supports stdio, such as Codex.
The Google Cloud CLI (`gcloud`) is not required. There is no hosted service to deploy.
## 1. Clone and build
```sh
git clone https://github.com/metacurb/google-health-mcp.git
cd google-health-mcp
npm ci
npm run build
```
Run the remaining terminal commands from this repository directory.
## 2. Create a Google Cloud project
1. Open [Google Cloud Console](https://console.cloud.google.com/) and sign in.
2. Open the project selector, choose **New project**, enter a name such as `Health MCP`, select the available organization/location, and click **Create**.
3. Select the new project. Keep it selected throughout setup.
4. Open **APIs & Services → Library**, search for **Google Health API**, open it and click **Enable**.
These steps follow the [Google Health project setup guide](https://developers.google.com/health/codelabs/make-your-first-api-call).
## 3. Configure OAuth consent and access
1. Open **Google Auth Platform**. If prompted, click **Get started**.
2. Enter an app name, a user support email and a developer contact email. Choose **External** for a personal Google account. Complete the required consent-screen fields and acknowledge Google's policy to create the configuration.
3. Under **Audience**, keep the publishing status as **Testing** for initial setup.
4. Under **Test users → Add users**, add the exact Google account email you will use for health-data sign-in, then **Save**. This is required even if you own the Cloud project.
See Google's [consent-screen setup](https://developers.google.com/health/codelabs/make-your-first-api-call) and [test-user instructions](https://developers.google.com/health/setup#add-test-users).
For ongoing personal use, **Audience → Publish app** changes the OAuth app to **In production**. This removes the test-user allowlist and Testing's seven-day refresh-token expiry. Unverified-app warnings and a 100-user cap still apply. Personal use can qualify for a verification exception; distributing one shared OAuth app to the public has separate verification requirements. Publishing the OAuth configuration keeps this MCP local. See [OAuth app states](https://developers.google.com/identity/protocols/oauth2/production-readiness/overview), [token expiry](https://developers.google.com/health/setup#token-behavior-during-testing) and the [personal-use exception](https://developers.google.com/identity/protocols/oauth2/production-readiness/restricted-scope-verification#personal-use).
## 4. Add the read-only scopes
Open **Google Auth Platform → Data Access → Add or remove scopes**. Find **Google Health API** and select exactly these three scopes, then click **Update** and **Save**:
```text
https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly
https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly
https://www.googleapis.com/auth/googlehealth.location.readonly
```
Activity access reads workouts; health-metrics access reads heart rate; location access permits optional TCX retrieval. GPS coordinates are excluded from tool output unless `include_gps` is true. The helper requests all three scopes and rejects incomplete or broader grants. See [Google's scope configuration instructions](https://developers.google.com/health/setup#add-scopes).
## 5. Create a Desktop OAuth client
1. Open **Google Auth Platform → Clients → Create client**.
2. Choose **Desktop app** as the application type, enter a client name and click **Create**.
3. Download the client's JSON file. Rename it to `credentials.json` and place it in the repository root, beside `package.json`.
This application requires a Desktop client with an `installed` section in its JSON file. The authentication helper checks the file type without printing its contents. Google Health's REST codelab uses a Web client; this project's local login uses the [Desktop loopback flow](https://developers.google.com/identity/protocols/oauth2/native-app#loopback-ip-address). The helper creates its callback on a random local port, so no hosted redirect URL needs to be configured. Keep the downloaded file private; it is already in `.gitignore`.
## 6. Sign in and test API access
```sh
npm run auth
npm run smoke
```
`auth` opens the system browser. Select the account with your workout data and grant all three read-only permissions. Complete sign-in within five minutes. Valid saved credentials are reused. After changing the OAuth app's publishing status, use `npm run auth -- --force` to obtain a fresh grant.
`smoke` lists up to five workouts from the past 30 days, gets the newest workout, then retrieves up to five available heart-rate samples. Use `npm run smoke -- --days=90` for a wider window. It prints health data to your terminal and saves no files. Exit code 2 means there were no workouts, so the heart-rate call could not be tested. Exit code 1 means a request or authentication failed. A successful request can still report missing heart-rate data.
## 7. Connect an MCP client
The server starts with `node dist/index.js` and waits for MCP messages on stdin. It does not open a web page. The MCP client normally starts and stops this process. `npm run --silent start` also works; avoid plain `npm start` in client configuration because its script banner would enter the protocol.
For Codex, run this from the repository directory in a macOS/Linux shell:
```sh
codex mcp add google-health -- "$(node -p 'process.execPath')" "$(pwd)/dist/index.js"
```
This records absolute paths to Node and the built server. To configure paths manually, run `node -p 'process.execPath'` and `node -p 'require("node:path").resolve("dist/index.js")'` from the repository. Copy the results into your client's stdio command and argument fields.
For example, in Codex's `config.toml`, replace both placeholder paths:
```toml
[mcp_servers.google-health]
command = '/absolute/path/to/node'
args = ['/absolute/path/to/google-health-mcp/dist/index.js']
tool_timeout_sec = 90
```
On Windows, use the paths printed by Node, including `node.exe`. The single-quoted TOML strings accept Windows backslashes. Other MCP clients need the same executable and server argument. No credentials or token environment variables belong in the MCP configuration.
Restart the MCP connection after registration or rebuilding. Try asking: “List my workouts from last month, including hikes.” See [Codex MCP configuration](https://developers.openai.com/codex/mcp/) for client setup details.
## Credential storage and privacy
Credentials are read explicitly from `~/.config/gcloud-health/application_default_credentials.json`. Valid credentials for this project's client are reused. Run `npm run auth` when sign-in is needed; `npm run auth -- --force` forces a new grant. The MCP server does not open a browser itself. On POSIX systems, the directory uses mode 0700 and new credential files use 0600. Access tokens are held in memory and refreshed on demand. The application rejects grants with missing or extra scopes.
Here, `~` means the current operating-system user's home directory; the `.config/gcloud-health` directory is also used on Windows. There is one saved OAuth client/account per OS user. Signing in with another client replaces the credentials in that isolated directory.
Authentication uses PKCE and a temporary callback bound to `127.0.0.1`. The callback stops when login ends. No gcloud command runs during authentication or MCP use. Existing gcloud configuration, default application credentials, shell startup files, `GOOGLE_APPLICATION_CREDENTIALS` and metadata-server credentials are not used or changed. Tool results are sent to the MCP client that requested them; that client's data-handling policy also applies.
No health write endpoints exist in this client. Health requests use GET only. The OAuth token exchange uses POST to Google's token endpoint. HTTP redirects are refused. Errors expose HTTP status and known Google error codes, never raw response bodies, OAuth URLs, client credentials or tokens. Diagnostics go to stderr.
## Setup troubleshooting
| Problem | Action |
| --- | --- |
| Google allows only developer-approved testers | In the OAuth client's project, add the exact sign-in email under **Audience → Test users**, or use the personal-use publishing option above. Start a fresh login. |
| `CLIENT_FILE_MISSING` or `INVALID_CLIENT_FILE` | Download a Desktop OAuth client JSON file and save it as `credentials.json` beside `package.json`. |
| `AUTH_REQUIRED`, `invalid_grant` or expired consent | Run `npm run auth -- --force` and complete sign-in again. |
| `SCOPE_MISMATCH` | Check the three Data Access scopes above, then sign in again and grant all three. |
| HTTP 403 or `SERVICE_DISABLED` | Confirm Google Health API is enabled in the same project as the downloaded client, and check account/organization access restrictions. |
| `LOGIN_TIMEOUT` or `BROWSER_FAILED` | Run authentication in a local desktop terminal with a working browser and complete the new sign-in within five minutes. |
| No workouts returned | Check the selected Google account and date range; try `npm run smoke -- --days=90`. |
| `INVALID_PAGE_TOKEN` | See the known workout-pagination limitation below. |
| TCX `RESPONSE_TOO_LARGE` | Retry with a larger `max_tcx_bytes`, up to `10485760` (10 MiB). If the client still rejects this value, rebuild and restart the MCP connection. Larger exports remain unsupported. |
## Tools
| Tool | Inputs | Result |
| --- | --- | --- |
| `list_workouts` | `start_date`, `end_date`; optional `activity_types`, `page_size` (1–25), `page_token` | Recorded summaries and next page token |
| `get_workout` | `workout_name`; optional `split_offset`, `split_limit` (1–100) | Summary, both split collections and data availability |
| `get_workout_series` | `workout_name`; optional `measurements`, `max_points_per_series` (1–500), `page_tokens`, `include_tcx`, `include_gps`, `max_tcx_bytes` (1024–10485760; default 1048576) | Detailed samples, a page token for each measurement, optional TCX |
Example list arguments:
```json
{"start_date":"2026-08-01","end_date":"2026-09-01","page_size":25}
```
Use the exact `name` returned by that tool as `workout_name`. Supported measurements are `heart_rate` (default), `distance`, `steps`, `active_energy_burned`, `active_zone_minutes` and `altitude` (elevation gain). To get multiple types, pass them in `measurements`. To retrieve TCX, set `include_tcx: true`; add `include_gps: true` only when you want coordinates. Default TCX output removes coordinates, vendor extensions and metadata while retaining standard measurements. Oversized TCX produces an explicit error rather than broken, truncated XML. No exports are saved.
To read the original TCX with GPS and vendor extensions preserved, use:
```json
{
"workout_name": "<name returned by list_workouts>",
"include_tcx": true,
"include_gps": true,
"max_tcx_bytes": 10485760,
"max_points_per_series": 1
}
```
TCX defaults to a 1 MiB limit; `max_tcx_bytes` can raise it to 10 MiB per call. The limit applies to both the downloaded XML and returned XML. With `include_gps: true`, valid TCX is returned unchanged (`transformation: none`), including any recorded coordinates. This cannot add GPS data that Google did not supply. No scope change or new sign-in is needed if the three required read-only scopes were already granted. GPS remains opt-in for other calls.
The source limits are defined in [`src/limits.ts`](src/limits.ts). After changing them, run `npm test` and restart the MCP connection so the client loads the new schema and server. A currently running process keeps its old limits until restarted.
Data rules:
- Resource names can contain Google's numeric user ID or `me`. The client preserves returned names and uses `users/me` for outgoing requests. A different workout ID or conflicting numeric user ID in a response is rejected.
- List dates use the workout's recorded local time: inclusive start, exclusive end, at most 366 days. All activity types, including `HIKING`, are included by default. An exact `activity_types` filter runs within each API page; an empty page can still have a next page. Keep the same filters and page size when paging.
- Activity labels are never reclassified. `splitSummaries` and default `splits` remain separate named collections; they can describe overlapping segments.
- Metrics include units and timestamps. Google integer strings remain strings to avoid precision loss. Missing metrics are `null`; recorded zero stays zero. `source: recorded` means Google supplied the value, which may itself be calculated by Google. Local calculations appear under `derivedMetrics` with their formulas. Derived pace uses recorded active time, never elapsed time.
- `get_workout` marks telemetry and TCX as `not_checked`; a summary metric does not prove detailed samples exist. Series results distinguish `available`, `missing`, `unknown_more_pages` and `error`.
- Detailed streams are selected by the workout's time window, not an activity foreign key. They may contain overlapping device sources or paused periods. Samples and interval starts use `[startTime, endTime)`. Interval measurements are not clipped, prorated, deduplicated or summed. A returned page is not a whole-workout total.
- Each selected series has its own page token. Continue with that series in `measurements` and its token in `page_tokens`. Up to six series are read per call. JSON responses are capped at 4 MiB per request and TCX at the requested byte limit. Each serialized MCP result is capped at 21 MiB before its text copy (up to about 42 MiB combined), allowing room for escaped XML and measurements. A request times out after 20 seconds; only a 401 gets one token refresh and retry.
## Verification
Run `npm test` to build and execute the tests. Google credentials and live API access are not required for these tests.
See [TESTING.md](TESTING.md) for the exact live-access status. Tests use invented TypeScript fixtures and injected HTTP responses. They never load personal health exports.
**Known Google limitation observed on 2026-09-07:** workout listing returned a next-page token that Google rejected with HTTP 400, `Invalid page token`. The client reports `INVALID_PAGE_TOKEN`; it does not treat a failed page as complete. Use narrower date ranges and inspect `nextPageToken` for possible incomplete results. Heart-rate pagination passed live checks. This limitation can still prevent a complete workout history from being retrieved.
Credentials, tokens, `.env` files, health export formats and data directories are ignored by Git. JSON files are ignored by default except the package files and TypeScript config. Keep any manual health downloads in `data/`, `downloads/` or `exports/`.
## Official references
Checked on 2026-09-07:
- [Google Health setup](https://developers.google.com/health/get-started) and [workouts guide](https://developers.google.com/health/data-types/workouts).
- [List endpoint and supported filters](https://developers.google.com/health/reference/rest/v4/users.dataTypes.dataPoints/list), [get endpoint](https://developers.google.com/health/reference/rest/v4/users.dataTypes.dataPoints/get), [data schemas](https://developers.google.com/health/reference/rest/v4/users.dataTypes.dataPoints) and [TCX endpoint](https://developers.google.com/health/reference/rest/v4/users.dataTypes.dataPoints/exportExerciseTcx).
- [Desktop OAuth with PKCE](https://developers.google.com/identity/protocols/oauth2/native-app) and [local Codex MCP configuration](https://developers.openai.com/codex/mcp/).
The workout guide's simple heart-rate example uses `startTime`/`endTime` query parameters. This client follows the endpoint reference's documented `filter` expression instead. Exercise listing uses the explicitly documented civil-time filter. TCX uses `alt=media` and `partialData=true`, which permits available non-GPS data. The REST reference does not document an activity-type filter, so that filter is local.
TDQS
A4/5.0
Scored across 3 tools
Disambiguation5/5
Each tool has a distinct purpose: retrieving a single workout summary, listing multiple workouts, and fetching detailed measurement series. No overlap or ambiguity.
Naming Consistency5/5
All tool names follow the same verb_noun pattern (get_workout, list_workouts, get_workout_series), providing consistent and predictable naming.
Tool Count5/5
With only 3 tools, the server is well-scoped for its apparent read-only focus on workout data, staying within the typical 3-15 range.
Completeness4/5
The tools cover reading workouts comprehensively (single, list, and series), but lack any write or delete operations. If the server is intended as read-only, this is complete, though it may miss update/create actions for a full health API.
Maintenance
ActivityMaintained
ResponsivenessNo issues