#!/usr/bin/env node
/**
* Garmin Health MCP Server
* Provides Garmin Connect health data access to Claude Desktop via MCP
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Path to Python scripts (bundled with this server)
const SCRIPTS_DIR = join(__dirname, 'scripts');
/**
* Execute a Python script and return JSON result
*/
function runPythonScript(scriptName, args = []) {
try {
const scriptPath = join(SCRIPTS_DIR, scriptName);
// Check if script exists
if (!fs.existsSync(scriptPath)) {
throw new Error(`Python script not found: ${scriptPath}`);
}
// Build command with environment variables
const env = {
...process.env,
GARMIN_EMAIL: process.env.GARMIN_EMAIL,
GARMIN_PASSWORD: process.env.GARMIN_PASSWORD,
};
const cmd = `python3 "${scriptPath}" ${args.join(' ')}`;
const result = execSync(cmd, {
encoding: 'utf-8',
env,
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
});
// Try to parse as JSON
try {
return JSON.parse(result);
} catch (e) {
// Return raw output if not JSON
return { output: result.trim() };
}
} catch (error) {
throw new Error(`Python script failed: ${error.message}`);
}
}
/**
* MCP Server
*/
const server = new Server(
{
name: 'garmin-health',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
/**
* List available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_sleep_data',
description: 'Get sleep data including hours, stages (light/deep/REM), and quality scores',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to retrieve (default: 7)',
default: 7,
},
start_date: {
type: 'string',
description: 'Start date in YYYY-MM-DD format (optional)',
},
end_date: {
type: 'string',
description: 'End date in YYYY-MM-DD format (optional)',
},
},
},
},
{
name: 'get_body_battery',
description: "Get Body Battery data (Garmin's recovery metric, 0-100)",
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to retrieve (default: 7)',
default: 7,
},
},
},
},
{
name: 'get_hrv_data',
description: 'Get Heart Rate Variability (HRV) data in milliseconds',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to retrieve (default: 30)',
default: 30,
},
},
},
},
{
name: 'get_heart_rate',
description: 'Get resting, max, and min heart rate data',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to retrieve (default: 7)',
default: 7,
},
},
},
},
{
name: 'get_activities',
description: 'Get workout/activity data including type, duration, calories, and distance',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to retrieve (default: 30)',
default: 30,
},
},
},
},
{
name: 'get_stress_levels',
description: 'Get all-day stress level data',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to retrieve (default: 7)',
default: 7,
},
},
},
},
{
name: 'get_summary',
description: 'Get combined health summary with averages across all metrics',
inputSchema: {
type: 'object',
properties: {
days: {
type: 'number',
description: 'Number of days to retrieve (default: 7)',
default: 7,
},
},
},
},
{
name: 'get_user_profile',
description: 'Get user profile information and connected devices',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'generate_chart',
description: 'Generate an interactive HTML chart (sleep, body_battery, hrv, activities, or dashboard)',
inputSchema: {
type: 'object',
properties: {
chart_type: {
type: 'string',
enum: ['sleep', 'body_battery', 'hrv', 'activities', 'dashboard'],
description: 'Type of chart to generate',
},
days: {
type: 'number',
description: 'Number of days to include (default: 30)',
default: 30,
},
output_path: {
type: 'string',
description: 'Path to save HTML file (optional, defaults to temp file)',
},
},
required: ['chart_type'],
},
},
],
};
});
/**
* Handle tool calls
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result;
switch (name) {
case 'get_sleep_data': {
const cmdArgs = ['sleep', '--days', args.days || 7];
if (args.start_date) cmdArgs.push('--start', args.start_date);
if (args.end_date) cmdArgs.push('--end', args.end_date);
result = runPythonScript('garmin_data.py', cmdArgs);
break;
}
case 'get_body_battery': {
result = runPythonScript('garmin_data.py', [
'body_battery',
'--days',
args.days || 7,
]);
break;
}
case 'get_hrv_data': {
result = runPythonScript('garmin_data.py', [
'hrv',
'--days',
args.days || 30,
]);
break;
}
case 'get_heart_rate': {
result = runPythonScript('garmin_data.py', [
'heart_rate',
'--days',
args.days || 7,
]);
break;
}
case 'get_activities': {
result = runPythonScript('garmin_data.py', [
'activities',
'--days',
args.days || 30,
]);
break;
}
case 'get_stress_levels': {
result = runPythonScript('garmin_data.py', [
'stress',
'--days',
args.days || 7,
]);
break;
}
case 'get_summary': {
result = runPythonScript('garmin_data.py', [
'summary',
'--days',
args.days || 7,
]);
break;
}
case 'get_user_profile': {
result = runPythonScript('garmin_data.py', ['profile']);
break;
}
case 'generate_chart': {
const cmdArgs = [args.chart_type, '--days', args.days || 30];
if (args.output_path) {
cmdArgs.push('--output', args.output_path);
}
result = runPythonScript('garmin_chart.py', cmdArgs);
break;
}
default:
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
/**
* Start the server
*/
async function main() {
// Check environment
if (!process.env.GARMIN_EMAIL || !process.env.GARMIN_PASSWORD) {
console.error('Warning: GARMIN_EMAIL and GARMIN_PASSWORD environment variables not set');
console.error('Set these in your Claude Desktop config or .env file');
}
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Garmin Health MCP server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});