@einlogic/mcp-fitbit 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Russell "TheDigitalNinja" Perkins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # Fitbit MCP Connector for AI
2
+
3
+ [![Fitbit API](https://img.shields.io/badge/Fitbit-00B0B9?logo=fitbit&logoColor=white)](https://dev.fitbit.com/)
4
+ [![CI](https://github.com/TheDigitalNinja/mcp-fitbit/actions/workflows/ci.yml/badge.svg)](https://github.com/TheDigitalNinja/mcp-fitbit/actions/workflows/ci.yml)
5
+ [![Coverage Status](https://coveralls.io/repos/github/TheDigitalNinja/mcp-fitbit/badge.svg?branch=main)](https://coveralls.io/github/TheDigitalNinja/mcp-fitbit?branch=main)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![npm version](https://badge.fury.io/js/mcp-fitbit.svg)](https://badge.fury.io/js/mcp-fitbit)
8
+ [![npm downloads](https://img.shields.io/npm/dm/mcp-fitbit.svg)](https://www.npmjs.com/package/mcp-fitbit)
9
+
10
+ > Connect AI assistants to your Fitbit health data
11
+
12
+ Give your AI assistant access to your Fitbit data for personalized health insights, trend analysis, and automated tracking. Works with Claude Desktop and other MCP-compatible AI tools.
13
+
14
+ ## What it does
15
+
16
+ 🏃 **Exercise & Activities** - Get detailed workout logs and activity data
17
+ 😴 **Sleep Analysis** - Retrieve sleep patterns and quality metrics
18
+ ⚖️ **Weight Tracking** - Access weight trends over time
19
+ ❤️ **Heart Rate Data** - Monitor heart rate patterns and zones
20
+ 🍎 **Nutrition Logs** - Review food intake, calories, and macros
21
+ 👤 **Profile Info** - Access basic Fitbit profile details
22
+
23
+ *Ask your AI things like: "Show me my sleep patterns this week" or "What's my average heart rate during workouts?"*
24
+
25
+ ## Quick Start
26
+
27
+ **🚀 Want to test the tools right away?**
28
+
29
+ ### Option 1: Install from npm (Recommended)
30
+ 1. [Get Fitbit API credentials](https://dev.fitbit.com/)
31
+ - Create an app with OAuth 2.0 Application Type: `Personal`
32
+ - Set Callback URL: `http://localhost:3000/callback`
33
+ - Note your **Client ID** and **Client Secret**
34
+
35
+ 2. Install the package globally:
36
+ ```bash
37
+ npm install -g mcp-fitbit
38
+ ```
39
+
40
+ 3. Add to your Claude Desktop config file:
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "fitbit": {
45
+ "command": "mcp-fitbit",
46
+ "args": [],
47
+ "env": {
48
+ "FITBIT_CLIENT_ID": "your_client_id_here",
49
+ "FITBIT_CLIENT_SECRET": "your_client_secret_here"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+ - Config file location:
56
+ - Windows: %AppData%\Claude\claude_desktop_config.json
57
+ - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
58
+ - Linux: ~/.config/Claude/claude_desktop_config.json
59
+
60
+ 4. Restart Claude Desktop and ask about your Fitbit data!
61
+
62
+
63
+
64
+ ### Option 2: Development Setup
65
+ 1. [Get Fitbit API credentials](https://dev.fitbit.com/) (see Installation below)
66
+ 2. Then run:
67
+
68
+ ```bash
69
+ git clone https://github.com/TheDigitalNinja/mcp-fitbit
70
+ cd mcp-fitbit
71
+ npm install
72
+ # Create .env with your Fitbit credentials
73
+ npm run dev
74
+ ```
75
+
76
+ Both options open the **MCP Inspector** at `http://localhost:5173` where you can test all tools interactively and handle the OAuth flow.
77
+
78
+ ## Installation
79
+
80
+ ### For End Users (npm package)
81
+
82
+ 1. **Get Fitbit API credentials** at [dev.fitbit.com](https://dev.fitbit.com/)
83
+ - Set **OAuth 2.0 Application Type** to `Personal`
84
+ - Set **Callback URL** to `http://localhost:3000/callback`
85
+
86
+ 2. **Install the package:**
87
+ ```bash
88
+ npm install -g mcp-fitbit
89
+ ```
90
+
91
+ 3. **Create `.env` file in the package directory:**
92
+
93
+ When you run `mcp-fitbit` for the first time, it will tell you exactly where to create the `.env` file. It will look something like:
94
+ ```
95
+ C:\Users\YourName\AppData\Roaming\npm\node_modules\mcp-fitbit\.env
96
+ ```
97
+
98
+ 4. **Add your credentials to the `.env` file:**
99
+ ```bash
100
+ FITBIT_CLIENT_ID=your_client_id_here
101
+ FITBIT_CLIENT_SECRET=your_client_secret_here
102
+ ```
103
+
104
+ 5. **Run the server:**
105
+ ```bash
106
+ mcp-fitbit
107
+ ```
108
+
109
+ ### For Developers (from source)
110
+
111
+ 1. **Get Fitbit API credentials** at [dev.fitbit.com](https://dev.fitbit.com/)
112
+ - Set **OAuth 2.0 Application Type** to `Personal`
113
+ - Set **Callback URL** to `http://localhost:3000/callback`
114
+
115
+ 2. **Clone and setup:**
116
+ ```bash
117
+ git clone https://github.com/TheDigitalNinja/mcp-fitbit
118
+ cd mcp-fitbit
119
+ npm install
120
+ ```
121
+
122
+ 3. **Create `.env` file:**
123
+ ```bash
124
+ FITBIT_CLIENT_ID=your_client_id_here
125
+ FITBIT_CLIENT_SECRET=your_client_secret_here
126
+ ```
127
+
128
+ 4. **Build the server:**
129
+ ```bash
130
+ npm run build
131
+ ```
132
+
133
+
134
+ ## Available Tools
135
+
136
+ | Tool | Description | Parameters |
137
+ |------|-------------|------------|
138
+ | `get_weight` | Weight data over time periods | `period`: `1d`, `7d`, `30d`, `3m`, `6m`, `1y` |
139
+ | `get_sleep_by_date_range` | Sleep logs for date range (max 100 days) | `startDate`, `endDate` (YYYY-MM-DD) |
140
+ | `get_exercises` | Activity/exercise logs after date | `afterDate` (YYYY-MM-DD), `limit` (1-100) |
141
+ | `get_daily_activity_summary` | Daily activity summary with goals | `date` (YYYY-MM-DD) |
142
+ | `get_activity_goals` | User's activity goals (daily/weekly) | `period`: `daily`, `weekly` |
143
+ | `get_activity_timeseries` | Activity time series data (max 30 days) | `resourcePath`, `startDate`, `endDate` (YYYY-MM-DD) |
144
+ | `get_azm_timeseries` | Active Zone Minutes time series (max 1095 days) | `startDate`, `endDate` (YYYY-MM-DD) |
145
+ | `get_heart_rate` | Heart rate for time period | `period`: `1d`, `7d`, `30d`, `1w`, `1m`, optional `date` |
146
+ | `get_heart_rate_by_date_range` | Heart rate for date range (max 1 year) | `startDate`, `endDate` (YYYY-MM-DD) |
147
+ | `get_food_log` | Complete nutrition data for a day | `date` (YYYY-MM-DD or "today") |
148
+ | `get_nutrition` | Individual nutrient over time | `resource`, `period`, optional `date` |
149
+ | `get_nutrition_by_date_range` | Individual nutrient for date range | `resource`, `startDate`, `endDate` |
150
+ | `get_profile` | User profile information | None |
151
+
152
+ **Nutrition resources:** `caloriesIn`, `water`, `protein`, `carbs`, `fat`, `fiber`, `sodium`
153
+
154
+ **Activity time series resources:** `steps`, `distance`, `calories`, `activityCalories`, `caloriesBMR`, `tracker/activityCalories`, `tracker/calories`, `tracker/distance`
155
+
156
+ ### Claude Desktop
157
+
158
+ **Using npm package (recommended):**
159
+
160
+ Add to `claude_desktop_config.json`:
161
+ ```json
162
+ {
163
+ "mcpServers": {
164
+ "fitbit": {
165
+ "command": "mcp-fitbit",
166
+ "args": []
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ **Using local development version:**
173
+
174
+ Add to `claude_desktop_config.json`:
175
+ ```json
176
+ {
177
+ "mcpServers": {
178
+ "fitbit": {
179
+ "command": "node",
180
+ "args": ["C:\\path\\to\\mcp-fitbit\\build\\index.js"]
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ **Config file locations:**
187
+ - Windows: `%AppData%\Claude\claude_desktop_config.json`
188
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
189
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
190
+
191
+ ### First Run Authorization
192
+
193
+ When you first ask your AI assistant to use Fitbit data:
194
+ 1. The server opens your browser to `http://localhost:3000/auth`
195
+ 2. Log in to Fitbit and grant permissions
196
+ 3. You'll be redirected to a success page
197
+ 4. Your AI can now access your Fitbit data!
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ npm run lint # Check code quality
203
+ npm run format # Fix formatting
204
+ npm run build # Compile TypeScript
205
+ npm run dev # Run with MCP inspector
206
+ ```
207
+
208
+ **Architecture:** See [TASKS.md](TASKS.md) for improvement opportunities and technical details.
209
+
@@ -0,0 +1,25 @@
1
+ import { registerTool, CommonSchemas, handleFitbitApiCall } from './utils.js';
2
+ // --- Tool Registration ---
3
+ /**
4
+ * Registers a Fitbit activities/exercises tool with the MCP server.
5
+ * @param server The McpServer instance.
6
+ * @param getAccessTokenFn Function to retrieve the current access token.
7
+ */
8
+ export function registerActivitiesTool(server, getAccessTokenFn) {
9
+ registerTool(server, {
10
+ name: 'get_exercises',
11
+ description: "Get the raw JSON response for exercise and activity logs from Fitbit after a specific date. Requires 'afterDate' parameter in 'YYYY-MM-DD' format. Retrieves a detailed list of logged exercises and activities.",
12
+ parametersSchema: {
13
+ afterDate: CommonSchemas.afterDate,
14
+ limit: CommonSchemas.limit,
15
+ },
16
+ handler: async ({ afterDate, limit = 20 }) => {
17
+ const endpoint = `activities/list.json?afterDate=${afterDate}&sort=asc&offset=0&limit=${limit}`;
18
+ return handleFitbitApiCall(endpoint, { afterDate, limit }, getAccessTokenFn, {
19
+ successDataExtractor: (data) => data.activities || [],
20
+ noDataMessage: `after date '${afterDate}'`,
21
+ errorContext: `after date '${afterDate}'`
22
+ });
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import { registerTool, handleFitbitApiCall } from './utils.js';
3
+ /**
4
+ * Registers an activity goals tool with the MCP server.
5
+ * @param server The McpServer instance.
6
+ * @param getAccessTokenFn Function to retrieve the current access token.
7
+ */
8
+ export function registerActivityGoalsTool(server, getAccessTokenFn) {
9
+ registerTool(server, {
10
+ name: 'get_activity_goals',
11
+ description: "Get the raw JSON response for user's activity goals from Fitbit. Supports 'daily' and 'weekly' periods. Returns goal values for steps, distance, calories, floors, active minutes, and active zone minutes.",
12
+ parametersSchema: {
13
+ period: z
14
+ .enum(['daily', 'weekly'])
15
+ .describe("Goal period - either 'daily' or 'weekly'")
16
+ },
17
+ handler: async ({ period }) => {
18
+ const endpoint = `activities/goals/${period}.json`;
19
+ return handleFitbitApiCall(endpoint, { period }, getAccessTokenFn, {
20
+ errorContext: `period '${period}'`
21
+ });
22
+ }
23
+ });
24
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { registerTool, CommonSchemas, handleFitbitApiCall } from './utils.js';
3
+ /**
4
+ * Registers an activity time series tool with the MCP server.
5
+ * @param server The McpServer instance.
6
+ * @param getAccessTokenFn Function to retrieve the current access token.
7
+ */
8
+ export function registerActivityTimeSeriesTool(server, getAccessTokenFn) {
9
+ registerTool(server, {
10
+ name: 'get_activity_timeseries',
11
+ description: "Get the raw JSON response for activity time series data from Fitbit over a date range (max 30 days). Supports various resource paths like 'steps', 'distance', 'calories', 'activityCalories', 'caloriesBMR'.",
12
+ parametersSchema: {
13
+ resourcePath: z
14
+ .enum([
15
+ 'steps',
16
+ 'distance',
17
+ 'calories',
18
+ 'activityCalories',
19
+ 'caloriesBMR',
20
+ 'tracker/activityCalories',
21
+ 'tracker/calories',
22
+ 'tracker/distance'
23
+ ])
24
+ .describe("Activity resource to retrieve (e.g., 'steps', 'distance', 'calories')"),
25
+ startDate: CommonSchemas.startDate,
26
+ endDate: CommonSchemas.endDate,
27
+ },
28
+ handler: async ({ resourcePath, startDate, endDate }) => {
29
+ const endpoint = `activities/${resourcePath}/date/${startDate}/${endDate}.json`;
30
+ return handleFitbitApiCall(endpoint, { resourcePath, startDate, endDate }, getAccessTokenFn, {
31
+ errorContext: `resource '${resourcePath}' from ${startDate} to ${endDate}`
32
+ });
33
+ }
34
+ });
35
+ }
package/build/auth.js ADDED
@@ -0,0 +1,274 @@
1
+ import dotenv from 'dotenv';
2
+ import express from 'express';
3
+ import open from 'open';
4
+ import path from 'path';
5
+ import { AuthorizationCode } from 'simple-oauth2';
6
+ import { fileURLToPath } from 'url';
7
+ import fs from 'fs/promises';
8
+ import { existsSync } from 'fs';
9
+ import { FITBIT_OAUTH_CONFIG } from './config.js';
10
+ // Determine the directory of the current module (build/auth.js)
11
+ const currentFilename = fileURLToPath(import.meta.url);
12
+ const currentDirname = path.dirname(currentFilename);
13
+ // Load environment variables from .env file located in the project root
14
+ const envPath = path.resolve(currentDirname, '..', '.env');
15
+ dotenv.config({ path: envPath });
16
+ // Fitbit OAuth2 Configuration
17
+ const fitbitConfig = {
18
+ client: {
19
+ id: process.env.FITBIT_CLIENT_ID || '',
20
+ secret: process.env.FITBIT_CLIENT_SECRET || '',
21
+ },
22
+ auth: {
23
+ tokenHost: 'https://api.fitbit.com',
24
+ authorizePath: 'https://www.fitbit.com/oauth2/authorize',
25
+ tokenPath: 'https://api.fitbit.com/oauth2/token',
26
+ },
27
+ options: {
28
+ authorizationMethod: 'header',
29
+ },
30
+ };
31
+ // OAuth2 Redirect URI and local server port
32
+ const REDIRECT_URI = 'http://localhost:3000/callback';
33
+ const PORT = 3000;
34
+ // --- State Management ---
35
+ // Storage for the access token and token data
36
+ let accessToken = null;
37
+ let tokenData = null;
38
+ // Holds the temporary HTTP server instance used for the OAuth callback
39
+ let oauthServer = null;
40
+ // --- File paths for token persistence ---
41
+ const TOKEN_FILE_PATH = path.resolve(currentDirname, '..', '.fitbit-token.json');
42
+ // --- OAuth Client Initialization ---
43
+ const oauthClient = new AuthorizationCode(fitbitConfig);
44
+ /**
45
+ * Saves the token data to a file for persistence
46
+ * @param tokenData The token data to save
47
+ */
48
+ async function saveTokenToFile(tokenData) {
49
+ try {
50
+ await fs.writeFile(TOKEN_FILE_PATH, JSON.stringify(tokenData, null, 2), 'utf8');
51
+ console.error(`Token saved to ${TOKEN_FILE_PATH}`);
52
+ }
53
+ catch (error) {
54
+ console.error(`Error saving token to file: ${error}`);
55
+ }
56
+ }
57
+ /**
58
+ * Loads the token data from file
59
+ * @returns The token data or null if not found
60
+ */
61
+ async function loadTokenFromFile() {
62
+ try {
63
+ if (!existsSync(TOKEN_FILE_PATH)) {
64
+ console.error(`Token file not found at ${TOKEN_FILE_PATH}`);
65
+ return null;
66
+ }
67
+ const data = await fs.readFile(TOKEN_FILE_PATH, 'utf8');
68
+ const parsedData = JSON.parse(data);
69
+ console.error('Token loaded from file successfully');
70
+ return parsedData;
71
+ }
72
+ catch (error) {
73
+ console.error(`Error loading token from file: ${error}`);
74
+ return null;
75
+ }
76
+ }
77
+ // --- Fitbit Authorization Flow ---
78
+ /**
79
+ * Initiates the Fitbit OAuth2 authorization code flow.
80
+ * Starts a temporary local web server to handle the redirect callback.
81
+ * Opens the user's browser to the Fitbit authorization page.
82
+ */
83
+ export function startAuthorizationFlow() {
84
+ // Prevent multiple authorization flows from running simultaneously
85
+ if (oauthServer) {
86
+ console.error('OAuth server is already running.');
87
+ return;
88
+ }
89
+ // Ensure Client ID and Secret are loaded before starting
90
+ if (!fitbitConfig.client.id || !fitbitConfig.client.secret) {
91
+ console.error('Error: Fitbit Client ID or Secret not found. Check environment variables.');
92
+ return;
93
+ }
94
+ const app = express();
95
+ // Generate the Fitbit authorization URL
96
+ const authorizationUri = oauthClient.authorizeURL({
97
+ redirect_uri: REDIRECT_URI,
98
+ // Define necessary scopes required by the application
99
+ scope: FITBIT_OAUTH_CONFIG.SCOPES,
100
+ });
101
+ // Route to initiate the authorization flow by redirecting the user to Fitbit
102
+ app.get('/auth', (req, res) => {
103
+ console.error('Redirecting to Fitbit for authorization...');
104
+ res.redirect(authorizationUri);
105
+ });
106
+ // Callback route that Fitbit redirects to after user authorization
107
+ app.get('/callback', async (req, res) => {
108
+ const code = req.query.code;
109
+ // Handle cases where the authorization code is missing
110
+ if (!code) {
111
+ console.error('Authorization code missing in callback.');
112
+ res.status(400).send('Error: Authorization code missing.');
113
+ // Attempt to close the server if it exists
114
+ if (oauthServer) {
115
+ oauthServer.close(() => {
116
+ console.error('OAuth server closed due to missing code.');
117
+ });
118
+ oauthServer = null;
119
+ }
120
+ return;
121
+ }
122
+ console.error('Received authorization code. Exchanging for token...');
123
+ const tokenParams = { code: code, redirect_uri: REDIRECT_URI };
124
+ try {
125
+ // Exchange the authorization code for an access token
126
+ const tokenResult = await oauthClient.getToken(tokenParams);
127
+ console.error('Access Token received successfully!');
128
+ accessToken = tokenResult.token.access_token;
129
+ tokenData = tokenResult.token;
130
+ // Persist token data to file
131
+ if (tokenData) {
132
+ await saveTokenToFile(tokenData);
133
+ console.error('Token data has been persisted to file');
134
+ }
135
+ res.send('Authorization successful! You can close this window. The MCP Server is now authenticated.');
136
+ }
137
+ catch (error) {
138
+ // Handle errors during token exchange
139
+ const typedError = error;
140
+ console.error('Error obtaining access token:', typedError.message || typedError);
141
+ if (typedError.response) {
142
+ try {
143
+ const errorDetails = await typedError.response.text();
144
+ console.error('Error details:', errorDetails);
145
+ // Optionally parse errorDetails if it's JSON and log typedError.response.body
146
+ }
147
+ catch {
148
+ console.error('Could not parse error response body.');
149
+ }
150
+ }
151
+ res
152
+ .status(500)
153
+ .send('Error obtaining access token. Check MCP server logs.');
154
+ }
155
+ finally {
156
+ // Ensure the temporary server is always shut down after handling the callback
157
+ if (oauthServer) {
158
+ console.error('Shutting down temporary OAuth server...');
159
+ oauthServer.close(() => {
160
+ console.error('OAuth server closed.');
161
+ oauthServer = null;
162
+ });
163
+ }
164
+ }
165
+ });
166
+ // Start the temporary local server
167
+ oauthServer = app.listen(PORT, async () => {
168
+ const authUrl = `http://localhost:${PORT}/auth`;
169
+ console.error(`--------------------------------------------------------------------`);
170
+ console.error(`ACTION REQUIRED: Fitbit Authorization Needed`);
171
+ console.error(`Attempting to open authorization page in your browser:`);
172
+ console.error(authUrl);
173
+ console.error(`If the browser doesn't open, please navigate there manually.`);
174
+ console.error(`Waiting for authorization callback...`);
175
+ console.error(`--------------------------------------------------------------------`);
176
+ // Attempt to automatically open the authorization URL in the default browser
177
+ try {
178
+ await open(authUrl);
179
+ console.error(`Browser opened (or attempted).`);
180
+ }
181
+ catch (err) {
182
+ console.error(`Failed to open browser automatically:`, err);
183
+ }
184
+ });
185
+ // Handle potential errors during server startup
186
+ oauthServer.on('error', (err) => {
187
+ console.error('Error starting temporary OAuth server:', err);
188
+ oauthServer = null;
189
+ });
190
+ }
191
+ /**
192
+ * Retrieves the current Fitbit access token.
193
+ * Automatically checks for token expiry and refreshes if necessary.
194
+ * @returns The access token string or null if not available.
195
+ */
196
+ export async function getAccessToken() {
197
+ // Return null if no token data exists
198
+ if (!tokenData || !accessToken) {
199
+ console.error('No valid access token found.'); // Added missing console.error and fixed surrounding logic
200
+ return null;
201
+ }
202
+ // Check if token is expired
203
+ if (tokenData.expires_at && new Date(tokenData.expires_at) < new Date()) {
204
+ console.error('Token is expired. Attempting to refresh...');
205
+ try {
206
+ // Create AccessToken instance from the current token data
207
+ // FitbitTokenData is compatible with the 'Token' type expected by createToken
208
+ const accessTokenObj = oauthClient.createToken(tokenData);
209
+ // Refresh the token
210
+ if (accessTokenObj.expired()) {
211
+ const refreshedToken = await accessTokenObj.refresh();
212
+ accessToken = refreshedToken.token.access_token;
213
+ tokenData = refreshedToken.token;
214
+ // Save the refreshed token
215
+ await saveTokenToFile(tokenData);
216
+ console.error('Token refreshed and saved successfully.');
217
+ return accessToken;
218
+ }
219
+ }
220
+ catch (refreshError) {
221
+ console.error('Failed to refresh token:', refreshError);
222
+ accessToken = null;
223
+ tokenData = null;
224
+ return null;
225
+ }
226
+ }
227
+ return accessToken;
228
+ }
229
+ /**
230
+ * Initializes the authentication module.
231
+ * Loads persisted token from file storage if available.
232
+ */
233
+ export async function initializeAuth() {
234
+ console.error('Auth initialized. Checking for persisted token...');
235
+ try {
236
+ // Load token data from file
237
+ tokenData = await loadTokenFromFile();
238
+ if (tokenData && tokenData.access_token) {
239
+ accessToken = tokenData.access_token;
240
+ console.error('Persisted access token loaded successfully.');
241
+ // Check if token is expired and needs refresh
242
+ if (tokenData.expires_at && new Date(tokenData.expires_at) < new Date()) {
243
+ console.error('Token is expired. Attempting to refresh...');
244
+ try {
245
+ // Create AccessToken instance from the loaded token data
246
+ // FitbitTokenData is compatible with the 'Token' type expected by createToken
247
+ const accessTokenObj = oauthClient.createToken(tokenData);
248
+ // Refresh the token
249
+ if (accessTokenObj.expired()) {
250
+ const refreshedToken = await accessTokenObj.refresh();
251
+ accessToken = refreshedToken.token.access_token;
252
+ tokenData = refreshedToken.token;
253
+ // Save the refreshed token
254
+ if (tokenData) {
255
+ await saveTokenToFile(tokenData);
256
+ console.error('Refreshed token saved successfully.');
257
+ }
258
+ }
259
+ }
260
+ catch (refreshError) {
261
+ console.error('Error refreshing token:', refreshError);
262
+ accessToken = null;
263
+ tokenData = null;
264
+ }
265
+ }
266
+ }
267
+ else {
268
+ console.error('No valid access token found.');
269
+ }
270
+ }
271
+ catch (error) {
272
+ console.error('Error during token initialization:', error);
273
+ }
274
+ }
@@ -0,0 +1,22 @@
1
+ import { registerTool, CommonSchemas, handleFitbitApiCall } from './utils.js';
2
+ /**
3
+ * Registers an Active Zone Minutes time series tool with the MCP server.
4
+ * @param server The McpServer instance.
5
+ * @param getAccessTokenFn Function to retrieve the current access token.
6
+ */
7
+ export function registerAzmTimeSeriesTool(server, getAccessTokenFn) {
8
+ registerTool(server, {
9
+ name: 'get_azm_timeseries',
10
+ description: "Get the raw JSON response for Active Zone Minutes (AZM) time series data from Fitbit over a date range (max 1095 days). Returns total AZM plus breakdown by fat burn, cardio, and peak zones.",
11
+ parametersSchema: {
12
+ startDate: CommonSchemas.startDate,
13
+ endDate: CommonSchemas.endDate,
14
+ },
15
+ handler: async ({ startDate, endDate }) => {
16
+ const endpoint = `activities/active-zone-minutes/date/${startDate}/${endDate}.json`;
17
+ return handleFitbitApiCall(endpoint, { startDate, endDate }, getAccessTokenFn, {
18
+ errorContext: `AZM data from ${startDate} to ${endDate}`
19
+ });
20
+ }
21
+ });
22
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Centralized configuration for the MCP Fitbit server
3
+ * Contains all constants, API URLs, and shared configurations
4
+ */
5
+ // === API Configuration ===
6
+ export const FITBIT_API_VERSIONS = {
7
+ V1: 'https://api.fitbit.com/1',
8
+ V1_2: 'https://api.fitbit.com/1.2',
9
+ };
10
+ export const FITBIT_OAUTH_CONFIG = {
11
+ AUTHORIZE_URL: 'https://www.fitbit.com/oauth2/authorize',
12
+ TOKEN_URL: 'https://api.fitbit.com/oauth2/token',
13
+ CALLBACK_URI: 'http://localhost:3000/callback',
14
+ SCOPES: [
15
+ 'weight',
16
+ 'sleep',
17
+ 'profile',
18
+ 'activity',
19
+ 'heartrate',
20
+ 'nutrition'
21
+ ].join(' ')
22
+ };
23
+ // === Tool Configuration ===
24
+ // === Common Parameter Types ===
25
+ export const TIME_PERIODS = ['1d', '7d', '30d', '3m', '6m', '1y'];
26
+ export const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
27
+ export const DATE_FORMAT_ERROR = 'Date must be in YYYY-MM-DD format';
28
+ export const HEART_RATE_DETAIL_LEVELS = ['1sec', '1min', '15min'];
29
+ // === Common Validation Messages ===
30
+ export const VALIDATION_MESSAGES = {
31
+ DATE_FORMAT: DATE_FORMAT_ERROR,
32
+ PERIOD_REQUIRED: 'The time period for which to retrieve data',
33
+ START_DATE_REQUIRED: 'The start date for which to retrieve data (YYYY-MM-DD)',
34
+ END_DATE_REQUIRED: 'The end date for which to retrieve data (YYYY-MM-DD)',
35
+ DETAIL_LEVEL_REQUIRED: 'The granularity level of the data',
36
+ MAX_RANGE_100_DAYS: 'Note: The API enforces a maximum range of 100 days',
37
+ MAX_RANGE_31_DAYS: 'Note: The API enforces a maximum range of 31 days'
38
+ };
39
+ // === HTTP Configuration ===
40
+ export const HTTP_CONFIG = {
41
+ USER_AGENT: 'mcp-fitbit-server/1.0',
42
+ TIMEOUT_MS: 30000,
43
+ MAX_RETRIES: 3
44
+ };
45
+ // === File Paths ===
46
+ export const FILE_PATHS = {
47
+ TOKEN_STORAGE: '.fitbit-token.json'
48
+ };
49
+ // === Error Messages ===
50
+ export const ERROR_MESSAGES = {
51
+ NO_ACCESS_TOKEN: 'No Fitbit Access Token available. Please authorize first.',
52
+ API_REQUEST_FAILED: 'Failed to retrieve data from Fitbit API',
53
+ TOKEN_EXPIRED: 'Access token might be expired or invalid. Re-authorization may be needed.',
54
+ NO_DATA_FOUND: 'No data found for the specified period/range',
55
+ CHECK_TOKEN_PERMISSIONS: 'Check token and permissions'
56
+ };