@einlogic/mcp-fitbit 1.0.1 → 2.0.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.
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+ import { registerTool, CommonSchemas, listDataPoints, handleGoogleHealthCall, dataPointsPath, } from './utils.js';
3
+ import { DATA_TYPES } from './config.js';
4
+ const dataTypeSchema = z
5
+ .enum(DATA_TYPES)
6
+ .describe('Google Health data type (kebab-case), e.g. "steps", "body-fat"');
7
+ const filterSchema = z
8
+ .string()
9
+ .optional()
10
+ .describe('Optional filter, e.g. steps.interval.civil_start_time >= "2026-03-04T00:00:00". Data type names are snake_case inside filters');
11
+ /**
12
+ * Registers generic data point tools (escape hatches for any data type).
13
+ * @param server The McpServer instance.
14
+ * @param getAccessTokenFn Function to retrieve the current access token.
15
+ */
16
+ export function registerDataPointTools(server, getAccessTokenFn) {
17
+ registerTool(server, {
18
+ name: 'list_data_points',
19
+ description: 'Get the raw JSON response for data points of any Google Health data type (GET users/me/dataTypes/{type}/dataPoints), with an optional raw filter. Use this for data types not covered by the named tools.',
20
+ parametersSchema: {
21
+ dataType: dataTypeSchema,
22
+ filter: filterSchema,
23
+ pageSize: CommonSchemas.pageSize,
24
+ pageToken: CommonSchemas.pageToken,
25
+ },
26
+ handler: async ({ dataType, filter, pageSize, pageToken, }) => listDataPoints(dataType, getAccessTokenFn, { filter, pageSize, pageToken }, `data type '${dataType}'`),
27
+ });
28
+ registerTool(server, {
29
+ name: 'reconcile_data_points',
30
+ description: 'Get the raw JSON response for reconciled data points of any Google Health data type (GET .../dataPoints:reconcile). Reconciling removes overlapping intervals across devices and sync batches, so use it for timelines and durations.',
31
+ parametersSchema: {
32
+ dataType: dataTypeSchema,
33
+ filter: filterSchema,
34
+ dataSourceFamily: CommonSchemas.dataSourceFamily,
35
+ pageSize: CommonSchemas.pageSize,
36
+ pageToken: CommonSchemas.pageToken,
37
+ },
38
+ handler: async ({ dataType, filter, dataSourceFamily, pageSize, pageToken, }) => handleGoogleHealthCall(dataPointsPath(dataType, ':reconcile'), getAccessTokenFn, {
39
+ query: { filter, dataSourceFamily, pageSize, pageToken },
40
+ successDataExtractor: (data) => data.dataPoints,
41
+ errorContext: `reconciled data type '${dataType}'`,
42
+ }),
43
+ });
44
+ }
@@ -1,106 +1,36 @@
1
- import { z } from 'zod';
2
- import { makeFitbitRequest } from './utils.js';
3
- const FITBIT_API_BASE = 'https://api.fitbit.com/1';
4
- // --- Tool Registration ---
1
+ import { registerTool, CommonSchemas, listDataPoints, dailyRollUp, buildRangeFilter, } from './utils.js';
2
+ import { VALIDATION_MESSAGES } from './config.js';
5
3
  /**
6
- * Registers Fitbit heart rate tools with the MCP server.
4
+ * Registers heart rate tools with the MCP server.
7
5
  * @param server The McpServer instance.
8
6
  * @param getAccessTokenFn Function to retrieve the current access token.
9
7
  */
10
8
  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
