Skip to main content
Glama

Withings MCP Client

by gchallen
index.js12 kB
#!/usr/bin/env node 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 { WithingsClient } from './withingsClient.js'; import * as dotenv from 'dotenv'; dotenv.config(); const server = new Server({ name: 'withings-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, }); let withingsClient = null; async function initializeClient() { const config = { clientId: process.env.WITHINGS_CLIENT_ID || '', clientSecret: process.env.WITHINGS_CLIENT_SECRET || '', redirectUri: process.env.WITHINGS_REDIRECT_URI || 'http://localhost:3000/callback', }; if (!config.clientId || !config.clientSecret) { throw new Error('Missing Withings credentials. Please set WITHINGS_CLIENT_ID and WITHINGS_CLIENT_SECRET environment variables.'); } const client = new WithingsClient(config); await client.loadTokens(); return client; } server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = [ { name: 'withings_get_weight', description: 'Get the latest weight measurement from Withings scale', inputSchema: { type: 'object', properties: { userAttrib: { type: 'number', description: 'User attribution (0=device owner, 1+=other users). If not specified, returns data from all users.', }, unitSystem: { type: 'string', enum: ['metric', 'imperial'], description: 'Unit system for measurements (metric=kg, imperial=lb). If not specified, uses WITHINGS_UNIT_SYSTEM environment variable or defaults to metric.', }, }, }, }, { name: 'withings_get_body_composition', description: 'Get complete body composition data including weight, fat mass, muscle mass, bone mass, hydration, and more', inputSchema: { type: 'object', properties: { userAttrib: { type: 'number', description: 'User attribution (0=device owner, 1+=other users). If not specified, returns data from all users.', }, unitSystem: { type: 'string', enum: ['metric', 'imperial'], description: 'Unit system for measurements (metric=kg, imperial=lb). If not specified, uses WITHINGS_UNIT_SYSTEM environment variable or defaults to metric.', }, }, }, }, { name: 'withings_get_measurements', description: 'Get raw measurement data with optional filters and pagination', inputSchema: { type: 'object', properties: { measureTypes: { type: 'array', items: { type: 'number' }, description: 'Measurement types to retrieve (1=Weight, 5=Fat Mass, 6=Muscle Mass, 8=Bone Mass, etc.)', }, startDate: { type: 'string', description: 'Start date in ISO format', }, endDate: { type: 'string', description: 'End date in ISO format', }, userAttrib: { type: 'number', description: 'User attribution (0=device owner, 1+=other users). If not specified, returns data from all users.', }, limit: { type: 'number', description: 'Maximum number of measurement groups to return (e.g., 10, 50). Useful for pagination.', }, offset: { type: 'number', description: 'Number of measurement groups to skip. Use with limit for pagination (e.g., offset=20, limit=10 gets results 21-30).', }, }, }, }, { name: 'withings_get_users', description: 'Get list of users who have measurements on the Withings scale', inputSchema: { type: 'object', properties: {}, }, }, { name: 'withings_get_user_settings', description: 'Get user settings including timezone, unit preferences, and client configuration', inputSchema: { type: 'object', properties: {}, }, }, ]; return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!withingsClient) { try { withingsClient = await initializeClient(); } catch (error) { return { content: [ { type: 'text', text: `Error initializing Withings client: ${error.message}`, }, ], }; } } try { switch (name) { case 'withings_get_weight': { const { userAttrib, unitSystem } = args; const effectiveUserAttrib = userAttrib ?? withingsClient.getDefaultUserAttrib(); const weight = await withingsClient.getLatestWeight(userAttrib, unitSystem); if (weight === null) { const userText = effectiveUserAttrib !== undefined ? ` for user ${effectiveUserAttrib}` : ''; return { content: [ { type: 'text', text: `No weight measurements found${userText}.`, }, ], }; } const userText = effectiveUserAttrib !== undefined ? ` (user ${effectiveUserAttrib})` : ''; return { content: [ { type: 'text', text: `Latest weight: ${weight.value.toFixed(2)} ${weight.unit}${userText}\nMeasured: ${weight.date}\nTimestamp: ${weight.timestamp}`, }, ], }; } case 'withings_get_body_composition': { const { userAttrib, unitSystem } = args; const effectiveUserAttrib = userAttrib ?? withingsClient.getDefaultUserAttrib(); const composition = await withingsClient.getBodyComposition(userAttrib, unitSystem); const formatted = JSON.stringify(composition, null, 2); const userText = effectiveUserAttrib !== undefined ? ` (user ${effectiveUserAttrib})` : ''; return { content: [ { type: 'text', text: `Body Composition${userText}:\n${formatted}`, }, ], }; } case 'withings_get_measurements': { const { measureTypes, startDate, endDate, userAttrib, limit, offset } = args; const startTimestamp = startDate ? Math.floor(new Date(startDate).getTime() / 1000) : undefined; const endTimestamp = endDate ? Math.floor(new Date(endDate).getTime() / 1000) : undefined; const effectiveUserAttrib = userAttrib ?? withingsClient.getDefaultUserAttrib(); const result = await withingsClient.getMeasuresWithPagination(measureTypes, startTimestamp, endTimestamp, effectiveUserAttrib, limit, offset); // Format response with pagination info const response = { measurements: result.measures, pagination: result.pagination }; const paginationText = result.pagination.more ? `\n\n📄 Pagination: Showing ${result.pagination.total_returned} results (offset: ${result.pagination.offset || 0}, limit: ${result.pagination.limit || 'none'}). More data available - use offset=${(result.pagination.offset || 0) + result.pagination.total_returned} for next page.` : `\n\n📄 Pagination: Showing ${result.pagination.total_returned} results (offset: ${result.pagination.offset || 0}, limit: ${result.pagination.limit || 'none'}). No more data available.`; return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) + paginationText, }, ], }; } case 'withings_get_users': { const users = await withingsClient.getAvailableUsers(); const formatted = users.map(user => { const userType = user.attrib === 0 ? 'Device Owner' : user.attrib === 2 ? 'Manual Entry' : user.attrib === 4 ? 'Auto Detection' : `User ${user.attrib}`; return `• User ${user.attrib} (${userType}): ${user.count} measurements, latest: ${user.latestDate}`; }).join('\n'); return { content: [ { type: 'text', text: `Available Users:\n${formatted}`, }, ], }; } case 'withings_get_user_settings': { const settings = await withingsClient.getUserSettings(); const formatted = [ `Timezone: ${settings.timezone}`, `Unit System: ${settings.unit_system}`, `Default User: ${settings.default_user_attrib !== undefined ? settings.default_user_attrib : 'All users'}`, `Client ID: ${settings.client_id}`, `Redirect URI: ${settings.redirect_uri}`, `Access Token: ${settings.has_access_token ? 'Present' : 'Missing'}`, `Refresh Token: ${settings.has_refresh_token ? 'Present' : 'Missing'}`, ].join('\n'); return { content: [ { type: 'text', text: `User Settings:\n${formatted}`, }, ], }; } default: return { content: [ { type: 'text', text: `Unknown tool: ${name}`, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], }; } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Withings MCP server running on stdio'); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); }); //# sourceMappingURL=index.js.map

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/gchallen/withings-mcp'

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