@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
package/build/utils.js
CHANGED
|
@@ -1,60 +1,140 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared utility functions for
|
|
2
|
+
* Shared utility functions for Google Health API integration
|
|
3
3
|
*/
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { HTTP_CONFIG, ERROR_MESSAGES,
|
|
5
|
+
import { HTTP_CONFIG, ERROR_MESSAGES, GOOGLE_HEALTH_API_BASE, DATE_REGEX, CIVIL_TIME_REGEX, CIVIL_TIME_FORMAT_ERROR, RFC3339_REGEX, RFC3339_FORMAT_ERROR, VALIDATION_MESSAGES, DATA_SOURCE_FAMILIES, } from './config.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
* Error thrown when the Google Health API returns a non-success status.
|
|
8
|
+
*/
|
|
9
|
+
export class GoogleHealthApiError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
body;
|
|
12
|
+
constructor(status, body, url) {
|
|
13
|
+
super(`Google Health API Error! Status: ${status}, URL: ${url}, Body: ${body}`);
|
|
14
|
+
this.name = 'GoogleHealthApiError';
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function buildQueryString(query) {
|
|
20
|
+
if (!query)
|
|
21
|
+
return '';
|
|
22
|
+
const parts = Object.entries(query)
|
|
23
|
+
.filter(([, value]) => value !== undefined && value !== '')
|
|
24
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
25
|
+
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
26
|
+
}
|
|
27
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
/**
|
|
29
|
+
* Makes a request to the Google Health API.
|
|
30
|
+
* @param path Path relative to the API base, e.g. 'users/me/dataTypes/steps/dataPoints'
|
|
10
31
|
* @param getAccessTokenFn A function that returns the current valid access token or null.
|
|
11
|
-
* @param
|
|
12
|
-
* @returns
|
|
32
|
+
* @param options Method, query parameters and JSON body.
|
|
33
|
+
* @returns The parsed JSON response, or null when no access token is available.
|
|
34
|
+
* @throws GoogleHealthApiError for non-success responses (after retrying 429/504),
|
|
35
|
+
* or the underlying error for network failures.
|
|
13
36
|
*/
|
|
14
|
-
export async function
|
|
37
|
+
export async function makeGoogleHealthRequest(path, getAccessTokenFn, options = {}) {
|
|
15
38
|
const currentAccessToken = await getAccessTokenFn();
|
|
16
39
|
if (!currentAccessToken) {
|
|
17
40
|
console.error(`Error: ${ERROR_MESSAGES.NO_ACCESS_TOKEN}`);
|
|
18
41
|
return null;
|
|
19
42
|
}
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const url = `${apiBase}/user/-/${cleanEndpoint}`;
|
|
26
|
-
console.error(`Attempting Fitbit API request to: ${url}`);
|
|
43
|
+
const { method = 'GET', query, body } = options;
|
|
44
|
+
const retryBaseDelayMs = options.retryBaseDelayMs ?? HTTP_CONFIG.RETRY_BASE_DELAY_MS;
|
|
45
|
+
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
|
46
|
+
const url = `${GOOGLE_HEALTH_API_BASE}/${cleanPath}${buildQueryString(query)}`;
|
|
47
|
+
console.error(`Attempting Google Health API request: ${method} ${url}`);
|
|
27
48
|
const headers = {
|
|
28
49
|
'User-Agent': HTTP_CONFIG.USER_AGENT,
|
|
29
50
|
Authorization: `Bearer ${currentAccessToken}`,
|
|
30
51
|
Accept: 'application/json',
|
|
31
52
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
if (body !== undefined) {
|
|
54
|
+
headers['Content-Type'] = 'application/json';
|
|
55
|
+
}
|
|
56
|
+
for (let attempt = 0;; attempt++) {
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await fetch(url, {
|
|
60
|
+
method,
|
|
61
|
+
headers,
|
|
62
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`Error making Google Health request to ${url}:`, error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
if (response.ok) {
|
|
70
|
+
// Handle potential empty response body (e.g., 204 No Content)
|
|
71
|
+
if (response.status === 204) {
|
|
72
|
+
return {};
|
|
39
73
|
}
|
|
40
|
-
return
|
|
74
|
+
return (await response.json());
|
|
75
|
+
}
|
|
76
|
+
const errorBody = await response.text();
|
|
77
|
+
const retryable = HTTP_CONFIG.RETRYABLE_STATUSES.includes(response.status);
|
|
78
|
+
if (retryable && attempt < HTTP_CONFIG.MAX_RETRIES) {
|
|
79
|
+
await sleep(retryBaseDelayMs * 2 ** attempt);
|
|
80
|
+
continue;
|
|
41
81
|
}
|
|
42
|
-
|
|
43
|
-
if (response.status ===
|
|
44
|
-
|
|
82
|
+
console.error(`Google Health API Error! Status: ${response.status}, URL: ${url}, Body: ${errorBody}`);
|
|
83
|
+
if (response.status === 401) {
|
|
84
|
+
console.error(ERROR_MESSAGES.TOKEN_EXPIRED);
|
|
45
85
|
}
|
|
46
|
-
|
|
86
|
+
throw new GoogleHealthApiError(response.status, errorBody, url);
|
|
47
87
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
88
|
+
}
|
|
89
|
+
// === Filter / path / range helpers ===
|
|
90
|
+
/** Path for the data points collection of a data type (kebab-case). */
|
|
91
|
+
export function dataPointsPath(dataType, suffix = '') {
|
|
92
|
+
return `users/me/dataTypes/${dataType}/dataPoints${suffix}`;
|
|
93
|
+
}
|
|
94
|
+
/** Filters use snake_case data type names, URLs use kebab-case. */
|
|
95
|
+
export function toFilterName(dataType) {
|
|
96
|
+
return dataType.replace(/-/g, '_');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Builds an AIP-160 style range filter on a field.
|
|
100
|
+
* Start is inclusive; end (optional) is exclusive.
|
|
101
|
+
*/
|
|
102
|
+
export function buildRangeFilter(field, start, end) {
|
|
103
|
+
const clauses = [`${field} >= "${start}"`];
|
|
104
|
+
if (end) {
|
|
105
|
+
clauses.push(`${field} < "${end}"`);
|
|
51
106
|
}
|
|
107
|
+
return clauses.join(' AND ');
|
|
108
|
+
}
|
|
109
|
+
/** Returns the calendar day after a YYYY-MM-DD date. */
|
|
110
|
+
export function nextDate(date) {
|
|
111
|
+
const d = new Date(`${date}T00:00:00Z`);
|
|
112
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
113
|
+
return d.toISOString().substring(0, 10);
|
|
114
|
+
}
|
|
115
|
+
function toCivilDateTime(date) {
|
|
116
|
+
const [year, month, day] = date.split('-').map(Number);
|
|
117
|
+
return { date: { year, month, day }, time: {} };
|
|
118
|
+
}
|
|
119
|
+
/** Body for POST dataPoints:dailyRollUp over a closed-open civil date range. */
|
|
120
|
+
export function buildDailyRollUpBody(startDate, endDate, extra = {}) {
|
|
121
|
+
return {
|
|
122
|
+
range: {
|
|
123
|
+
start: toCivilDateTime(startDate),
|
|
124
|
+
end: toCivilDateTime(endDate),
|
|
125
|
+
},
|
|
126
|
+
windowSizeDays: extra.windowSizeDays ?? 1,
|
|
127
|
+
...(extra.dataSourceFamily
|
|
128
|
+
? { dataSourceFamily: extra.dataSourceFamily }
|
|
129
|
+
: {}),
|
|
130
|
+
};
|
|
52
131
|
}
|
|
53
132
|
// === Parameter Validation Schemas ===
|
|
54
133
|
export const CommonSchemas = {
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
.
|
|
134
|
+
date: z
|
|
135
|
+
.string()
|
|
136
|
+
.regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT)
|
|
137
|
+
.describe('The date for which to retrieve data (YYYY-MM-DD)'),
|
|
58
138
|
startDate: z
|
|
59
139
|
.string()
|
|
60
140
|
.regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT)
|
|
@@ -63,23 +143,39 @@ export const CommonSchemas = {
|
|
|
63
143
|
.string()
|
|
64
144
|
.regex(DATE_REGEX, VALIDATION_MESSAGES.DATE_FORMAT)
|
|
65
145
|
.describe(VALIDATION_MESSAGES.END_DATE_REQUIRED),
|
|
66
|
-
|
|
67
|
-
.enum(HEART_RATE_DETAIL_LEVELS)
|
|
68
|
-
.describe(VALIDATION_MESSAGES.DETAIL_LEVEL_REQUIRED),
|
|
69
|
-
date: z
|
|
146
|
+
civilStartTime: z
|
|
70
147
|
.string()
|
|
71
|
-
.regex(
|
|
72
|
-
.describe(
|
|
73
|
-
|
|
148
|
+
.regex(CIVIL_TIME_REGEX, CIVIL_TIME_FORMAT_ERROR)
|
|
149
|
+
.describe("Start of the range, inclusive, as civil time ('YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS', user's local time)"),
|
|
150
|
+
civilEndTime: z
|
|
74
151
|
.string()
|
|
75
|
-
.regex(
|
|
76
|
-
.
|
|
77
|
-
|
|
152
|
+
.regex(CIVIL_TIME_REGEX, CIVIL_TIME_FORMAT_ERROR)
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Optional end of the range, EXCLUSIVE, as civil time ('YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS')"),
|
|
155
|
+
physicalStartTime: z
|
|
156
|
+
.string()
|
|
157
|
+
.regex(RFC3339_REGEX, RFC3339_FORMAT_ERROR)
|
|
158
|
+
.describe("Start of the range, inclusive, as an RFC 3339 timestamp (e.g. '2026-03-04T00:00:00Z')"),
|
|
159
|
+
physicalEndTime: z
|
|
160
|
+
.string()
|
|
161
|
+
.regex(RFC3339_REGEX, RFC3339_FORMAT_ERROR)
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Optional end of the range, EXCLUSIVE, as an RFC 3339 timestamp (e.g. '2026-03-05T00:00:00Z')"),
|
|
164
|
+
pageSize: z
|
|
78
165
|
.number()
|
|
166
|
+
.int()
|
|
79
167
|
.min(1)
|
|
80
|
-
.max(
|
|
168
|
+
.max(10000)
|
|
169
|
+
.optional()
|
|
170
|
+
.describe('Maximum number of data points to return. Most data types allow up to 10000; sleep and exercise are capped at 25'),
|
|
171
|
+
pageToken: z
|
|
172
|
+
.string()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe(`Page token for pagination. ${VALIDATION_MESSAGES.PAGINATION}`),
|
|
175
|
+
dataSourceFamily: z
|
|
176
|
+
.enum(DATA_SOURCE_FAMILIES)
|
|
81
177
|
.optional()
|
|
82
|
-
.describe('
|
|
178
|
+
.describe('Optional data source family. Defaults to all sources (Google/Fitbit wearables plus third-party)'),
|
|
83
179
|
};
|
|
84
180
|
// === Tool Response Helpers ===
|
|
85
181
|
export function createErrorResponse(message) {
|
|
@@ -96,16 +192,30 @@ export function createSuccessResponse(data) {
|
|
|
96
192
|
}
|
|
97
193
|
export function createNoDataResponse(context) {
|
|
98
194
|
return {
|
|
99
|
-
content: [
|
|
195
|
+
content: [
|
|
196
|
+
{ type: 'text', text: `${ERROR_MESSAGES.NO_DATA_FOUND} ${context}.` },
|
|
197
|
+
],
|
|
100
198
|
};
|
|
101
199
|
}
|
|
102
200
|
export function registerTool(server, config) {
|
|
103
201
|
server.tool(config.name, config.description, config.parametersSchema, config.handler);
|
|
104
202
|
}
|
|
105
|
-
// ===
|
|
106
|
-
export async function
|
|
107
|
-
const {
|
|
108
|
-
|
|
203
|
+
// === Google Health-Specific Tool Helpers ===
|
|
204
|
+
export async function handleGoogleHealthCall(path, getAccessTokenFn, options = {}) {
|
|
205
|
+
const { successDataExtractor, noDataMessage, errorContext = path, ...requestOptions } = options;
|
|
206
|
+
let responseData;
|
|
207
|
+
try {
|
|
208
|
+
responseData = await makeGoogleHealthRequest(path, getAccessTokenFn, requestOptions);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error instanceof GoogleHealthApiError) {
|
|
212
|
+
if (error.status === 400 && error.body.includes('ACCOUNT_NOT_LINKED')) {
|
|
213
|
+
return createErrorResponse(ERROR_MESSAGES.ACCOUNT_NOT_LINKED);
|
|
214
|
+
}
|
|
215
|
+
return createErrorResponse(`${ERROR_MESSAGES.API_REQUEST_FAILED} for ${errorContext} (HTTP ${error.status}): ${error.body}`);
|
|
216
|
+
}
|
|
217
|
+
return createErrorResponse(`${ERROR_MESSAGES.API_REQUEST_FAILED} for ${errorContext}: ${error instanceof Error ? error.message : String(error)}`);
|
|
218
|
+
}
|
|
109
219
|
if (!responseData) {
|
|
110
220
|
return createErrorResponse(`${ERROR_MESSAGES.API_REQUEST_FAILED} for ${errorContext}. ${ERROR_MESSAGES.CHECK_TOKEN_PERMISSIONS}.`);
|
|
111
221
|
}
|
|
@@ -118,3 +228,36 @@ export async function handleFitbitApiCall(endpoint, params, getAccessTokenFn, op
|
|
|
118
228
|
}
|
|
119
229
|
return createSuccessResponse(responseData);
|
|
120
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Lists data points of a data type with a filter (GET .../dataPoints).
|
|
233
|
+
*/
|
|
234
|
+
export function listDataPoints(dataType, getAccessTokenFn, params, errorContext, suffix = '') {
|
|
235
|
+
return handleGoogleHealthCall(dataPointsPath(dataType, suffix), getAccessTokenFn, {
|
|
236
|
+
query: {
|
|
237
|
+
filter: params.filter,
|
|
238
|
+
pageSize: params.pageSize,
|
|
239
|
+
pageToken: params.pageToken,
|
|
240
|
+
},
|
|
241
|
+
successDataExtractor: (data) => data.dataPoints,
|
|
242
|
+
errorContext,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Rolls up data points of a data type per civil day (POST .../dataPoints:dailyRollUp).
|
|
247
|
+
* endDate is exclusive (closed-open range), as in the Google Health API.
|
|
248
|
+
*/
|
|
249
|
+
export function dailyRollUp(dataType, getAccessTokenFn, params, errorContext) {
|
|
250
|
+
const body = {
|
|
251
|
+
...buildDailyRollUpBody(params.startDate, params.endDate, {
|
|
252
|
+
dataSourceFamily: params.dataSourceFamily,
|
|
253
|
+
}),
|
|
254
|
+
...(params.pageSize ? { pageSize: params.pageSize } : {}),
|
|
255
|
+
...(params.pageToken ? { pageToken: params.pageToken } : {}),
|
|
256
|
+
};
|
|
257
|
+
return handleGoogleHealthCall(dataPointsPath(dataType, ':dailyRollUp'), getAccessTokenFn, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
body,
|
|
260
|
+
successDataExtractor: (data) => data.rollupDataPoints,
|
|
261
|
+
errorContext,
|
|
262
|
+
});
|
|
263
|
+
}
|
package/build/weight.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import { registerTool, CommonSchemas,
|
|
2
|
-
// --- Tool Registration ---
|
|
1
|
+
import { registerTool, CommonSchemas, listDataPoints, buildRangeFilter, } from './utils.js';
|
|
3
2
|
/**
|
|
4
|
-
* Registers
|
|
3
|
+
* Registers the weight tool with the MCP server (Google Health `weight` data type).
|
|
5
4
|
* @param server The McpServer instance.
|
|
6
5
|
* @param getAccessTokenFn Function to retrieve the current access token.
|
|
7
6
|
*/
|
|
8
7
|
export function registerWeightTool(server, getAccessTokenFn) {
|
|
9
8
|
registerTool(server, {
|
|
10
9
|
name: 'get_weight',
|
|
11
|
-
description:
|
|
10
|
+
description: 'Get the raw JSON response for weight data points from the Google Health API (data type "weight"), filtered by sample time. Requires a start timestamp (RFC 3339); the end is optional and exclusive. Supports pagination via pageToken.',
|
|
12
11
|
parametersSchema: {
|
|
13
|
-
|
|
12
|
+
startTime: CommonSchemas.physicalStartTime,
|
|
13
|
+
endTime: CommonSchemas.physicalEndTime,
|
|
14
|
+
pageSize: CommonSchemas.pageSize,
|
|
15
|
+
pageToken: CommonSchemas.pageToken,
|
|
14
16
|
},
|
|
15
|
-
handler: async ({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
errorContext: `period '${period}'`
|
|
21
|
-
});
|
|
22
|
-
}
|
|
17
|
+
handler: async ({ startTime, endTime, pageSize, pageToken, }) => listDataPoints('weight', getAccessTokenFn, {
|
|
18
|
+
filter: buildRangeFilter('weight.sample_time.physical_time', startTime, endTime),
|
|
19
|
+
pageSize,
|
|
20
|
+
pageToken,
|
|
21
|
+
}, `weight from '${startTime}'${endTime ? ` to '${endTime}'` : ''}`),
|
|
23
22
|
});
|
|
24
23
|
}
|
package/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@einlogic/mcp-fitbit",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"type": "module",
|
|
5
|
-
"main": "./build/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"mcp-fitbit": "./build/index.js",
|
|
8
|
-
"mkoziel2000-mcp-fitbit": "./build/index.js"
|
|
9
|
-
},
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "https://github.com/mkoziel2000/mcp-fitbit.git"
|
|
13
|
-
},
|
|
14
|
-
"homepage": "https://github.com/mkoziel2000/mcp-fitbit",
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/mkoziel2000/mcp-fitbit/issues"
|
|
17
|
-
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"test": "vitest run",
|
|
20
|
-
"test:ui": "vitest --ui",
|
|
21
|
-
"test:coverage": "vitest run --coverage",
|
|
22
|
-
"build": "tsc",
|
|
23
|
-
"start": "node build/index.js",
|
|
24
|
-
"dev": "npm run build && npx @modelcontextprotocol/inspector node build/index.js",
|
|
25
|
-
"lint": "eslint src/**/*.ts --ignore-pattern src/**/*.test.ts",
|
|
26
|
-
"lint:fix": "eslint src/**/*.ts --fix",
|
|
27
|
-
"format": "prettier --write src/**/*.ts",
|
|
28
|
-
"format:check": "prettier --check src/**/*.ts",
|
|
29
|
-
"prepublishOnly": "npm run build && npm run test && npm run lint",
|
|
30
|
-
"prepack": "npm run build"
|
|
31
|
-
},
|
|
32
|
-
"keywords": [
|
|
33
|
-
"mcp",
|
|
34
|
-
"fitbit",
|
|
35
|
-
"
|
|
36
|
-
"mcp-server",
|
|
37
|
-
"claude",
|
|
38
|
-
"model-context-protocol",
|
|
39
|
-
"health-data",
|
|
40
|
-
"fitness-tracking",
|
|
41
|
-
"ai-assistant",
|
|
42
|
-
"typescript"
|
|
43
|
-
],
|
|
44
|
-
"author": "mkoziel2000",
|
|
45
|
-
"license": "MIT",
|
|
46
|
-
"description": "
|
|
47
|
-
"files": [
|
|
48
|
-
"build/",
|
|
49
|
-
"README.md",
|
|
50
|
-
"LICENSE"
|
|
51
|
-
],
|
|
52
|
-
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
54
|
-
"@types/express": "^5.0.1",
|
|
55
|
-
"dotenv": "^16.5.0",
|
|
56
|
-
"express": "^5.1.0",
|
|
57
|
-
"open": "^10.1.2",
|
|
58
|
-
"simple-oauth2": "^5.1.0",
|
|
59
|
-
"zod": "^3.24.3"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
62
|
-
"@eslint/js": "^9.27.0",
|
|
63
|
-
"@types/node": "^22.15.3",
|
|
64
|
-
"@types/simple-oauth2": "^5.0.7",
|
|
65
|
-
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
66
|
-
"@typescript-eslint/parser": "^8.32.1",
|
|
67
|
-
"@vitest/coverage-v8": "^3.1.4",
|
|
68
|
-
"@vitest/ui": "^3.1.4",
|
|
69
|
-
"eslint": "^9.27.0",
|
|
70
|
-
"prettier": "^3.5.3",
|
|
71
|
-
"typescript": "^5.8.3",
|
|
72
|
-
"vitest": "^3.1.4"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@einlogic/mcp-fitbit",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-fitbit": "./build/index.js",
|
|
8
|
+
"mkoziel2000-mcp-fitbit": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/mkoziel2000/mcp-fitbit.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/mkoziel2000/mcp-fitbit",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/mkoziel2000/mcp-fitbit/issues"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:ui": "vitest --ui",
|
|
21
|
+
"test:coverage": "vitest run --coverage",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"start": "node build/index.js",
|
|
24
|
+
"dev": "npm run build && npx @modelcontextprotocol/inspector node build/index.js",
|
|
25
|
+
"lint": "eslint src/**/*.ts --ignore-pattern src/**/*.test.ts",
|
|
26
|
+
"lint:fix": "eslint src/**/*.ts --fix",
|
|
27
|
+
"format": "prettier --write src/**/*.ts",
|
|
28
|
+
"format:check": "prettier --check src/**/*.ts",
|
|
29
|
+
"prepublishOnly": "npm run build && npm run test && npm run lint",
|
|
30
|
+
"prepack": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"fitbit",
|
|
35
|
+
"google-health-api",
|
|
36
|
+
"mcp-server",
|
|
37
|
+
"claude",
|
|
38
|
+
"model-context-protocol",
|
|
39
|
+
"health-data",
|
|
40
|
+
"fitness-tracking",
|
|
41
|
+
"ai-assistant",
|
|
42
|
+
"typescript"
|
|
43
|
+
],
|
|
44
|
+
"author": "mkoziel2000",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"description": "MCP server for accessing Fitbit / Pixel Watch health and fitness data through the Google Health API. Connect AI assistants like Claude to your data for personalized health insights.",
|
|
47
|
+
"files": [
|
|
48
|
+
"build/",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
54
|
+
"@types/express": "^5.0.1",
|
|
55
|
+
"dotenv": "^16.5.0",
|
|
56
|
+
"express": "^5.1.0",
|
|
57
|
+
"open": "^10.1.2",
|
|
58
|
+
"simple-oauth2": "^5.1.0",
|
|
59
|
+
"zod": "^3.24.3"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^9.27.0",
|
|
63
|
+
"@types/node": "^22.15.3",
|
|
64
|
+
"@types/simple-oauth2": "^5.0.7",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
|
66
|
+
"@typescript-eslint/parser": "^8.32.1",
|
|
67
|
+
"@vitest/coverage-v8": "^3.1.4",
|
|
68
|
+
"@vitest/ui": "^3.1.4",
|
|
69
|
+
"eslint": "^9.27.0",
|
|
70
|
+
"prettier": "^3.5.3",
|
|
71
|
+
"typescript": "^5.8.3",
|
|
72
|
+
"vitest": "^3.1.4"
|
|
73
|
+
}
|
|
74
|
+
}
|