@einlogic/mcp-fitbit 1.0.1 → 2.1.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 +185 -209
- package/build/account.js +32 -0
- package/build/activities.js +12 -14
- package/build/activity-timeseries.js +14 -20
- package/build/auth-tools.js +31 -0
- package/build/auth.js +197 -105
- package/build/azm-timeseries.js +4 -8
- package/build/compat/api.js +136 -0
- package/build/compat/convert.js +434 -0
- package/build/compat/tools.js +432 -0
- package/build/config.js +102 -32
- package/build/daily-activity.js +35 -7
- package/build/data-points.js +44 -0
- package/build/heart-rate.js +28 -98
- package/build/hrv.js +15 -15
- package/build/index.js +36 -20
- package/build/nutrition.js +41 -152
- package/build/profile.js +6 -9
- package/build/sleep.js +12 -16
- package/build/utils.js +193 -50
- package/build/weight.js +12 -13
- package/package.json +74 -74
|
@@ -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
|
+
}
|
package/build/heart-rate.js
CHANGED
|
@@ -1,106 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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,
|
|
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)
|
|
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)
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
@@ -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';
|
|
@@ -14,10 +14,14 @@ 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 { registerAuthTools } from './auth-tools.js';
|
|
22
|
+
import { registerDataPointTools } from './data-points.js';
|
|
23
|
+
import { registerCompatTools } from './compat/tools.js';
|
|
24
|
+
import { resolveClientCredentials, resolveOutputMode } from './config.js';
|
|
21
25
|
// Import utilities
|
|
22
26
|
import './utils.js';
|
|
23
27
|
// Calculate the directory name of the current module (build/index.js)
|
|
@@ -29,20 +33,22 @@ const envPath = path.resolve(__dirname, '..', '.env');
|
|
|
29
33
|
dotenv.config({ path: envPath });
|
|
30
34
|
// Validate required environment variables
|
|
31
35
|
function validateEnvironment() {
|
|
36
|
+
// GOOGLE_HEALTH_* is preferred; legacy FITBIT_* names are accepted as a fallback
|
|
37
|
+
const credentials = resolveClientCredentials();
|
|
32
38
|
const requiredVars = {
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
GOOGLE_HEALTH_CLIENT_ID: credentials.id,
|
|
40
|
+
GOOGLE_HEALTH_CLIENT_SECRET: credentials.secret,
|
|
35
41
|
};
|
|
36
42
|
const missingVars = Object.entries(requiredVars)
|
|
37
43
|
.filter(([, value]) => !value)
|
|
38
44
|
.map(([key]) => key);
|
|
39
45
|
if (missingVars.length > 0) {
|
|
40
46
|
console.error('Missing required environment variables:');
|
|
41
|
-
missingVars.forEach(varName => {
|
|
47
|
+
missingVars.forEach((varName) => {
|
|
42
48
|
console.error(` - ${varName}`);
|
|
43
49
|
});
|
|
44
50
|
console.error(`Please create a .env file at: ${envPath}`);
|
|
45
|
-
console.error('See README.md for details on getting
|
|
51
|
+
console.error('See README.md for details on getting Google Health API credentials.');
|
|
46
52
|
process.exit(1);
|
|
47
53
|
}
|
|
48
54
|
}
|
|
@@ -52,25 +58,35 @@ validateEnvironment();
|
|
|
52
58
|
console.error('✅ Environment variables loaded successfully');
|
|
53
59
|
// Create the main MCP server instance
|
|
54
60
|
const server = new McpServer({
|
|
55
|
-
name: '
|
|
56
|
-
version: '1.0
|
|
61
|
+
name: 'google-health',
|
|
62
|
+
version: '2.1.0',
|
|
57
63
|
capabilities: {
|
|
58
64
|
resources: {},
|
|
59
65
|
tools: {}, // Tools are registered dynamically below
|
|
60
66
|
},
|
|
61
67
|
});
|
|
62
|
-
// Register available tools with the MCP server
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
// Register available tools with the MCP server.
|
|
69
|
+
// GOOGLE_HEALTH_OUTPUT_MODE=fitbit swaps the tools that overlap the legacy Fitbit tools for
|
|
70
|
+
// Fitbit-shaped versions; everything else stays a 1:1 Google Health proxy.
|
|
71
|
+
if (resolveOutputMode() === 'fitbit') {
|
|
72
|
+
console.error('Output mode: fitbit (legacy Fitbit tool names, parameters, shapes and units)');
|
|
73
|
+
registerCompatTools(server, getAccessToken);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
registerWeightTool(server, getAccessToken);
|
|
77
|
+
registerSleepTool(server, getAccessToken);
|
|
78
|
+
registerHeartRateTools(server, getAccessToken);
|
|
79
|
+
registerNutritionTools(server, getAccessToken);
|
|
80
|
+
registerDailyActivityTool(server, getAccessToken);
|
|
81
|
+
registerActivityTimeSeriesTool(server, getAccessToken);
|
|
82
|
+
registerAzmTimeSeriesTool(server, getAccessToken);
|
|
83
|
+
registerHrvTools(server, getAccessToken);
|
|
84
|
+
}
|
|
65
85
|
registerProfileTool(server, getAccessToken);
|
|
66
86
|
registerActivitiesTool(server, getAccessToken);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
registerActivityGoalsTool(server, getAccessToken);
|
|
71
|
-
registerActivityTimeSeriesTool(server, getAccessToken);
|
|
72
|
-
registerAzmTimeSeriesTool(server, getAccessToken);
|
|
73
|
-
registerHrvTools(server, getAccessToken);
|
|
87
|
+
registerAccountTools(server, getAccessToken);
|
|
88
|
+
registerAuthTools(server, reauthenticate);
|
|
89
|
+
registerDataPointTools(server, getAccessToken);
|
|
74
90
|
// --- Main Application Entry Point ---
|
|
75
91
|
async function main() {
|
|
76
92
|
// Initialize the authentication module (e.g., load persisted token)
|
|
@@ -80,12 +96,12 @@ async function main() {
|
|
|
80
96
|
try {
|
|
81
97
|
// Connect the MCP server to the transport
|
|
82
98
|
await server.connect(transport);
|
|
83
|
-
console.error('
|
|
99
|
+
console.error('Google Health MCP Server connected via stdio.');
|
|
84
100
|
// Check if an access token is available after connection
|
|
85
101
|
// If not, initiate the OAuth2 authorization flow
|
|
86
102
|
const token = await getAccessToken();
|
|
87
103
|
if (!token) {
|
|
88
|
-
console.error('No access token found. Starting
|
|
104
|
+
console.error('No access token found. Starting Google Health authorization flow...');
|
|
89
105
|
startAuthorizationFlow(); // Start flow in background, do not await
|
|
90
106
|
}
|
|
91
107
|
else {
|
package/build/nutrition.js
CHANGED
|
@@ -1,162 +1,51 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
|
1
|
+
import { registerTool, handleGoogleHealthCall } from './utils.js';
|
|
2
2
|
/**
|
|
3
|
-
* Registers
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
15
|
-
|
|
12
|
+
startTime: CommonSchemas.civilStartTime,
|
|
13
|
+
endTime: CommonSchemas.civilEndTime,
|
|
14
|
+
pageSize: CommonSchemas.pageSize,
|
|
15
|
+
pageToken: CommonSchemas.pageToken,
|
|
16
16
|
},
|
|
17
|
-
handler: async ({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
}
|