- };
9
+ // Raw heart rate samples
10
+ registerTool(server, {
11
+ name: 'get_heart_rate',
12
+ description: 'Get the raw JSON response for heart rate samples from the Google Health API (data type "heart-rate"), filtered by sample time. Requires a start timestamp (RFC 3339); the end is optional and exclusive. Supports pagination via pageToken.',
13
+ parametersSchema: {
14
+ startTime: CommonSchemas.physicalStartTime,
15
+ endTime: CommonSchemas.physicalEndTime,
16
+ pageSize: CommonSchemas.pageSize,
17
+ pageToken: CommonSchemas.pageToken,
18
+ },
19
+ handler: async ({ startTime, endTime, pageSize, pageToken, }) => listDataPoints('heart-rate', getAccessTokenFn, {
20
+ filter: buildRangeFilter('heart_rate.sample_time.physical_time', startTime, endTime),
21
+ pageSize,
22
+ pageToken,
23
+ }, `heart rate from '${startTime}'${endTime ? ` to '${endTime}'` : ''}`),
57
24
  });
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
- };
25
+ // Daily heart rate roll-up
26
+ registerTool(server, {
27
+ name: 'get_heart_rate_by_date_range',
28
+ description: `Get the raw JSON response for daily heart rate roll-ups from the Google Health API (dailyRollUp of "heart-rate") over a civil date range. startDate is inclusive, endDate is exclusive. ${VALIDATION_MESSAGES.MAX_RANGE_14_DAYS}.`,
29
+ parametersSchema: {
30
+ startDate: CommonSchemas.startDate,
31
+ endDate: CommonSchemas.endDate,
32
+ dataSourceFamily: CommonSchemas.dataSourceFamily,
33
+ },
34
+ handler: async ({ startDate, endDate, dataSourceFamily, }) => dailyRollUp('heart-rate', getAccessTokenFn, { startDate, endDate, dataSourceFamily }, `heart rate from ${startDate} to ${endDate}`),
105
35
  });
106
36
  }
package/build/hrv.js CHANGED
@@ -1,35 +1,35 @@
1
- import { registerTool, CommonSchemas, handleFitbitApiCall, } from './utils.js';
1
+ import { registerTool, CommonSchemas, listDataPoints, buildRangeFilter, nextDate, } from './utils.js';
2
+ export const HRV_DATA_TYPE = 'daily-heart-rate-variability';
3
+ export const HRV_DATE_FIELD = 'daily_heart_rate_variability.date';
2
4
  /**
3
5
  * Registers Heart Rate Variability (HRV) tools with the MCP server.
4
6
  * @param server The McpServer instance.
5
7
  * @param getAccessTokenFn Function to retrieve the current access token.
6
8
  */
7
9
  export function registerHrvTools(server, getAccessTokenFn) {
10
+ // Tool 1: Get daily HRV for a single date
8
11
  registerTool(server, {
9
12
  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.',
13
+ description: 'Get the raw JSON response for daily Heart Rate Variability (HRV) from the Google Health API (data type "daily-heart-rate-variability") for a single date.',
11
14
  parametersSchema: {
12
15
  date: CommonSchemas.date,
13
16
  },
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
- },
17
+ handler: async ({ date }) => listDataPoints(HRV_DATA_TYPE, getAccessTokenFn, { filter: buildRangeFilter(HRV_DATE_FIELD, date, nextDate(date)) }, `HRV data for ${date}`),
20
18
  });
19
+ // Tool 2: Get daily HRV over a date range
21
20
  registerTool(server, {
22
21
  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.',
22
+ description: 'Get the raw JSON response for daily Heart Rate Variability (HRV) from the Google Health API (data type "daily-heart-rate-variability") over a date range. startDate is inclusive, endDate is exclusive. Supports pagination via pageToken.',
24
23
  parametersSchema: {
25
24
  startDate: CommonSchemas.startDate,
26
25
  endDate: CommonSchemas.endDate,
26
+ pageSize: CommonSchemas.pageSize,
27
+ pageToken: CommonSchemas.pageToken,
27
28
  },
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
- },
29
+ handler: async ({ startDate, endDate, pageSize, pageToken, }) => listDataPoints(HRV_DATA_TYPE, getAccessTokenFn, {
30
+ filter: buildRangeFilter(HRV_DATE_FIELD, startDate, endDate),
31
+ pageSize,
32
+ pageToken,
33
+ }, `HRV data from ${startDate} to ${endDate}`),
34
34
  });
35
35
  }
