@einlogic/mcp-fitbit 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/auth-tools.js +31 -0
- package/build/auth.js +49 -0
- package/build/config.js +3 -8
- package/build/index.js +4 -3
- package/package.json +74 -74
package/README.md
CHANGED
|
@@ -23,8 +23,7 @@ the Google Health API and returns its raw JSON.
|
|
|
23
23
|
## ⚠️ Breaking changes from the Fitbit version
|
|
24
24
|
|
|
25
25
|
- You must create a **Google Cloud** OAuth client (see below) and re-authorize. Old Fitbit tokens cannot be reused.
|
|
26
|
-
- Credentials are now `GOOGLE_HEALTH_CLIENT_ID` / `GOOGLE_HEALTH_CLIENT_SECRET` (the old `
|
|
27
|
-
- The token file is now `.google-health-token.json` (the old `.fitbit-token.json` is ignored and can be deleted).
|
|
26
|
+
- Credentials are now `GOOGLE_HEALTH_CLIENT_ID` / `GOOGLE_HEALTH_CLIENT_SECRET`.- The token file is now `.google-health-token.json` (the old `.fitbit-token.json` is ignored and can be deleted).
|
|
28
27
|
- Responses are the Google Health API's JSON, not Fitbit's. Fitbit-style `period` (`1d`, `7d`, ...) parameters are gone: tools take explicit start/end times, as the API does.
|
|
29
28
|
- `get_activity_goals` was removed: the Google Health API has no goals endpoint.
|
|
30
29
|
- New tools: `get_identity`, `get_settings`, `list_paired_devices`, `list_data_points`, `reconcile_data_points`.
|
|
@@ -131,6 +130,7 @@ List tools accept `pageSize` and `pageToken` (use `nextPageToken` from the respo
|
|
|
131
130
|
| `get_identity` | `users/me/identity` | none |
|
|
132
131
|
| `get_settings` | `users/me/settings` | none |
|
|
133
132
|
| `list_paired_devices` | `users/me/pairedDevices` | none |
|
|
133
|
+
| `reauthenticate` | not an API call: deletes the local tokens (memory + `.google-health-token.json`) and opens a new Google sign-in; new tokens are saved on approval, no restart needed. Cancels a pending sign-in. Confirm with `get_identity` | none |
|
|
134
134
|
| `list_data_points` | any of the 38 data types | `dataType`, optional raw `filter` |
|
|
135
135
|
| `reconcile_data_points` | `dataPoints:reconcile` for any data type | `dataType`, optional `filter`, `dataSourceFamily` |
|
|
136
136
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { registerTool, createSuccessResponse, createErrorResponse, } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registers the re-authorization tool.
|
|
4
|
+
* @param server The McpServer instance.
|
|
5
|
+
* @param reauthenticate Clears the local tokens and starts a new OAuth flow.
|
|
6
|
+
*/
|
|
7
|
+
export function registerAuthTools(server, reauthenticate) {
|
|
8
|
+
registerTool(server, {
|
|
9
|
+
name: 'reauthenticate',
|
|
10
|
+
description: "Clear this MCP server's locally stored Google Health tokens (memory and .google-health-token.json) and start a new Google sign-in. A browser window opens for consent; once approved, new tokens are saved and the other tools work again without restarting the server. Use when the token has expired (testing-mode refresh tokens last 7 days), was revoked, or the wrong Google account is connected. Cancels any sign-in that is still pending. Verify afterwards with get_identity.",
|
|
11
|
+
parametersSchema: {},
|
|
12
|
+
handler: async () => {
|
|
13
|
+
try {
|
|
14
|
+
const { tokenFileCleared, authUrl } = await reauthenticate();
|
|
15
|
+
if (!authUrl) {
|
|
16
|
+
return createErrorResponse('Local tokens were cleared, but the authorization flow could not start. Check GOOGLE_HEALTH_CLIENT_ID / GOOGLE_HEALTH_CLIENT_SECRET and that the OAuth callback port is free, then call reauthenticate again.');
|
|
17
|
+
}
|
|
18
|
+
return createSuccessResponse({
|
|
19
|
+
status: 'authorization_started',
|
|
20
|
+
tokenFileCleared,
|
|
21
|
+
authUrl,
|
|
22
|
+
next: `Approve access in the browser window that opened (or open ${authUrl} manually). Tools will fail until you finish; then call get_identity to confirm.`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('Re-authentication failed:', error);
|
|
27
|
+
return createErrorResponse(`Re-authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
package/build/auth.js
CHANGED
|
@@ -280,6 +280,55 @@ export function startAuthorizationFlow() {
|
|
|
280
280
|
oauthServer = null;
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Stops a pending authorization callback server (if any) and waits until its
|
|
285
|
+
* port is free, so a new flow can bind to it.
|
|
286
|
+
*/
|
|
287
|
+
async function stopAuthorizationServer() {
|
|
288
|
+
const server = oauthServer;
|
|
289
|
+
if (!server)
|
|
290
|
+
return;
|
|
291
|
+
oauthServer = null;
|
|
292
|
+
await new Promise((resolve) => {
|
|
293
|
+
server.close(() => resolve());
|
|
294
|
+
// Drop keep-alive connections (e.g. an open browser tab) so close() completes
|
|
295
|
+
server.closeAllConnections?.();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Forgets the current token: clears it from memory and deletes the token file.
|
|
300
|
+
* @returns true when a token file was deleted, false when there was none.
|
|
301
|
+
*/
|
|
302
|
+
export async function clearStoredToken() {
|
|
303
|
+
accessToken = null;
|
|
304
|
+
tokenData = null;
|
|
305
|
+
try {
|
|
306
|
+
await fs.unlink(TOKEN_FILE_PATH);
|
|
307
|
+
console.error(`Token file deleted: ${TOKEN_FILE_PATH}`);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error.code === 'ENOENT')
|
|
312
|
+
return false;
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Clears the local tokens and starts a fresh authorization flow. Any flow that
|
|
318
|
+
* is still waiting for a callback is cancelled first. The new token is stored
|
|
319
|
+
* (in memory and on disk) once the user finishes consent in the browser.
|
|
320
|
+
* @returns whether a token file was removed, and the URL that starts consent
|
|
321
|
+
* (null when the flow could not start, e.g. missing client credentials).
|
|
322
|
+
*/
|
|
323
|
+
export async function reauthenticate() {
|
|
324
|
+
await stopAuthorizationServer();
|
|
325
|
+
const tokenFileCleared = await clearStoredToken();
|
|
326
|
+
startAuthorizationFlow();
|
|
327
|
+
return {
|
|
328
|
+
tokenFileCleared,
|
|
329
|
+
authUrl: oauthServer ? `http://localhost:${PORT}/auth` : null,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
283
332
|
/**
|
|
284
333
|
* Retrieves the current Google Health access token.
|
|
285
334
|
* Automatically checks for token expiry and refreshes if necessary.
|
package/build/config.js
CHANGED
|
@@ -20,16 +20,11 @@ export const GOOGLE_OAUTH_CONFIG = {
|
|
|
20
20
|
`${SCOPE_PREFIX}.settings.readonly`,
|
|
21
21
|
].join(' '),
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
* Resolves OAuth client credentials from the environment.
|
|
25
|
-
* GOOGLE_HEALTH_* is preferred; the legacy FITBIT_* names are accepted as a fallback.
|
|
26
|
-
*/
|
|
23
|
+
/** Resolves the Google OAuth client credentials from GOOGLE_HEALTH_CLIENT_ID / _SECRET. */
|
|
27
24
|
export function resolveClientCredentials() {
|
|
28
25
|
return {
|
|
29
|
-
id: process.env.GOOGLE_HEALTH_CLIENT_ID ||
|
|
30
|
-
secret: process.env.GOOGLE_HEALTH_CLIENT_SECRET ||
|
|
31
|
-
process.env.FITBIT_CLIENT_SECRET ||
|
|
32
|
-
'',
|
|
26
|
+
id: process.env.GOOGLE_HEALTH_CLIENT_ID || '',
|
|
27
|
+
secret: process.env.GOOGLE_HEALTH_CLIENT_SECRET || '',
|
|
33
28
|
};
|
|
34
29
|
}
|
|
35
30
|
export function resolveOutputMode() {
|
package/build/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
// Import authentication functions
|
|
8
|
-
import { initializeAuth, startAuthorizationFlow, getAccessToken, } from './auth.js';
|
|
8
|
+
import { initializeAuth, startAuthorizationFlow, getAccessToken, reauthenticate, } from './auth.js';
|
|
9
9
|
// Import tool registration function(s)
|
|
10
10
|
import { registerWeightTool } from './weight.js';
|
|
11
11
|
import { registerSleepTool } from './sleep.js';
|
|
@@ -18,6 +18,7 @@ import { registerActivityTimeSeriesTool } from './activity-timeseries.js';
|
|
|
18
18
|
import { registerAzmTimeSeriesTool } from './azm-timeseries.js';
|
|
19
19
|
import { registerHrvTools } from './hrv.js';
|
|
20
20
|
import { registerAccountTools } from './account.js';
|
|
21
|
+
import { registerAuthTools } from './auth-tools.js';
|
|
21
22
|
import { registerDataPointTools } from './data-points.js';
|
|
22
23
|
import { registerCompatTools } from './compat/tools.js';
|
|
23
24
|
import { resolveClientCredentials, resolveOutputMode } from './config.js';
|
|
@@ -32,7 +33,6 @@ const envPath = path.resolve(__dirname, '..', '.env');
|
|
|
32
33
|
dotenv.config({ path: envPath });
|
|
33
34
|
// Validate required environment variables
|
|
34
35
|
function validateEnvironment() {
|
|
35
|
-
// GOOGLE_HEALTH_* is preferred; legacy FITBIT_* names are accepted as a fallback
|
|
36
36
|
const credentials = resolveClientCredentials();
|
|
37
37
|
const requiredVars = {
|
|
38
38
|
GOOGLE_HEALTH_CLIENT_ID: credentials.id,
|
|
@@ -58,7 +58,7 @@ console.error('✅ Environment variables loaded successfully');
|
|
|
58
58
|
// Create the main MCP server instance
|
|
59
59
|
const server = new McpServer({
|
|
60
60
|
name: 'google-health',
|
|
61
|
-
version: '2.
|
|
61
|
+
version: '2.2.0',
|
|
62
62
|
capabilities: {
|
|
63
63
|
resources: {},
|
|
64
64
|
tools: {}, // Tools are registered dynamically below
|
|
@@ -84,6 +84,7 @@ else {
|
|
|
84
84
|
registerProfileTool(server, getAccessToken);
|
|
85
85
|
registerActivitiesTool(server, getAccessToken);
|
|
86
86
|
registerAccountTools(server, getAccessToken);
|
|
87
|
+
registerAuthTools(server, reauthenticate);
|
|
87
88
|
registerDataPointTools(server, getAccessToken);
|
|
88
89
|
// --- Main Application Entry Point ---
|
|
89
90
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@einlogic/mcp-fitbit",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"main": "./build/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"mcp-fitbit": "./build/index.js",
|
|
8
|
-
"mkoziel2000-mcp-fitbit": "./build/index.js"
|
|
9
|
-
},
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "https://github.com/mkoziel2000/mcp-fitbit.git"
|
|
13
|
-
},
|
|
14
|
-
"homepage": "https://github.com/mkoziel2000/mcp-fitbit",
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/mkoziel2000/mcp-fitbit/issues"
|
|
17
|
-
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"test": "vitest run",
|
|
20
|
-
"test:ui": "vitest --ui",
|
|
21
|
-
"test:coverage": "vitest run --coverage",
|
|
22
|
-
"build": "tsc",
|
|
23
|
-
"start": "node build/index.js",
|
|
24
|
-
"dev": "npm run build && npx @modelcontextprotocol/inspector node build/index.js",
|
|
25
|
-
"lint": "eslint src/**/*.ts --ignore-pattern src/**/*.test.ts",
|
|
26
|
-
"lint:fix": "eslint src/**/*.ts --fix",
|
|
27
|
-
"format": "prettier --write src/**/*.ts",
|
|
28
|
-
"format:check": "prettier --check src/**/*.ts",
|
|
29
|
-
"prepublishOnly": "npm run build && npm run test && npm run lint",
|
|
30
|
-
"prepack": "npm run build"
|
|
31
|
-
},
|
|
32
|
-
"keywords": [
|
|
33
|
-
"mcp",
|
|
34
|
-
"fitbit",
|
|
35
|
-
"google-health-api",
|
|
36
|
-
"mcp-server",
|
|
37
|
-
"claude",
|
|
38
|
-
"model-context-protocol",
|
|
39
|
-
"health-data",
|
|
40
|
-
"fitness-tracking",
|
|
41
|
-
"ai-assistant",
|
|
42
|
-
"typescript"
|
|
43
|
-
],
|
|
44
|
-
"author": "mkoziel2000",
|
|
45
|
-
"license": "MIT",
|
|
46
|
-
"description": "MCP server for accessing Fitbit / Pixel Watch health and fitness data through the Google Health API. Connect AI assistants like Claude to your data for personalized health insights.",
|
|
47
|
-
"files": [
|
|
48
|
-
"build/",
|
|
49
|
-
"README.md",
|
|
50
|
-
"LICENSE"
|
|
51
|
-
],
|
|
52
|
-
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
54
|
-
"@types/express": "^5.0.1",
|
|
55
|
-
"dotenv": "^16.5.0",
|
|
56
|
-
"express": "^5.1.0",
|
|
57
|
-
"open": "^10.1.2",
|
|
58
|
-
"simple-oauth2": "^5.1.0",
|
|
59
|
-
"zod": "^3.24.3"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
62
|
-
"@eslint/js": "^9.27.0",
|
|
63
|
-
"@types/node": "^22.15.3",
|
|
64
|
-
"@types/simple-oauth2": "^5.0.7",
|
|
65
|
-
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
66
|
-
"@typescript-eslint/parser": "^8.32.1",
|
|
67
|
-
"@vitest/coverage-v8": "^3.1.4",
|
|
68
|
-
"@vitest/ui": "^3.1.4",
|
|
69
|
-
"eslint": "^9.27.0",
|
|
70
|
-
"prettier": "^3.5.3",
|
|
71
|
-
"typescript": "^5.8.3",
|
|
72
|
-
"vitest": "^3.1.4"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@einlogic/mcp-fitbit",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-fitbit": "./build/index.js",
|
|
8
|
+
"mkoziel2000-mcp-fitbit": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/mkoziel2000/mcp-fitbit.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/mkoziel2000/mcp-fitbit",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/mkoziel2000/mcp-fitbit/issues"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:ui": "vitest --ui",
|
|
21
|
+
"test:coverage": "vitest run --coverage",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"start": "node build/index.js",
|
|
24
|
+
"dev": "npm run build && npx @modelcontextprotocol/inspector node build/index.js",
|
|
25
|
+
"lint": "eslint src/**/*.ts --ignore-pattern src/**/*.test.ts",
|
|
26
|
+
"lint:fix": "eslint src/**/*.ts --fix",
|
|
27
|
+
"format": "prettier --write src/**/*.ts",
|
|
28
|
+
"format:check": "prettier --check src/**/*.ts",
|
|
29
|
+
"prepublishOnly": "npm run build && npm run test && npm run lint",
|
|
30
|
+
"prepack": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"fitbit",
|
|
35
|
+
"google-health-api",
|
|
36
|
+
"mcp-server",
|
|
37
|
+
"claude",
|
|
38
|
+
"model-context-protocol",
|
|
39
|
+
"health-data",
|
|
40
|
+
"fitness-tracking",
|
|
41
|
+
"ai-assistant",
|
|
42
|
+
"typescript"
|
|
43
|
+
],
|
|
44
|
+
"author": "mkoziel2000",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"description": "MCP server for accessing Fitbit / Pixel Watch health and fitness data through the Google Health API. Connect AI assistants like Claude to your data for personalized health insights.",
|
|
47
|
+
"files": [
|
|
48
|
+
"build/",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
54
|
+
"@types/express": "^5.0.1",
|
|
55
|
+
"dotenv": "^16.5.0",
|
|
56
|
+
"express": "^5.1.0",
|
|
57
|
+
"open": "^10.1.2",
|
|
58
|
+
"simple-oauth2": "^5.1.0",
|
|
59
|
+
"zod": "^3.24.3"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^9.27.0",
|
|
63
|
+
"@types/node": "^22.15.3",
|
|
64
|
+
"@types/simple-oauth2": "^5.0.7",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
66
|
+
"@typescript-eslint/parser": "^8.32.1",
|
|
67
|
+
"@vitest/coverage-v8": "^3.1.4",
|
|
68
|
+
"@vitest/ui": "^3.1.4",
|
|
69
|
+
"eslint": "^9.27.0",
|
|
70
|
+
"prettier": "^3.5.3",
|
|
71
|
+
"typescript": "^5.8.3",
|
|
72
|
+
"vitest": "^3.1.4"
|
|
73
|
+
}
|
|
74
|
+
}
|