UNPKG

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.

121 lines (120 loc) 4.99 kB
/** * Shared utility functions for Fitbit API integration */ import { z } from 'zod'; import { HTTP_CONFIG, ERROR_MESSAGES, FITBIT_API_VERSIONS, DATE_REGEX, VALIDATION_MESSAGES, TIME_PERIODS, HEART_RATE_DETAIL_LEVELS } from './config.js'; /** * Makes a generic request to the Fitbit API. * Handles adding the base URL, authorization header, and basic error handling. * @param endpoint The specific API endpoint path (e.g., '/body/weight/date/today/30d.json'). * @param getAccessTokenFn A function that returns the current valid access token or null. * @param apiBase The base URL for the API (defaults to v1, can be overridden for different API versions) * @returns A promise resolving to the parsed JSON response (type T) or null if the request fails. */ export async function makeFitbitRequest(endpoint, getAccessTokenFn, apiBase = FITBIT_API_VERSIONS.V1) { const currentAccessToken = await getAccessTokenFn(); if (!currentAccessToken) { console.error(`Error: ${ERROR_MESSAGES.NO_ACCESS_TOKEN}`); return null; } // Ensure endpoint starts correctly relative to the user path const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // Construct the full URL including the user scope '-'. const url = `${apiBase}/user/-/${cleanEndpoint}`; console.error(`Attempting Fitbit API request to: ${url}`); const headers = { 'User-Agent': HTTP_CONFIG.USER_AGENT, Authorization: `Bearer ${currentAccessToken}`, Accept: 'application/json', }; try { const response = await fetch(url, { headers }); if (!response.ok) { const errorBody = await response.text(); console.error(`Fitbit API Error! Status: ${response.status}, URL: ${url}, Body: ${errorBody}`); if (response.status === 401) { console.error(ERROR_MESSAGES.TOKEN_EXPIRED); } return null; } // Handle potential empty response body for certain success statuses (e.g., 204 No Content) if (response.status === 204) { return {}; // Return an empty object or appropriate type for no content } return (await response.json()); } catch (error) { console.error(`Error making Fitbit request to ${url}:`, error); return null; } } // === Parameter Validation Schemas === export const CommonSchemas = { period: z .enum(TIME_PERIODS) .describe(VALIDATION_MESSAGES.PERIOD_REQUIRED), startDate: z .string() .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT) .describe(VALIDATION_MESSAGES.START_DATE_REQUIRED), endDate: z .string() .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT) .describe(VALIDATION_MESSAGES.END_DATE_REQUIRED), detailLevel: z .enum(HEART_RATE_DETAIL_LEVELS) .describe(VALIDATION_MESSAGES.DETAIL_LEVEL_REQUIRED), date: z .string() .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT) .describe('The date for which to retrieve data (YYYY-MM-DD)'), afterDate: z .string() .regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT) .describe('Retrieve activities after this date (YYYY-MM-DD)'), limit: z .number() .min(1) .max(100) .optional() .describe('Maximum number of items to return (1-100, default: 20)') }; // === Tool Response Helpers === export function createErrorResponse(message) { return { content: [{ type: 'text', text: message }], isError: true, }; } export function createSuccessResponse(data) { const rawJsonResponse = JSON.stringify(data, null, 2); return { content: [{ type: 'text', text: rawJsonResponse }], }; } export function createNoDataResponse(context) { return { content: [{ type: 'text', text: `${ERROR_MESSAGES.NO_DATA_FOUND} ${context}.` }], }; } export function registerTool(server, config) { server.tool(config.name, config.description, config.parametersSchema, config.handler); } // === Fitbit-Specific Tool Helpers === export async function handleFitbitApiCall(endpoint, params, getAccessTokenFn, options = {}) { const { apiBase = FITBIT_API_VERSIONS.V1, successDataExtractor, noDataMessage, errorContext = JSON.stringify(params) } = options; const responseData = await makeFitbitRequest(endpoint, getAccessTokenFn, apiBase); if (!responseData) { return createErrorResponse(`${ERROR_MESSAGES.API_REQUEST_FAILED} for ${errorContext}. ${ERROR_MESSAGES.CHECK_TOKEN_PERMISSIONS}.`); } // Check for empty data if extractor provided if (successDataExtractor) { const extractedData = successDataExtractor(responseData); if (!extractedData || extractedData.length === 0) { return createNoDataResponse(noDataMessage || errorContext); } } return createSuccessResponse(responseData); }