package/build/index.js CHANGED
@@ -14,10 +14,13 @@ import { registerActivitiesTool } from './activities.js';
14
14
  import { registerHeartRateTools } from './heart-rate.js';
15
15
  import { registerNutritionTools } from './nutrition.js';
16
16
  import { registerDailyActivityTool } from './daily-activity.js';
17
- import { registerActivityGoalsTool } from './activity-goals.js';
18
17
  import { registerActivityTimeSeriesTool } from './activity-timeseries.js';
19
18
  import { registerAzmTimeSeriesTool } from './azm-timeseries.js';
20
19
  import { registerHrvTools } from './hrv.js';
20
+ import { registerAccountTools } from './account.js';
21
+ import { registerDataPointTools } from './data-points.js';
22
+ import { registerCompatTools } from './compat/tools.js';
23
+ import { resolveClientCredentials, resolveOutputMode } from './config.js';
21
24
  // Import utilities
22
25
  import './utils.js';
23
26
  // Calculate the directory name of the current module (build/index.js)
@@ -29,20 +32,22 @@ const envPath = path.resolve(__dirname, '..', '.env');
29
32
  dotenv.config({ path: envPath });
30
33
  // Validate required environment variables
31
34
  function validateEnvironment() {
35
+ // GOOGLE_HEALTH_* is preferred; legacy FITBIT_* names are accepted as a fallback
36
+ const credentials = resolveClientCredentials();
32
37
  const requiredVars = {
33
- FITBIT_CLIENT_ID: process.env.FITBIT_CLIENT_ID,
34
- FITBIT_CLIENT_SECRET: process.env.FITBIT_CLIENT_SECRET,
38
+ GOOGLE_HEALTH_CLIENT_ID: credentials.id,
39
+ GOOGLE_HEALTH_CLIENT_SECRET: credentials.secret,
35
40
  };
36
41
  const missingVars = Object.entries(requiredVars)
37
42
  .filter(([, value]) => !value)
38
43
  .map(([key]) => key);
39
44
  if (missingVars.length > 0) {
40
45
  console.error('Missing required environment variables:');
41
- missingVars.forEach(varName => {
46
+ missingVars.forEach((varName) => {
42
47
  console.error(` - ${varName}`);
43
48
  });
44
49
  console.error(`Please create a .env file at: ${envPath}`);
45
- console.error('See README.md for details on getting Fitbit API credentials.');
50
+ console.error('See README.md for details on getting Google Health API credentials.');
46
51
  process.exit(1);
47
52
  }
48
53
  }
@@ -52,25 +57,34 @@ validateEnvironment();
52
57
  console.error('✅ Environment variables loaded successfully');
53
58
  // Create the main MCP server instance
54
59
  const server = new McpServer({
55
- name: 'fitbit',
56
- version: '1.0.0',
60
+ name: 'google-health',
61
+ version: '2.0.0',
57
62
  capabilities: {
58
63
  resources: {},
59
64
  tools: {}, // Tools are registered dynamically below
60
65
  },
61
66
  });
62
- // Register available tools with the MCP server
63
- registerWeightTool(server, getAccessToken);
64
- registerSleepTool(server, getAccessToken);
67
+ // Register available tools with the MCP server.
68
+ // GOOGLE_HEALTH_OUTPUT_MODE=fitbit swaps the tools that overlap the legacy Fitbit tools for
69
+ // Fitbit-shaped versions; everything else stays a 1:1 Google Health proxy.
70
+ if (resolveOutputMode() === 'fitbit') {
71
+ console.error('Output mode: fitbit (legacy Fitbit tool names, parameters, shapes and units)');
72
+ registerCompatTools(server, getAccessToken);
73
+ }
74
+ else {
75
+ registerWeightTool(server, getAccessToken);
76
+ registerSleepTool(server, getAccessToken);
77
+ registerHeartRateTools(server, getAccessToken);
78
+ registerNutritionTools(server, getAccessToken);
79
+ registerDailyActivityTool(server, getAccessToken);
80
+ registerActivityTimeSeriesTool(server, getAccessToken);
81
+ registerAzmTimeSeriesTool(server, getAccessToken);
82
+ registerHrvTools(server, getAccessToken);
83
+ }
65
84
  registerProfileTool(server, getAccessToken);
66
85
  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);
86
+ registerAccountTools(server, getAccessToken);
87
+ registerDataPointTools(server, getAccessToken);
74
88
  // --- Main Application Entry Point ---
75
89
  async function main() {
76
90
  // Initialize the authentication module (e.g., load persisted token)
@@ -80,12 +94,12 @@ async function main() {
80
94
  try {
81
95
  // Connect the MCP server to the transport
82
96
  await server.connect(transport);
83
- console.error('Fitbit MCP Server connected via stdio.');
97
+ console.error('Google Health MCP Server connected via stdio.');
84
98
  // Check if an access token is available after connection
85
99
  // If not, initiate the OAuth2 authorization flow
86
100
  const token = await getAccessToken();
87
101
  if (!token) {
88
- console.error('No access token found. Starting Fitbit authorization flow...');
102
+ console.error('No access token found. Starting Google Health authorization flow...');
89
103
  startAuthorizationFlow(); // Start flow in background, do not await
90
104
  }
91
105
  else {
@@ -1,162 +1,51 @@
1
1
  import { z } from 'zod';
2
- import { makeFitbitRequest } from './utils.js';
3
- const FITBIT_API_BASE = 'https://api.fitbit.com/1';
4
- // --- Tool Registration ---
2
+ import { registerTool, CommonSchemas, listDataPoints, dailyRollUp, buildRangeFilter, nextDate, } from './utils.js';
3
+ // Nutrition-related data types that support roll-ups
4
+ export const NUTRITION_DATA_TYPES = ['nutrition-log', 'hydration-log'];
5
+ const dataTypeSchema = z
6
+ .enum(NUTRITION_DATA_TYPES)
7
+ .describe("Data type to roll up: 'nutrition-log' (calories and macronutrients) or 'hydration-log' (water)");
5
8
  /**
6
- * Registers Fitbit nutrition tools with the MCP server.
9
+ * Registers nutrition tools with the MCP server.
7
10
  * @param server The McpServer instance.
8
11
  * @param getAccessTokenFn Function to retrieve the current access token.
9
12
  */
10
13
  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
- };
14
+ // --- Food Log Tool ---
15
+ registerTool(server, {
16
+ name: 'get_food_log',
17
+ description: 'Get the raw JSON response for logged nutrition entries from the Google Health API (data type "nutrition-log"), filtered by civil start time. Requires a start civil time; the end is optional and exclusive. Supports pagination via pageToken.',
18
+ parametersSchema: {
19
+ startTime: CommonSchemas.civilStartTime,
20
+ endTime: CommonSchemas.civilEndTime,
21
+ pageSize: CommonSchemas.pageSize,
22
+ pageToken: CommonSchemas.pageToken,
23
+ },
24
+ handler: async ({ startTime, endTime, pageSize, pageToken, }) => listDataPoints('nutrition-log', getAccessTokenFn, {
25
+ filter: buildRangeFilter('nutrition_log.interval.civil_start_time', startTime, endTime),
26
+ pageSize,
27
+ pageToken,
28
+ }, `food log from '${startTime}'${endTime ? ` to '${endTime}'` : ''}`),
42
29
  });
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
- };
30
+ // --- Nutrition for a single date ---
31
+ registerTool(server, {
32
+ name: 'get_nutrition',
33
+ description: 'Get the raw JSON response for the daily nutrition roll-up from the Google Health API (dailyRollUp) for a single date.',
34
+ parametersSchema: {
35
+ dataType: dataTypeSchema,
36
+ date: CommonSchemas.date,
37
+ },
38
+ handler: async ({ dataType, date, }) => dailyRollUp(dataType, getAccessTokenFn, { startDate: date, endDate: nextDate(date) }, `${dataType} for ${date}`),
101
39
  });
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
- };
40
+ // --- Nutrition by date range ---
41
+ registerTool(server, {
42
+ name: 'get_nutrition_by_date_range',
43
+ description: 'Get the raw JSON response for daily nutrition roll-ups from the Google Health API (dailyRollUp) over a civil date range. startDate is inclusive, endDate is exclusive. Note: The API allows at most 90 days per request.',
44
+ parametersSchema: {
45
+ dataType: dataTypeSchema,
46
+ startDate: CommonSchemas.startDate,
47
+ endDate: CommonSchemas.endDate,
48
+ },
49
+ handler: async ({ dataType, startDate, endDate, }) => dailyRollUp(dataType, getAccessTokenFn, { startDate, endDate }, `${dataType} from ${startDate} to ${endDate}`),
161
50
  });
162
51
  }
package/build/profile.js CHANGED
@@ -1,19 +1,16 @@
1
- import { registerTool, handleFitbitApiCall } from './utils.js';
1
+ import { registerTool, handleGoogleHealthCall } from './utils.js';
2
2
  /**
3
- * Registers a single, parameterized Fitbit profile tool with the MCP server.
3
+ * Registers the profile tool with the MCP server.
4
4
  * @param server The McpServer instance.
5
5
  * @param getAccessTokenFn Function to retrieve the current access token.
6
6
  */
7
7
  export function registerProfileTool(server, getAccessTokenFn) {
8
8
  registerTool(server, {
9
9
  name: 'get_profile',
10
- description: "Get the raw JSON response for the user's Fitbit profile.",
10
+ description: "Get the raw JSON response for the user's Google Health profile (age, membership start date, stride lengths).",
11
11
  parametersSchema: {},
12
- handler: async () => {
13
- const endpoint = 'profile.json';
14
- return handleFitbitApiCall(endpoint, {}, getAccessTokenFn, {
15
- errorContext: 'profile data'
16
- });
17
- }
12
+ handler: async () => handleGoogleHealthCall('users/me/profile', getAccessTokenFn, {
13
+ errorContext: 'profile data',
14
+ }),
18
15
  });
19
16
  }
package/build/sleep.js CHANGED
@@ -1,27 +1,23 @@
1
- import { registerTool, CommonSchemas, handleFitbitApiCall } from './utils.js';
2
- import { FITBIT_API_VERSIONS, VALIDATION_MESSAGES } from './config.js';
3
- // --- Tool Registration ---
1
+ import { registerTool, CommonSchemas, listDataPoints, buildRangeFilter, } from './utils.js';
4
2
  /**
5
- * Registers a single, parameterized Fitbit sleep tool with the MCP server for a date range.
3
+ * Registers the sleep tool with the MCP server (Google Health `sleep` data type).
6
4
  * @param server The McpServer instance.
7
5
  * @param getAccessTokenFn Function to retrieve the current access token.
8
6
  */
9
7
  export function registerSleepTool(server, getAccessTokenFn) {
10
8
  registerTool(server, {
11
9
  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}.`,
10
+ description: 'Get the raw JSON response for sleep sessions from the Google Health API (data type "sleep"), filtered by the civil end time of the sleep. Requires a start civil time; the end is optional and exclusive. Note: the API returns at most 25 sleep sessions per page; use pageToken to continue.',
13
11
  parametersSchema: {
14
- startDate: CommonSchemas.startDate,
15
- endDate: CommonSchemas.endDate,
12
+ startTime: CommonSchemas.civilStartTime,
13
+ endTime: CommonSchemas.civilEndTime,
14
+ pageSize: CommonSchemas.pageSize,
15
+ pageToken: CommonSchemas.pageToken,
16
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
- }
17
+ handler: async ({ startTime, endTime, pageSize, pageToken, }) => listDataPoints('sleep', getAccessTokenFn, {
18
+ filter: buildRangeFilter('sleep.interval.civil_end_time', startTime, endTime),
19
+ pageSize,
20
+ pageToken,
21
+ }, `sleep from '${startTime}'${endTime ? ` to '${endTime}'` : ''}`),
26
22
  });
27
23
  }