mcp-fitbit
Version:
Model Context Protocol (MCP) server for accessing Fitbit health and fitness data. Connect AI assistants like Claude to your Fitbit data for personalized health insights.
163 lines (162 loc) • 7.5 kB
JavaScript
import { z } from 'zod';
import { makeFitbitRequest } from './utils.js';
const FITBIT_API_BASE = 'https://api.fitbit.com/1';
// --- Tool Registration ---
/**
* Registers Fitbit nutrition tools with the MCP server.
* @param server The McpServer instance.
* @param getAccessTokenFn Function to retrieve the current access token.
*/
export function registerNutritionTools(server, getAccessTokenFn) {
// --- Food Log Tool (comprehensive nutrition data) ---
const foodLogToolName = 'get_food_log';
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.';
const foodLogParametersSchemaShape = {
date: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$|^today$/, "Date must be in YYYY-MM-DD format or 'today'.")
.optional()
.describe("The date for which to retrieve food log data (YYYY-MM-DD or 'today'). Defaults to 'today'."),
};
server.tool(foodLogToolName, foodLogDescription, foodLogParametersSchemaShape, async ({ date = 'today', }) => {
// Construct the endpoint
const endpoint = `foods/log/date/${date}.json`;
const foodLogData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
// Handle API call failure
if (!foodLogData) {
return {
content: [
{
type: 'text',
text: `Failed to retrieve food log data from Fitbit API for date '${date}'. Check token and permissions.`,
},
],
isError: true,
};
}
// Return successful response with raw JSON
const rawJsonResponse = JSON.stringify(foodLogData, null, 2);
return {
content: [{ type: 'text', text: rawJsonResponse }],
};
});
// --- Nutrition by Date/Period Tool ---
const periodToolName = 'get_nutrition';
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.";
const periodParametersSchemaShape = {
resource: z
.enum([
'caloriesIn',
'water',
'protein',
'carbs',
'fat',
'fiber',
'sodium',
])
.describe('The nutrition resource to retrieve data for.'),
period: z
.enum(['1d', '7d', '30d', '1w', '1m', '3m', '6m', '1y'])
.describe('The time period for which to retrieve nutrition data.'),
date: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$|^today$/, "Date must be in YYYY-MM-DD format or 'today'.")
.optional()
.describe("The date for which to retrieve nutrition data (YYYY-MM-DD or 'today'). Defaults to 'today'."),
};
server.tool(periodToolName, periodDescription, periodParametersSchemaShape, async ({ resource, period, date = 'today', }) => {
// Construct the endpoint dynamically
const endpoint = `foods/log/${resource}/date/${date}/${period}.json`;
const nutritionData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
// Handle API call failure
if (!nutritionData) {
return {
content: [
{
type: 'text',
text: `Failed to retrieve nutrition data from Fitbit API for resource '${resource}', date '${date}' and period '${period}'. Check token and permissions.`,
},
],
isError: true,
};
}
// Handle no data found for the period
const resourceKey = `foods-log-${resource}`;
const nutritionEntries = nutritionData[resourceKey] || [];
if (nutritionEntries.length === 0) {
return {
content: [
{
type: 'text',
text: `No nutrition data found for resource '${resource}', date '${date}' and period '${period}'.`,
},
],
};
}
// Return successful response with raw JSON
const rawJsonResponse = JSON.stringify(nutritionData, null, 2);
return {
content: [{ type: 'text', text: rawJsonResponse }],
};
});
// --- Nutrition by Date Range Tool ---
const rangeToolName = 'get_nutrition_by_date_range';
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.";
const rangeParametersSchemaShape = {
resource: z
.enum([
'caloriesIn',
'water',
'protein',
'carbs',
'fat',
'fiber',
'sodium',
])
.describe('The nutrition resource to retrieve data for.'),
startDate: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Start date must be in YYYY-MM-DD format.')
.describe('The start date for which to retrieve nutrition data (YYYY-MM-DD).'),
endDate: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, 'End date must be in YYYY-MM-DD format.')
.describe('The end date for which to retrieve nutrition data (YYYY-MM-DD).'),
};
server.tool(rangeToolName, rangeDescription, rangeParametersSchemaShape, async ({ resource, startDate, endDate, }) => {
// Construct the endpoint dynamically
const endpoint = `foods/log/${resource}/date/${startDate}/${endDate}.json`;
// Make the request
const nutritionData = await makeFitbitRequest(endpoint, getAccessTokenFn, FITBIT_API_BASE);
// Handle API call failure
if (!nutritionData) {
return {
content: [
{
type: 'text',
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.`,
},
],
isError: true,
};
}
// Handle no data found
const resourceKey = `foods-log-${resource}`;
const nutritionEntries = nutritionData[resourceKey] || [];
if (nutritionEntries.length === 0) {
return {
content: [
{
type: 'text',
text: `No nutrition data found for resource '${resource}' and the date range '${startDate}' to '${endDate}'.`,
},
],
};
}
// Return successful response
const rawJsonResponse = JSON.stringify(nutritionData, null, 2);
return {
content: [{ type: 'text', text: rawJsonResponse }],
};
});
}