@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.
@@ -0,0 +1,21 @@
1
+ import { registerTool, CommonSchemas, handleFitbitApiCall } from './utils.js';
2
+ /**
3
+ * Registers a daily activity summary 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 registerDailyActivityTool(server, getAccessTokenFn) {
8
+ registerTool(server, {
9
+ name: 'get_daily_activity_summary',
10
+ description: "Get the raw JSON response for daily activity summary from Fitbit for a specific date. Includes goals, steps, calories, distances, and heart rate zones. Requires a 'date' parameter in YYYY-MM-DD format.",
11
+ parametersSchema: {
12
+ date: CommonSchemas.date,
13
+ },
14
+ handler: async ({ date }) => {
15
+ const endpoint = `activities/date/${date}.json`;
16
+ return handleFitbitApiCall(endpoint, { date }, getAccessTokenFn, {
17
+ errorContext: `date '${date}'`
18
+ });
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,106 @@
1
+ import { z } from 'zod';
2
+ import { makeFitbitRequest } from './utils.js';
3
+ const FITBIT_API_BASE = 'https://api.fitbit.com/1';
4
+ // --- Tool Registration ---
5
+ /**
6
+ * Registers Fitbit heart rate tools with the MCP server.
7
+ * @param server The McpServer instance.
8
+ * @param getAccessTokenFn Function to retrieve the current access token.
9
+ */
10
+ export function registerHeartRateTools(server, getAccessTokenFn) {
11
+ // --- Heart Rate by Date/Period Tool ---
12
+ const periodToolName = 'get_heart_rate';
13
+ const periodDescription = "Get the raw JSON response for heart rate data from Fitbit for a specified period ending today or on a specific date. Requires a 'period' parameter such as '1d', '7d', '30d', '1w', '1m' and optionally accepts 'date' parameter.";
14
+ const periodParametersSchemaShape = {
15
+ period: z
16
+ .enum(['1d', '7d', '30d', '1w', '1m'])
17
+ .describe('The time period for which to retrieve heart rate data.'),
18
+ date: z
19
+ .string()
20
+ .regex(/^\d{4}-\d{2}-\d{2}$|^today$/, "Date must be in YYYY-MM-DD format or 'today'.")
21
+ .optional()
22
+ .describe("The date for which to retrieve heart rate data (YYYY-MM-DD or 'today'). Defaults to 'today'."),
23
+ };
24
+ server.tool(periodToolName, periodDescription, periodParametersSchemaShape, async ({ period, date = 'today', }) => {
25
+ // Construct the endpoint dynamically
26
+ const endpoint = `activities/heart/date/${date}/${period}.json`;
27
+ const heartRateData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
28
+ // Handle API call failure
29
+ if (!heartRateData) {
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: `Failed to retrieve heart rate data from Fitbit API for date '${date}' and period '${period}'. Check token and permissions.`,
35
+ },
36
+ ],
37
+ isError: true,
38
+ };
39
+ }
40
+ // Handle no data found for the period
41
+ const heartRateEntries = heartRateData['activities-heart'] || [];
42
+ if (heartRateEntries.length === 0) {
43
+ return {
44
+ content: [
45
+ {
46
+ type: 'text',
47
+ text: `No heart rate data found for date '${date}' and period '${period}'.`,
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ // Return successful response with raw JSON
53
+ const rawJsonResponse = JSON.stringify(heartRateData, null, 2);
54
+ return {
55
+ content: [{ type: 'text', text: rawJsonResponse }],
56
+ };
57
+ });
58
+ // --- Heart Rate by Date Range Tool ---
59
+ const rangeToolName = 'get_heart_rate_by_date_range';
60
+ const rangeDescription = "Get the raw JSON response for heart rate data from Fitbit for a specific date range. Requires 'startDate' and 'endDate' parameters in 'YYYY-MM-DD' format. Note: The API enforces a maximum range of 1 year.";
61
+ const rangeParametersSchemaShape = {
62
+ startDate: z
63
+ .string()
64
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Start date must be in YYYY-MM-DD format.')
65
+ .describe('The start date for which to retrieve heart rate data (YYYY-MM-DD).'),
66
+ endDate: z
67
+ .string()
68
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'End date must be in YYYY-MM-DD format.')
69
+ .describe('The end date for which to retrieve heart rate data (YYYY-MM-DD).'),
70
+ };
71
+ server.tool(rangeToolName, rangeDescription, rangeParametersSchemaShape, async ({ startDate, endDate, }) => {
72
+ // Construct the endpoint dynamically
73
+ const endpoint = `activities/heart/date/${startDate}/${endDate}.json`;
74
+ // Make the request
75
+ const heartRateData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
76
+ // Handle API call failure
77
+ if (!heartRateData) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: 'text',
82
+ text: `Failed to retrieve heart rate data from Fitbit API for the date range '${startDate}' to '${endDate}'. Check token, permissions, date format, and ensure the range is 1 year or less.`,
83
+ },
84
+ ],
85
+ isError: true,
86
+ };
87
+ }
88
+ // Handle no data found
89
+ const heartRateEntries = heartRateData['activities-heart'] || [];
90
+ if (heartRateEntries.length === 0) {
91
+ return {
92
+ content: [
93
+ {
94
+ type: 'text',
95
+ text: `No heart rate data found for the date range '${startDate}' to '${endDate}'.`,
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ // Return successful response
101
+ const rawJsonResponse = JSON.stringify(heartRateData, null, 2);
102
+ return {
103
+ content: [{ type: 'text', text: rawJsonResponse }],
104
+ };
105
+ });
106
+ }
package/build/hrv.js ADDED
@@ -0,0 +1,35 @@
1
+ import { registerTool, CommonSchemas, handleFitbitApiCall, } from './utils.js';
2
+ /**
3
+ * Registers Heart Rate Variability (HRV) tools with the MCP server.
4
+ * @param server The McpServer instance.
5
+ * @param getAccessTokenFn Function to retrieve the current access token.
6
+ */
7
+ export function registerHrvTools(server, getAccessTokenFn) {
8
+ registerTool(server, {
9
+ name: 'get_hrv',
10
+ description: 'Get the raw JSON response for Heart Rate Variability (HRV) summary data from Fitbit for a single date. Returns daily RMSSD and deep sleep RMSSD values.',
11
+ parametersSchema: {
12
+ date: CommonSchemas.date,
13
+ },
14
+ handler: async ({ date }) => {
15
+ const endpoint = `hrv/date/${date}.json`;
16
+ return handleFitbitApiCall(endpoint, { date }, getAccessTokenFn, {
17
+ errorContext: `HRV data for ${date}`,
18
+ });
19
+ },
20
+ });
21
+ registerTool(server, {
22
+ name: 'get_hrv_by_date_range',
23
+ description: 'Get the raw JSON response for Heart Rate Variability (HRV) summary data from Fitbit over a date range (max 30 days). Returns daily RMSSD and deep sleep RMSSD values for each day.',
24
+ parametersSchema: {
25
+ startDate: CommonSchemas.startDate,
26
+ endDate: CommonSchemas.endDate,
27
+ },
28
+ handler: async ({ startDate, endDate }) => {
29
+ const endpoint = `hrv/date/${startDate}/${endDate}.json`;
30
+ return handleFitbitApiCall(endpoint, { startDate, endDate }, getAccessTokenFn, {
31
+ errorContext: `HRV data from ${startDate} to ${endDate}`,
32
+ });
33
+ },
34
+ });
35
+ }
package/build/index.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import dotenv from 'dotenv';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ // Import authentication functions
8
+ import { initializeAuth, startAuthorizationFlow, getAccessToken, } from './auth.js';
9
+ // Import tool registration function(s)
10
+ import { registerWeightTool } from './weight.js';
11
+ import { registerSleepTool } from './sleep.js';
12
+ import { registerProfileTool } from './profile.js';
13
+ import { registerActivitiesTool } from './activities.js';
14
+ import { registerHeartRateTools } from './heart-rate.js';
15
+ import { registerNutritionTools } from './nutrition.js';
16
+ import { registerDailyActivityTool } from './daily-activity.js';
17
+ import { registerActivityGoalsTool } from './activity-goals.js';
18
+ import { registerActivityTimeSeriesTool } from './activity-timeseries.js';
19
+ import { registerAzmTimeSeriesTool } from './azm-timeseries.js';
20
+ import { registerHrvTools } from './hrv.js';
21
+ // Import utilities
22
+ import './utils.js';
23
+ // Calculate the directory name of the current module (build/index.js)
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+ // Construct the absolute path to the .env file (one level up from build/)
27
+ // Load environment variables early in the application lifecycle
28
+ const envPath = path.resolve(__dirname, '..', '.env');
29
+ dotenv.config({ path: envPath });
30
+ // Validate required environment variables
31
+ function validateEnvironment() {
32
+ const requiredVars = {
33
+ FITBIT_CLIENT_ID: process.env.FITBIT_CLIENT_ID,
34
+ FITBIT_CLIENT_SECRET: process.env.FITBIT_CLIENT_SECRET,
35
+ };
36
+ const missingVars = Object.entries(requiredVars)
37
+ .filter(([, value]) => !value)
38
+ .map(([key]) => key);
39
+ if (missingVars.length > 0) {
40
+ console.error('Missing required environment variables:');
41
+ missingVars.forEach(varName => {
42
+ console.error(` - ${varName}`);
43
+ });
44
+ console.error(`Please create a .env file at: ${envPath}`);
45
+ console.error('See README.md for details on getting Fitbit API credentials.');
46
+ process.exit(1);
47
+ }
48
+ }
49
+ // Validate environment before proceeding
50
+ validateEnvironment();
51
+ // Log successful environment loading
52
+ console.error('✅ Environment variables loaded successfully');
53
+ // Create the main MCP server instance
54
+ const server = new McpServer({
55
+ name: 'fitbit',
56
+ version: '1.0.0',
57
+ capabilities: {
58
+ resources: {},
59
+ tools: {}, // Tools are registered dynamically below
60
+ },
61
+ });
62
+ // Register available tools with the MCP server
63
+ registerWeightTool(server, getAccessToken);
64
+ registerSleepTool(server, getAccessToken);
65
+ registerProfileTool(server, getAccessToken);
66
+ registerActivitiesTool(server, getAccessToken);
67
+ registerHeartRateTools(server, getAccessToken);
68
+ registerNutritionTools(server, getAccessToken);
69
+ registerDailyActivityTool(server, getAccessToken);
70
+ registerActivityGoalsTool(server, getAccessToken);
71
+ registerActivityTimeSeriesTool(server, getAccessToken);
72
+ registerAzmTimeSeriesTool(server, getAccessToken);
73
+ registerHrvTools(server, getAccessToken);
74
+ // --- Main Application Entry Point ---
75
+ async function main() {
76
+ // Initialize the authentication module (e.g., load persisted token)
77
+ await initializeAuth();
78
+ // Set up the transport layer for communication (stdio in this case)
79
+ const transport = new StdioServerTransport();
80
+ try {
81
+ // Connect the MCP server to the transport
82
+ await server.connect(transport);
83
+ console.error('Fitbit MCP Server connected via stdio.');
84
+ // Check if an access token is available after connection
85
+ // If not, initiate the OAuth2 authorization flow
86
+ const token = await getAccessToken();
87
+ if (!token) {
88
+ console.error('No access token found. Starting Fitbit authorization flow...');
89
+ startAuthorizationFlow(); // Start flow in background, do not await
90
+ }
91
+ else {
92
+ console.error('Using existing/loaded access token.');
93
+ }
94
+ }
95
+ catch (error) {
96
+ console.error('Failed to connect MCP server:', error);
97
+ process.exit(1); // Exit if connection fails
98
+ }
99
+ console.error('MCP Server setup complete. Waiting for requests...');
100
+ // The server connection via StdioServerTransport keeps the process alive.
101
+ }
102
+ // Execute the main function and handle any top-level errors
103
+ main().catch((error) => {
104
+ console.error('Fatal error during MCP server startup:', error.message || error);
105
+ process.exit(1);
106
+ });
@@ -0,0 +1,162 @@
1
+ import { z } from 'zod';
2
+ import { makeFitbitRequest } from './utils.js';
3
+ const FITBIT_API_BASE = 'https://api.fitbit.com/1';
4
+ // --- Tool Registration ---
5
+ /**
6
+ * Registers Fitbit nutrition tools with the MCP server.
7
+ * @param server The McpServer instance.
8
+ * @param getAccessTokenFn Function to retrieve the current access token.
9
+ */
10
+ export function registerNutritionTools(server, getAccessTokenFn) {
11
+ // --- Food Log Tool (comprehensive nutrition data) ---
12
+ const foodLogToolName = 'get_food_log';
13
+ const foodLogDescription = 'Get comprehensive nutrition data (calories, protein, carbs, fat, fiber, sodium) from Fitbit food log for a specific date. Returns daily summary totals and individual food entries with nutritional values.';
14
+ const foodLogParametersSchemaShape = {
15
+ date: z
16
+ .string()
17
+ .regex(/^\d{4}-\d{2}-\d{2}$|^today$/, "Date must be in YYYY-MM-DD format or 'today'.")
18
+ .optional()
19
+ .describe("The date for which to retrieve food log data (YYYY-MM-DD or 'today'). Defaults to 'today'."),
20
+ };
21
+ server.tool(foodLogToolName, foodLogDescription, foodLogParametersSchemaShape, async ({ date = 'today', }) => {
22
+ // Construct the endpoint
23
+ const endpoint = `foods/log/date/${date}.json`;
24
+ const foodLogData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
25
+ // Handle API call failure
26
+ if (!foodLogData) {
27
+ return {
28
+ content: [
29
+ {
30
+ type: 'text',
31
+ text: `Failed to retrieve food log data from Fitbit API for date '${date}'. Check token and permissions.`,
32
+ },
33
+ ],
34
+ isError: true,
35
+ };
36
+ }
37
+ // Return successful response with raw JSON
38
+ const rawJsonResponse = JSON.stringify(foodLogData, null, 2);
39
+ return {
40
+ content: [{ type: 'text', text: rawJsonResponse }],
41
+ };
42
+ });
43
+ // --- Nutrition by Date/Period Tool ---
44
+ const periodToolName = 'get_nutrition';
45
+ const periodDescription = "Get the raw JSON response for nutrition data from Fitbit for a specified resource and period ending today or on a specific date. Requires 'resource' parameter (caloriesIn, water) and 'period' parameter such as '1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y' and optionally accepts 'date' parameter.";
46
+ const periodParametersSchemaShape = {
47
+ resource: z
48
+ .enum([
49
+ 'caloriesIn',
50
+ 'water',
51
+ 'protein',
52
+ 'carbs',
53
+ 'fat',
54
+ 'fiber',
55
+ 'sodium',
56
+ ])
57
+ .describe('The nutrition resource to retrieve data for.'),
58
+ period: z
59
+ .enum(['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y'])
60
+ .describe('The time period for which to retrieve nutrition data.'),
61
+ date: z
62
+ .string()
63
+ .regex(/^\d{4}-\d{2}-\d{2}$|^today$/, "Date must be in YYYY-MM-DD format or 'today'.")
64
+ .optional()
65
+ .describe("The date for which to retrieve nutrition data (YYYY-MM-DD or 'today'). Defaults to 'today'."),
66
+ };
67
+ server.tool(periodToolName, periodDescription, periodParametersSchemaShape, async ({ resource, period, date = 'today', }) => {
68
+ // Construct the endpoint dynamically
69
+ const endpoint = `foods/log/${resource}/date/${date}/${period}.json`;
70
+ const nutritionData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
71
+ // Handle API call failure
72
+ if (!nutritionData) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ text: `Failed to retrieve nutrition data from Fitbit API for resource '${resource}', date '${date}' and period '${period}'. Check token and permissions.`,
78
+ },
79
+ ],
80
+ isError: true,
81
+ };
82
+ }
83
+ // Handle no data found for the period
84
+ const resourceKey = `foods-log-${resource}`;
85
+ const nutritionEntries = nutritionData[resourceKey] || [];
86
+ if (nutritionEntries.length === 0) {
87
+ return {
88
+ content: [
89
+ {
90
+ type: 'text',
91
+ text: `No nutrition data found for resource '${resource}', date '${date}' and period '${period}'.`,
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ // Return successful response with raw JSON
97
+ const rawJsonResponse = JSON.stringify(nutritionData, null, 2);
98
+ return {
99
+ content: [{ type: 'text', text: rawJsonResponse }],
100
+ };
101
+ });
102
+ // --- Nutrition by Date Range Tool ---
103
+ const rangeToolName = 'get_nutrition_by_date_range';
104
+ const rangeDescription = "Get the raw JSON response for nutrition data from Fitbit for a specific resource and date range. Requires 'resource' parameter (caloriesIn, water), 'startDate' and 'endDate' parameters in 'YYYY-MM-DD' format. Note: The API enforces a maximum range of 1,095 days.";
105
+ const rangeParametersSchemaShape = {
106
+ resource: z
107
+ .enum([
108
+ 'caloriesIn',
109
+ 'water',
110
+ 'protein',
111
+ 'carbs',
112
+ 'fat',
113
+ 'fiber',
114
+ 'sodium',
115
+ ])
116
+ .describe('The nutrition resource to retrieve data for.'),
117
+ startDate: z
118
+ .string()
119
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Start date must be in YYYY-MM-DD format.')
120
+ .describe('The start date for which to retrieve nutrition data (YYYY-MM-DD).'),
121
+ endDate: z
122
+ .string()
123
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'End date must be in YYYY-MM-DD format.')
124
+ .describe('The end date for which to retrieve nutrition data (YYYY-MM-DD).'),
125
+ };
126
+ server.tool(rangeToolName, rangeDescription, rangeParametersSchemaShape, async ({ resource, startDate, endDate, }) => {
127
+ // Construct the endpoint dynamically
128
+ const endpoint = `foods/log/${resource}/date/${startDate}/${endDate}.json`;
129
+ // Make the request
130
+ const nutritionData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
131
+ // Handle API call failure
132
+ if (!nutritionData) {
133
+ return {
134
+ content: [
135
+ {
136
+ type: 'text',
137
+ text: `Failed to retrieve nutrition data from Fitbit API for resource '${resource}' and the date range '${startDate}' to '${endDate}'. Check token, permissions, date format, and ensure the range is 1,095 days or less.`,
138
+ },
139
+ ],
140
+ isError: true,
141
+ };
142
+ }
143
+ // Handle no data found
144
+ const resourceKey = `foods-log-${resource}`;
145
+ const nutritionEntries = nutritionData[resourceKey] || [];
146
+ if (nutritionEntries.length === 0) {
147
+ return {
148
+ content: [
149
+ {
150
+ type: 'text',
151
+ text: `No nutrition data found for resource '${resource}' and the date range '${startDate}' to '${endDate}'.`,
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ // Return successful response
157
+ const rawJsonResponse = JSON.stringify(nutritionData, null, 2);
158
+ return {
159
+ content: [{ type: 'text', text: rawJsonResponse }],
160
+ };
161
+ });
162
+ }
@@ -0,0 +1,19 @@
1
+ import { registerTool, handleFitbitApiCall } from './utils.js';
2
+ /**
3
+ * Registers a single, parameterized Fitbit profile 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 registerProfileTool(server, getAccessTokenFn) {
8
+ registerTool(server, {
9
+ name: 'get_profile',
10
+ description: "Get the raw JSON response for the user's Fitbit profile.",
11
+ parametersSchema: {},
12
+ handler: async () => {
13
+ const endpoint = 'profile.json';
14
+ return handleFitbitApiCall(endpoint, {}, getAccessTokenFn, {
15
+ errorContext: 'profile data'
16
+ });
17
+ }
18
+ });
19
+ }
package/build/sleep.js ADDED
@@ -0,0 +1,27 @@
1
+ import { registerTool, CommonSchemas, handleFitbitApiCall } from './utils.js';
2
+ import { FITBIT_API_VERSIONS, VALIDATION_MESSAGES } from './config.js';
3
+ // --- Tool Registration ---
4
+ /**
5
+ * Registers a single, parameterized Fitbit sleep tool with the MCP server for a date range.
6
+ * @param server The McpServer instance.
7
+ * @param getAccessTokenFn Function to retrieve the current access token.
8
+ */
9
+ export function registerSleepTool(server, getAccessTokenFn) {
10
+ registerTool(server, {
11
+ name: 'get_sleep_by_date_range',
12
+ description: `Get the raw JSON response for sleep logs from Fitbit for a specific date range. Requires 'startDate' and 'endDate' parameters in 'YYYY-MM-DD' format. ${VALIDATION_MESSAGES.MAX_RANGE_100_DAYS}.`,
13
+ parametersSchema: {
14
+ startDate: CommonSchemas.startDate,
15
+ endDate: CommonSchemas.endDate,
16
+ },
17
+ handler: async ({ startDate, endDate }) => {
18
+ const endpoint = `/sleep/date/${startDate}/${endDate}.json`;
19
+ return handleFitbitApiCall(endpoint, { startDate, endDate }, getAccessTokenFn, {
20
+ apiBase: FITBIT_API_VERSIONS.V1_2,
21
+ successDataExtractor: (data) => data.sleep || [],
22
+ noDataMessage: `the date range '${startDate}' to '${endDate}'`,
23
+ errorContext: `date range '${startDate}' to '${endDate}'`
24
+ });
25
+ }
26
+ });
27
+ }
package/build/utils.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Shared utility functions for Fitbit API integration
3
+ */
4
+ import { z } from 'zod';
5
+ import { HTTP_CONFIG, ERROR_MESSAGES, FITBIT_API_VERSIONS, DATE_REGEX, VALIDATION_MESSAGES, TIME_PERIODS, HEART_RATE_DETAIL_LEVELS } from './config.js';
6
+ /**
7
+ * Makes a generic request to the Fitbit API.
8
+ * Handles adding the base URL, authorization header, and basic error handling.
9
+ * @param endpoint The specific API endpoint path (e.g., '/body/weight/date/today/30d.json').
10
+ * @param getAccessTokenFn A function that returns the current valid access token or null.
11
+ * @param apiBase The base URL for the API (defaults to v1, can be overridden for different API versions)
12
+ * @returns A promise resolving to the parsed JSON response (type T) or null if the request fails.
13
+ */
14
+ export async function makeFitbitRequest(endpoint, getAccessTokenFn, apiBase = FITBIT_API_VERSIONS.V1) {
15
+ const currentAccessToken = await getAccessTokenFn();
16
+ if (!currentAccessToken) {
17
+ console.error(`Error: ${ERROR_MESSAGES.NO_ACCESS_TOKEN}`);
18
+ return null;
19
+ }
20
+ // Ensure endpoint starts correctly relative to the user path
21
+ const cleanEndpoint = endpoint.startsWith('/')
22
+ ? endpoint.substring(1)
23
+ : endpoint;
24
+ // Construct the full URL including the user scope '-'.
25
+ const url = `${apiBase}/user/-/${cleanEndpoint}`;
26
+ console.error(`Attempting Fitbit API request to: ${url}`);
27
+ const headers = {
28
+ 'User-Agent': HTTP_CONFIG.USER_AGENT,
29
+ Authorization: `Bearer ${currentAccessToken}`,
30
+ Accept: 'application/json',
31
+ };
32
+ try {
33
+ const response = await fetch(url, { headers });
34
+ if (!response.ok) {
35
+ const errorBody = await response.text();
36
+ console.error(`Fitbit API Error! Status: ${response.status}, URL: ${url}, Body: ${errorBody}`);
37
+ if (response.status === 401) {
38
+ console.error(ERROR_MESSAGES.TOKEN_EXPIRED);
39
+ }
40
+ return null;
41
+ }
42
+ // Handle potential empty response body for certain success statuses (e.g., 204 No Content)
43
+ if (response.status === 204) {
44
+ return {}; // Return an empty object or appropriate type for no content
45
+ }
46
+ return (await response.json());
47
+ }
48
+ catch (error) {
49
+ console.error(`Error making Fitbit request to ${url}:`, error);
50
+ return null;
51
+ }
52
+ }
53
+ // === Parameter Validation Schemas ===
54
+ export const CommonSchemas = {
55
+ period: z
56
+ .enum(TIME_PERIODS)
57
+ .describe(VALIDATION_MESSAGES.PERIOD_REQUIRED),
58
+ startDate: z
59
+ .string()
60
+ .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT)
61
+ .describe(VALIDATION_MESSAGES.START_DATE_REQUIRED),
62
+ endDate: z
63
+ .string()
64
+ .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT)
65
+ .describe(VALIDATION_MESSAGES.END_DATE_REQUIRED),
66
+ detailLevel: z
67
+ .enum(HEART_RATE_DETAIL_LEVELS)
68
+ .describe(VALIDATION_MESSAGES.DETAIL_LEVEL_REQUIRED),
69
+ date: z
70
+ .string()
71
+ .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT)
72
+ .describe('The date for which to retrieve data (YYYY-MM-DD)'),
73
+ afterDate: z
74
+ .string()
75
+ .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT)
76
+ .describe('Retrieve activities after this date (YYYY-MM-DD)'),
77
+ limit: z
78
+ .number()
79
+ .min(1)
80
+ .max(100)
81
+ .optional()
82
+ .describe('Maximum number of items to return (1-100, default: 20)')
83
+ };
84
+ // === Tool Response Helpers ===
85
+ export function createErrorResponse(message) {
86
+ return {
87
+ content: [{ type: 'text', text: message }],
88
+ isError: true,
89
+ };
90
+ }
91
+ export function createSuccessResponse(data) {
92
+ const rawJsonResponse = JSON.stringify(data, null, 2);
93
+ return {
94
+ content: [{ type: 'text', text: rawJsonResponse }],
95
+ };
96
+ }
97
+ export function createNoDataResponse(context) {
98
+ return {
99
+ content: [{ type: 'text', text: `${ERROR_MESSAGES.NO_DATA_FOUND} ${context}.` }],
100
+ };
101
+ }
102
+ export function registerTool(server, config) {
103
+ server.tool(config.name, config.description, config.parametersSchema, config.handler);
104
+ }
105
+ // === Fitbit-Specific Tool Helpers ===
106
+ export async function handleFitbitApiCall(endpoint, params, getAccessTokenFn, options = {}) {
107
+ const { apiBase = FITBIT_API_VERSIONS.V1, successDataExtractor, noDataMessage, errorContext = JSON.stringify(params) } = options;
108
+ const responseData = await makeFitbitRequest(endpoint, getAccessTokenFn, apiBase);
109
+ if (!responseData) {
110
+ return createErrorResponse(`${ERROR_MESSAGES.API_REQUEST_FAILED} for ${errorContext}. ${ERROR_MESSAGES.CHECK_TOKEN_PERMISSIONS}.`);
111
+ }
112
+ // Check for empty data if extractor provided
113
+ if (successDataExtractor) {
114
+ const extractedData = successDataExtractor(responseData);
115
+ if (!extractedData || extractedData.length === 0) {
116
+ return createNoDataResponse(noDataMessage || errorContext);
117
+ }
118
+ }
119
+ return createSuccessResponse(responseData);
120
+ }
@@ -0,0 +1,24 @@
1
+ import { registerTool, CommonSchemas, handleFitbitApiCall } from './utils.js';
2
+ // --- Tool Registration ---
3
+ /**
4
+ * Registers a single, parameterized Fitbit weight 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 registerWeightTool(server, getAccessTokenFn) {
9
+ registerTool(server, {
10
+ name: 'get_weight',
11
+ description: "Get the raw JSON response for weight entries from Fitbit for a specified period ending today. Requires a 'period' parameter such as '1d', '7d', '30d', '3m', '6m', '1y'",
12
+ parametersSchema: {
13
+ period: CommonSchemas.period,
14
+ },
15
+ handler: async ({ period }) => {
16
+ const endpoint = `/body/weight/date/today/${period}.json`;
17
+ return handleFitbitApiCall(endpoint, { period }, getAccessTokenFn, {
18
+ successDataExtractor: (data) => data['body-weight'] || [],
19
+ noDataMessage: `the period '${period}'`,
20
+ errorContext: `period '${period}'`
21
+ });
22
+ }
23
+ });
24
+ }