Skip to main content
Glama
activity-timeseries.test.ts7.25 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { registerActivityTimeSeriesTool } from './activity-timeseries.js'; import * as utils from './utils.js'; import { CommonSchemas } from './utils.js'; // Import CommonSchemas // Mock the utils module vi.mock('./utils.js', async (importOriginal) => { const actualUtils = await importOriginal<typeof utils>(); return { ...actualUtils, // Import and retain all original exports registerTool: vi.fn(), handleFitbitApiCall: vi.fn(), }; }); describe('Activity Time Series Tool', () => { let mockServer: McpServer; let mockGetAccessToken: ReturnType<typeof vi.fn>; let mockRegisterTool: ReturnType<typeof vi.fn>; let mockHandleFitbitApiCall: ReturnType<typeof vi.fn>; const validParams = { resourcePath: 'steps', startDate: '2024-01-01', endDate: '2024-01-07', }; const mockActivityTimeSeriesData = { 'activities-steps': [ { dateTime: '2024-01-01', value: '1000' }, { dateTime: '2024-01-02', value: '1500' }, ], }; beforeEach(() => { mockServer = {} as McpServer; mockGetAccessToken = vi.fn(); // Correctly mock functions from the imported 'utils' module mockRegisterTool = vi.mocked(utils.registerTool); mockHandleFitbitApiCall = vi.mocked(utils.handleFitbitApiCall); vi.clearAllMocks(); }); afterEach(() => { vi.resetAllMocks(); }); describe('registerActivityTimeSeriesTool', () => { it('should register the get_activity_timeseries tool with correct configuration', () => { registerActivityTimeSeriesTool(mockServer, mockGetAccessToken); expect(mockRegisterTool).toHaveBeenCalledOnce(); const registeredToolConfig = mockRegisterTool.mock.calls[0][1]; expect(registeredToolConfig.name).toBe('get_activity_timeseries'); expect(registeredToolConfig.description).toBe( "Get the raw JSON response for activity time series data from Fitbit over a date range (max 30 days). Supports various resource paths like 'steps', 'distance', 'calories', 'activityCalories', 'caloriesBMR'." ); expect(registeredToolConfig.parametersSchema).toEqual({ resourcePath: expect.any(z.ZodEnum), startDate: CommonSchemas.startDate, endDate: CommonSchemas.endDate, }); expect(registeredToolConfig.handler).toEqual(expect.any(Function)); }); it('should call handler with correct endpoint and parameters for a valid request', async () => { mockHandleFitbitApiCall.mockResolvedValue({ content: [{ type: 'text', text: JSON.stringify(mockActivityTimeSeriesData) }], }); registerActivityTimeSeriesTool(mockServer, mockGetAccessToken); const handler = mockRegisterTool.mock.calls[0][1].handler; const result = await handler(validParams); const expectedEndpoint = `activities/${validParams.resourcePath}/date/${validParams.startDate}/${validParams.endDate}.json`; expect(mockHandleFitbitApiCall).toHaveBeenCalledWith( expectedEndpoint, validParams, mockGetAccessToken, { errorContext: `resource '${validParams.resourcePath}' from ${validParams.startDate} to ${validParams.endDate}`, } ); expect(result).toEqual({ content: [{ type: 'text', text: JSON.stringify(mockActivityTimeSeriesData) }], }); }); it('should handle API errors gracefully', async () => { const errorMessage = 'Fitbit API error'; mockHandleFitbitApiCall.mockRejectedValue(new Error(errorMessage)); registerActivityTimeSeriesTool(mockServer, mockGetAccessToken); const handler = mockRegisterTool.mock.calls[0][1].handler; await expect(handler(validParams)).rejects.toThrow(errorMessage); const expectedEndpoint = `activities/${validParams.resourcePath}/date/${validParams.startDate}/${validParams.endDate}.json`; expect(mockHandleFitbitApiCall).toHaveBeenCalledWith( expectedEndpoint, validParams, mockGetAccessToken, { errorContext: `resource '${validParams.resourcePath}' from ${validParams.startDate} to ${validParams.endDate}`, } ); }); it('should handle null access token', async () => { mockGetAccessToken.mockResolvedValue(null); // Simulate handleFitbitApiCall throwing an error when no token is present mockHandleFitbitApiCall.mockRejectedValue(new Error('No access token available')); registerActivityTimeSeriesTool(mockServer, mockGetAccessToken); const handler = mockRegisterTool.mock.calls[0][1].handler; await expect(handler(validParams)).rejects.toThrow('No access token available'); }); // Test for different resource paths const resourcePathsToTest = [ 'steps', 'distance', 'calories', 'activityCalories', 'caloriesBMR', 'tracker/activityCalories', 'tracker/calories', 'tracker/distance' ]; resourcePathsToTest.forEach(resourcePath => { it(`should construct the correct endpoint for resourcePath: ${resourcePath}`, async () => { const params = { ...validParams, resourcePath }; registerActivityTimeSeriesTool(mockServer, mockGetAccessToken); const handler = mockRegisterTool.mock.calls[0][1].handler; // Catch expected error if API call fails, we only care about the endpoint construction await handler(params).catch(() => {}); const expectedEndpoint = `activities/${resourcePath}/date/${params.startDate}/${params.endDate}.json`; expect(mockHandleFitbitApiCall).toHaveBeenCalledWith( expectedEndpoint, params, mockGetAccessToken, expect.any(Object) // errorContext can vary, so not strictly checking its content here ); }); }); it('should have correct parameter schema definition', () => { registerActivityTimeSeriesTool(mockServer, mockGetAccessToken); const schema = mockRegisterTool.mock.calls[0][1].parametersSchema; // Test resourcePath enum const validResourcePaths = [ 'steps', 'distance', 'calories', 'activityCalories', 'caloriesBMR', 'tracker/activityCalories', 'tracker/calories', 'tracker/distance' ]; validResourcePaths.forEach(rp => { expect(schema.resourcePath.safeParse(rp).success).toBe(true); }); expect(schema.resourcePath.safeParse('invalidPath').success).toBe(false); // Test startDate format expect(schema.startDate.safeParse('2024-12-31').success).toBe(true); expect(schema.startDate.safeParse('2024/12/31').success).toBe(false); // Invalid format expect(schema.startDate.safeParse('not-a-date').success).toBe(false); // Test endDate format expect(schema.endDate.safeParse('2025-01-01').success).toBe(true); expect(schema.endDate.safeParse('2025/01/01').success).toBe(false); // Invalid format expect(schema.endDate.safeParse('another-invalid-date').success).toBe(false); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheDigitalNinja/mcp-fitbit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server