├── .gitignore ├── .claude └── settings.local.json ├── tsconfig.json ├── package.json ├── utils ├── test-date-conversion.js ├── test-mcp.js └── test-interactive.js ├── LICENSE ├── README.md └── src ├── cli.ts └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | dist/ 3 | node_modules 4 | -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(find:*)", 5 | "Bash(fd:*)", 6 | "Bash(ls:*)", 7 | "Bash(rg:*)", 8 | "Bash(node:*)", 9 | "Bash(npm run build:*)", 10 | "WebFetch(domain:raw.githubusercontent.com)", 11 | "WebFetch(domain:platform.fatsecret.com)" 12 | ], 13 | "deny": [] 14 | } 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "outDir": "./dist", 12 | "rootDir": "./src", 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "resolveJsonModule": true 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "dist" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fatsecret-mcp-server", 3 | "version": "0.1.0", 4 | "description": "Model Context Protocol server for FatSecret API with 3-Legged OAuth support", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js", 10 | "dev": "tsc && node dist/index.js", 11 | "clean": "rm -rf dist" 12 | }, 13 | "keywords": [ 14 | "mcp", 15 | "fatsecret", 16 | "nutrition", 17 | "api", 18 | "oauth" 19 | ], 20 | "author": "Your Name", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@modelcontextprotocol/sdk": "^0.4.0", 24 | "@types/node": "^20.0.0", 25 | "dotenv": "^17.0.1", 26 | "node-fetch": "^3.3.2" 27 | }, 28 | "devDependencies": { 29 | "typescript": "^5.8.3" 30 | }, 31 | "bin": { 32 | "fatsecret-mcp-server": "./dist/index.js" 33 | }, 34 | "files": [ 35 | "dist/" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /utils/test-date-conversion.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Test the date conversion function 5 | */ 6 | 7 | function dateToFatSecretFormat(dateString) { 8 | const date = dateString ? new Date(dateString) : new Date(); 9 | const epochStart = new Date('1970-01-01'); 10 | const daysSinceEpoch = Math.floor((date.getTime() - epochStart.getTime()) / (1000 * 60 * 60 * 24)); 11 | return daysSinceEpoch.toString(); 12 | } 13 | 14 | console.log('Testing date conversion to FatSecret format (days since epoch):\n'); 15 | 16 | // Test cases 17 | const testDates = [ 18 | '2025-07-07', 19 | '2024-01-01', 20 | '1970-01-01', 21 | '2023-12-31', 22 | null // Today's date 23 | ]; 24 | 25 | testDates.forEach(date => { 26 | const result = dateToFatSecretFormat(date); 27 | console.log(`${date || 'Today'} => ${result} days since epoch`); 28 | }); 29 | 30 | // Calculate what today is 31 | const today = new Date(); 32 | console.log(`\nToday's date: ${today.toISOString().split('T')[0]}`); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2025 Felipe Coury 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the “Software”), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /utils/test-mcp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Test script for FatSecret MCP Server 5 | * 6 | * This script simulates the JSON-RPC messages that Claude would send to test the MCP server. 7 | * Usage: node test-mcp.js | node dist/index.js 8 | */ 9 | 10 | // Test 1: Initialize connection 11 | console.log(JSON.stringify({ 12 | jsonrpc: "2.0", 13 | method: "initialize", 14 | params: { 15 | protocolVersion: "0.1.0", 16 | capabilities: {} 17 | }, 18 | id: 1 19 | })); 20 | 21 | // Test 2: List available tools 22 | console.log(JSON.stringify({ 23 | jsonrpc: "2.0", 24 | method: "tools/list", 25 | params: {}, 26 | id: 2 27 | })); 28 | 29 | // Test 3: Check authentication status 30 | console.log(JSON.stringify({ 31 | jsonrpc: "2.0", 32 | method: "tools/call", 33 | params: { 34 | name: "check_auth_status", 35 | arguments: {} 36 | }, 37 | id: 3 38 | })); 39 | 40 | // Test 4: Search for foods (doesn't require auth) 41 | console.log(JSON.stringify({ 42 | jsonrpc: "2.0", 43 | method: "tools/call", 44 | params: { 45 | name: "search_foods", 46 | arguments: { 47 | searchExpression: "apple", 48 | maxResults: 5 49 | } 50 | }, 51 | id: 4 52 | })); 53 | 54 | // Test 5: Get user food entries for today (requires auth) 55 | console.log(JSON.stringify({ 56 | jsonrpc: "2.0", 57 | method: "tools/call", 58 | params: { 59 | name: "get_user_food_entries", 60 | arguments: { 61 | date: "2025-07-07" 62 | } 63 | }, 64 | id: 5 65 | })); 66 | 67 | // Test 6: Get user food entries without date (should use today) 68 | console.log(JSON.stringify({ 69 | jsonrpc: "2.0", 70 | method: "tools/call", 71 | params: { 72 | name: "get_user_food_entries", 73 | arguments: {} 74 | }, 75 | id: 6 76 | })); -------------------------------------------------------------------------------- /utils/test-interactive.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Interactive test script for FatSecret MCP Server 5 | * 6 | * This provides an interactive way to test the MCP server 7 | * Usage: node test-interactive.js 8 | */ 9 | 10 | const { spawn } = require('child_process'); 11 | const readline = require('readline'); 12 | 13 | // Start the MCP server 14 | const server = spawn('node', ['dist/index.js'], { 15 | stdio: ['pipe', 'pipe', 'inherit'] 16 | }); 17 | 18 | let messageId = 1; 19 | 20 | // Create readline interface for user input 21 | const rl = readline.createInterface({ 22 | input: process.stdin, 23 | output: process.stdout 24 | }); 25 | 26 | // Handle server output 27 | server.stdout.on('data', (data) => { 28 | try { 29 | const response = JSON.parse(data.toString()); 30 | console.log('\n📥 Response:', JSON.stringify(response, null, 2)); 31 | } catch (e) { 32 | console.log('\n📥 Raw output:', data.toString()); 33 | } 34 | }); 35 | 36 | // Send a JSON-RPC message to the server 37 | function sendMessage(method, params = {}) { 38 | const message = { 39 | jsonrpc: "2.0", 40 | method: method, 41 | params: params, 42 | id: messageId++ 43 | }; 44 | 45 | console.log('\n📤 Sending:', JSON.stringify(message, null, 2)); 46 | server.stdin.write(JSON.stringify(message) + '\n'); 47 | } 48 | 49 | // Tool call helper 50 | function callTool(name, args = {}) { 51 | sendMessage('tools/call', { 52 | name: name, 53 | arguments: args 54 | }); 55 | } 56 | 57 | // Menu 58 | function showMenu() { 59 | console.log('\n=== FatSecret MCP Server Test Menu ==='); 60 | console.log('1. Initialize connection'); 61 | console.log('2. List available tools'); 62 | console.log('3. Check auth status'); 63 | console.log('4. Search foods'); 64 | console.log('5. Get food details'); 65 | console.log('6. Get user food entries (requires auth)'); 66 | console.log('7. Add food entry (requires auth)'); 67 | console.log('8. Custom tool call'); 68 | console.log('0. Exit'); 69 | console.log('=====================================\n'); 70 | 71 | rl.question('Choose an option: ', handleMenuChoice); 72 | } 73 | 74 | function handleMenuChoice(choice) { 75 | switch(choice) { 76 | case '1': 77 | sendMessage('initialize', { 78 | protocolVersion: "0.1.0", 79 | capabilities: {} 80 | }); 81 | setTimeout(showMenu, 1000); 82 | break; 83 | 84 | case '2': 85 | sendMessage('tools/list'); 86 | setTimeout(showMenu, 1000); 87 | break; 88 | 89 | case '3': 90 | callTool('check_auth_status'); 91 | setTimeout(showMenu, 1000); 92 | break; 93 | 94 | case '4': 95 | rl.question('Search term: ', (term) => { 96 | callTool('search_foods', { 97 | searchExpression: term, 98 | maxResults: 5 99 | }); 100 | setTimeout(showMenu, 1000); 101 | }); 102 | break; 103 | 104 | case '5': 105 | rl.question('Food ID: ', (id) => { 106 | callTool('get_food', { 107 | foodId: id 108 | }); 109 | setTimeout(showMenu, 1000); 110 | }); 111 | break; 112 | 113 | case '6': 114 | rl.question('Date (YYYY-MM-DD, or press Enter for today): ', (date) => { 115 | const args = date ? { date } : {}; 116 | callTool('get_user_food_entries', args); 117 | setTimeout(showMenu, 1000); 118 | }); 119 | break; 120 | 121 | case '7': 122 | console.log('Add food entry:'); 123 | rl.question('Food ID: ', (foodId) => { 124 | rl.question('Serving ID: ', (servingId) => { 125 | rl.question('Quantity: ', (quantity) => { 126 | rl.question('Meal type (breakfast/lunch/dinner/snack): ', (meal) => { 127 | rl.question('Date (YYYY-MM-DD, or press Enter for today): ', (date) => { 128 | const args = { 129 | foodId, 130 | servingId, 131 | quantity: parseFloat(quantity), 132 | mealType: meal 133 | }; 134 | if (date) args.date = date; 135 | callTool('add_food_entry', args); 136 | setTimeout(showMenu, 1000); 137 | }); 138 | }); 139 | }); 140 | }); 141 | }); 142 | break; 143 | 144 | case '8': 145 | rl.question('Tool name: ', (name) => { 146 | rl.question('Arguments (JSON): ', (argsStr) => { 147 | try { 148 | const args = argsStr ? JSON.parse(argsStr) : {}; 149 | callTool(name, args); 150 | } catch (e) { 151 | console.error('Invalid JSON:', e.message); 152 | } 153 | setTimeout(showMenu, 1000); 154 | }); 155 | }); 156 | break; 157 | 158 | case '0': 159 | console.log('Exiting...'); 160 | server.kill(); 161 | process.exit(0); 162 | break; 163 | 164 | default: 165 | console.log('Invalid option'); 166 | showMenu(); 167 | } 168 | } 169 | 170 | // Start by initializing 171 | console.log('Starting FatSecret MCP Server test...\n'); 172 | sendMessage('initialize', { 173 | protocolVersion: "0.1.0", 174 | capabilities: {} 175 | }); 176 | 177 | // Show menu after initialization 178 | setTimeout(showMenu, 1000); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FatSecret MCP Server 2 | 3 | A Model Context Protocol (MCP) server that provides access to the FatSecret nutrition database API with full 3-Legged OAuth authentication support. 4 | 5 | ## Features 6 | 7 | - **Complete OAuth 1.0a Implementation**: Full 3-legged OAuth flow for user authentication 8 | - **Food Database Access**: Search and retrieve detailed nutrition information 9 | - **Recipe Database**: Search for recipes and get detailed cooking instructions 10 | - **User Data Management**: Access user food diaries and add food entries 11 | - **Secure Credential Storage**: Encrypted storage of API credentials and tokens 12 | 13 | ## Getting Started 14 | 15 | ### Prerequisites 16 | 17 | - Node.js (v14 or higher) 18 | - npm or yarn 19 | - A FatSecret developer account 20 | 21 | ### Installation 22 | 23 | ```bash 24 | # Clone the repository 25 | git clone https://github.com/your-username/fatsecret-mcp.git 26 | cd fatsecret-mcp 27 | 28 | # Install dependencies 29 | npm install 30 | 31 | # Build the TypeScript 32 | npm run build 33 | ``` 34 | 35 | ## Setup 36 | 37 | ### 1. Get FatSecret API Credentials 38 | 39 | 1. Visit the [FatSecret Platform](https://platform.fatsecret.com/) 40 | 2. Create a developer account and register your application 41 | 3. Note down your **Client ID** and **Client Secret** 42 | 43 | ### 2. Configure the MCP Server 44 | 45 | The server needs to be configured in your MCP client (like Claude Desktop). Add this to your MCP configuration: 46 | 47 | ```json 48 | { 49 | "mcpServers": { 50 | "fatsecret": { 51 | "command": "node", 52 | "args": ["path/to/fatsecret-mcp-server/dist/index.js"] 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | ### 3. Authentication Process 59 | 60 | #### Option 1: Using the OAuth Console Utility (Recommended) 61 | 62 | The easiest way to authenticate is using the included OAuth console utility: 63 | 64 | ```bash 65 | # Make sure you've built the project first 66 | npm run build 67 | 68 | # Run the OAuth console utility 69 | node dist/cli.js 70 | ``` 71 | 72 | This interactive utility will: 73 | 1. Ask for your Client ID and Client Secret 74 | 2. Save them securely in `~/.fatsecret-mcp-config.json` 75 | 3. Guide you through the OAuth flow: 76 | - Opens your browser to the FatSecret authorization page 77 | - Prompts you to paste the verifier code after authorization 78 | - Saves the access tokens for future use 79 | 80 | #### Option 2: Manual Authentication via MCP Tools 81 | 82 | If you prefer to authenticate through the MCP interface (e.g., in Claude): 83 | 84 | 1. **Set your API credentials:** 85 | ``` 86 | Use tool: set_credentials 87 | Parameters: 88 | - clientId: "your_client_id_here" 89 | - clientSecret: "your_client_secret_here" 90 | ``` 91 | 92 | 2. **Start the OAuth flow:** 93 | ``` 94 | Use tool: start_oauth_flow 95 | Parameters: 96 | - callbackUrl: "oob" (for out-of-band authentication) 97 | ``` 98 | 99 | 3. **Visit the authorization URL** provided in the response: 100 | - Log in to your FatSecret account (or create one) 101 | - Click "Allow" to authorize the application 102 | - Copy the verifier code shown on the page 103 | 104 | 4. **Complete the OAuth flow:** 105 | ``` 106 | Use tool: complete_oauth_flow 107 | Parameters: 108 | - requestToken: [from step 2 response] 109 | - requestTokenSecret: [from step 2 response] 110 | - verifier: [the code you copied from the authorization page] 111 | ``` 112 | 113 | #### Option 3: Using Environment Variables 114 | 115 | You can also provide credentials via environment variables: 116 | 117 | ```bash 118 | # Create a .env file in the project root 119 | CLIENT_ID=your_client_id_here 120 | CLIENT_SECRET=your_client_secret_here 121 | 122 | # The server will automatically load these on startup 123 | ``` 124 | 125 | Note: You'll still need to complete the OAuth flow for user-specific operations. 126 | 127 | ## Usage 128 | 129 | ### 1. Set API Credentials 130 | 131 | First, set your FatSecret API credentials: 132 | 133 | ``` 134 | Use the set_credentials tool with your Client ID and Client Secret 135 | ``` 136 | 137 | ### 2. Authenticate a User (3-Legged OAuth) 138 | 139 | For user-specific operations, you need to complete the OAuth flow: 140 | 141 | ``` 142 | 1. Use start_oauth_flow tool (with callback URL or "oob" for out-of-band) 143 | 2. Visit the provided authorization URL 144 | 3. Authorize the application and get the verifier code 145 | 4. Use complete_oauth_flow tool with the request token, secret, and verifier 146 | ``` 147 | 148 | ### 3. Use the API 149 | 150 | Once authenticated, you can use all available tools: 151 | 152 | #### Food Search and Information 153 | 154 | - `search_foods`: Search for foods in the database 155 | - `get_food`: Get detailed nutrition information for a specific food 156 | 157 | #### Recipe Search and Information 158 | 159 | - `search_recipes`: Search for recipes 160 | - `get_recipe`: Get detailed recipe information including ingredients and instructions 161 | 162 | #### User Data (Requires Authentication) 163 | 164 | - `get_user_profile`: Get the authenticated user's profile 165 | - `get_user_food_entries`: Get food diary entries for a specific date 166 | - `add_food_entry`: Add a food entry to the user's diary 167 | 168 | #### Utility 169 | 170 | - `check_auth_status`: Check current authentication status 171 | 172 | ## Available Tools 173 | 174 | ### Authentication Tools 175 | 176 | #### `set_credentials` 177 | 178 | Set your FatSecret API credentials. 179 | 180 | **Parameters:** 181 | 182 | - `clientId` (string, required): Your FatSecret Client ID 183 | - `clientSecret` (string, required): Your FatSecret Client Secret 184 | 185 | #### `start_oauth_flow` 186 | 187 | Start the 3-legged OAuth flow. 188 | 189 | **Parameters:** 190 | 191 | - `callbackUrl` (string, optional): OAuth callback URL (default: "oob") 192 | 193 | #### `complete_oauth_flow` 194 | 195 | Complete the OAuth flow with authorization. 196 | 197 | **Parameters:** 198 | 199 | - `requestToken` (string, required): Request token from start_oauth_flow 200 | - `requestTokenSecret` (string, required): Request token secret from start_oauth_flow 201 | - `verifier` (string, required): OAuth verifier from authorization 202 | 203 | #### `check_auth_status` 204 | 205 | Check current authentication status. 206 | 207 | ### Food Database Tools 208 | 209 | #### `search_foods` 210 | 211 | Search for foods in the FatSecret database. 212 | 213 | **Parameters:** 214 | 215 | - `searchExpression` (string, required): Search term 216 | - `pageNumber` (number, optional): Page number (default: 0) 217 | - `maxResults` (number, optional): Max results per page (default: 20) 218 | 219 | #### `get_food` 220 | 221 | Get detailed information about a specific food. 222 | 223 | **Parameters:** 224 | 225 | - `foodId` (string, required): FatSecret food ID 226 | 227 | ### Recipe Database Tools 228 | 229 | #### `search_recipes` 230 | 231 | Search for recipes in the FatSecret database. 232 | 233 | **Parameters:** 234 | 235 | - `searchExpression` (string, required): Search term 236 | - `pageNumber` (number, optional): Page number (default: 0) 237 | - `maxResults` (number, optional): Max results per page (default: 20) 238 | 239 | #### `get_recipe` 240 | 241 | Get detailed information about a specific recipe. 242 | 243 | **Parameters:** 244 | 245 | - `recipeId` (string, required): FatSecret recipe ID 246 | 247 | ### User Data Tools (Requires Authentication) 248 | 249 | #### `get_user_profile` 250 | 251 | Get the authenticated user's profile information. 252 | 253 | #### `get_user_food_entries` 254 | 255 | Get user's food diary entries for a specific date. 256 | 257 | **Parameters:** 258 | 259 | - `date` (string, optional): Date in YYYY-MM-DD format (default: today) 260 | 261 | #### `add_food_entry` 262 | 263 | Add a food entry to the user's diary. 264 | 265 | **Parameters:** 266 | 267 | - `foodId` (string, required): FatSecret food ID 268 | - `servingId` (string, required): Serving ID for the food 269 | - `quantity` (number, required): Quantity of the serving 270 | - `mealType` (string, required): Meal type (breakfast, lunch, dinner, snack) 271 | - `date` (string, optional): Date in YYYY-MM-DD format (default: today) 272 | 273 | ## Example Workflow 274 | 275 | 1. **Setup Credentials:** 276 | 277 | ``` 278 | Tool: set_credentials 279 | - clientId: "your_client_id" 280 | - clientSecret: "your_client_secret" 281 | ``` 282 | 283 | 2. **Search for Foods:** 284 | 285 | ``` 286 | Tool: search_foods 287 | - searchExpression: "chicken breast" 288 | ``` 289 | 290 | 3. **Get Food Details:** 291 | 292 | ``` 293 | Tool: get_food 294 | - foodId: "12345" 295 | ``` 296 | 297 | 4. **Authenticate User (if needed):** 298 | 299 | ``` 300 | Tool: start_oauth_flow 301 | - callbackUrl: "oob" 302 | 303 | # Follow the authorization URL, then: 304 | 305 | Tool: complete_oauth_flow 306 | - requestToken: "from_start_oauth_flow" 307 | - requestTokenSecret: "from_start_oauth_flow" 308 | - verifier: "from_authorization_page" 309 | ``` 310 | 311 | 5. **Add Food to Diary:** 312 | ``` 313 | Tool: add_food_entry 314 | - foodId: "12345" 315 | - servingId: "67890" 316 | - quantity: 1 317 | - mealType: "lunch" 318 | ``` 319 | 320 | ## Configuration Storage 321 | 322 | The server stores configuration (credentials and tokens) in `~/.fatsecret-mcp-config.json`. This file contains: 323 | 324 | - API credentials (Client ID and Secret) 325 | - OAuth access tokens (when authenticated) 326 | - User ID (when authenticated) 327 | 328 | ## Security Notes 329 | 330 | - Credentials are stored locally in your home directory 331 | - OAuth tokens are securely managed using proper HMAC-SHA1 signing 332 | - All API communications use HTTPS 333 | - The server implements proper OAuth 1.0a security measures 334 | 335 | ## API Reference 336 | 337 | This server implements the FatSecret Platform API. For detailed API documentation, visit: 338 | 339 | - [FatSecret Platform API Documentation](https://platform.fatsecret.com/docs/guides) 340 | - [OAuth 1.0a Specification](https://tools.ietf.org/html/rfc5849) 341 | 342 | ## Error Handling 343 | 344 | The server provides detailed error messages for common issues: 345 | 346 | - Missing or invalid credentials 347 | - OAuth flow errors 348 | - API rate limiting 349 | - Network connectivity issues 350 | - Invalid parameters 351 | 352 | ## Testing 353 | 354 | ### Testing from the Command Line 355 | 356 | The project includes several test utilities: 357 | 358 | #### 1. Interactive Test Tool 359 | 360 | ```bash 361 | # Run the interactive test menu 362 | node test-interactive.js 363 | ``` 364 | 365 | This provides a menu-driven interface to test all MCP tools. 366 | 367 | #### 2. Date Conversion Test 368 | 369 | ```bash 370 | # Test the date conversion logic 371 | node test-date-conversion.js 372 | ``` 373 | 374 | Verifies that dates are correctly converted to FatSecret's "days since epoch" format. 375 | 376 | #### 3. Direct JSON-RPC Testing 377 | 378 | ```bash 379 | # Send test messages via pipe 380 | node test-mcp.js | node dist/index.js 381 | ``` 382 | 383 | ### Testing in Claude Desktop 384 | 385 | 1. Restart Claude Desktop after configuring the MCP server 386 | 2. Look for "fatsecret" in the available tools 387 | 3. Start by using `check_auth_status` to verify the connection 388 | 389 | ## Troubleshooting 390 | 391 | ### Common Issues 392 | 393 | #### "Invalid integer value: date" 394 | - The FatSecret API expects dates as days since epoch (1970-01-01) 395 | - The server automatically converts YYYY-MM-DD format dates 396 | - If you get this error, ensure you're using the latest version 397 | 398 | #### OAuth Authentication Fails 399 | - Verify your Client ID and Client Secret are correct 400 | - Ensure you're using the correct URLs (authentication.fatsecret.com for OAuth) 401 | - Check that you're copying the entire verifier code from the authorization page 402 | 403 | #### Server Not Found in Claude 404 | - Ensure the path in your MCP configuration is absolute, not relative 405 | - Verify the server was built successfully (`npm run build`) 406 | - Check Claude's logs for any error messages 407 | 408 | #### "User authentication required" 409 | - Complete the OAuth flow using either the CLI utility or MCP tools 410 | - Check authentication status with `check_auth_status` tool 411 | - Tokens are saved in `~/.fatsecret-mcp-config.json` 412 | 413 | ## Development 414 | 415 | To modify or extend the server: 416 | 417 | ```bash 418 | # Install dependencies 419 | npm install 420 | 421 | # Build and run 422 | npm run build 423 | npm start 424 | 425 | # Development mode with auto-rebuild 426 | npm run dev 427 | ``` 428 | 429 | ### Project Structure 430 | 431 | ``` 432 | fatsecret-mcp/ 433 | ├── src/ 434 | │ ├── index.ts # Main MCP server implementation 435 | │ └── cli.ts # OAuth console utility 436 | ├── dist/ # Compiled JavaScript files 437 | ├── test-*.js # Test utilities 438 | ├── package.json 439 | ├── tsconfig.json 440 | └── README.md 441 | ``` 442 | 443 | ## License 444 | 445 | MIT License - see LICENSE file for details. 446 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Standalone FatSecret OAuth Console Utility 5 | * 6 | * This script can be run independently to complete the OAuth flow 7 | * and save credentials to the config file. 8 | */ 9 | 10 | import crypto from "crypto"; 11 | import fetch from "node-fetch"; 12 | import querystring from "querystring"; 13 | import fs from "fs/promises"; 14 | import path from "path"; 15 | import os from "os"; 16 | import readline from "readline"; 17 | import { exec } from "child_process"; 18 | import { promisify } from "util"; 19 | import * as dotenv from "dotenv"; 20 | 21 | // Suppress dotenv console output by temporarily overriding console.log 22 | const originalLog = console.log; 23 | console.log = () => {}; 24 | dotenv.config(); 25 | console.log = originalLog; 26 | 27 | const execAsync = promisify(exec); 28 | 29 | interface FatSecretConfig { 30 | clientId: string; 31 | clientSecret: string; 32 | accessToken?: string; 33 | accessTokenSecret?: string; 34 | userId?: string; 35 | } 36 | 37 | class FatSecretOAuthConsole { 38 | private config: FatSecretConfig; 39 | private configPath: string; 40 | private readonly apiUrl = "https://platform.fatsecret.com/rest/server.api"; 41 | private readonly requestTokenUrl = "https://authentication.fatsecret.com/oauth/request_token"; 42 | private readonly authorizeUrl = "https://authentication.fatsecret.com/oauth/authorize"; 43 | private readonly accessTokenUrl = "https://authentication.fatsecret.com/oauth/access_token"; 44 | 45 | constructor() { 46 | this.configPath = path.join(os.homedir(), ".fatsecret-mcp-config.json"); 47 | this.config = { 48 | clientId: process.env.CLIENT_ID || "", 49 | clientSecret: process.env.CLIENT_SECRET || "", 50 | }; 51 | } 52 | 53 | private async loadConfig(): Promise { 54 | try { 55 | const configData = await fs.readFile(this.configPath, "utf-8"); 56 | this.config = { ...this.config, ...JSON.parse(configData) }; 57 | } catch (error) { 58 | // Config file doesn't exist 59 | } 60 | } 61 | 62 | private async saveConfig(): Promise { 63 | await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2)); 64 | } 65 | 66 | private createReadlineInterface(): readline.Interface { 67 | return readline.createInterface({ 68 | input: process.stdin, 69 | output: process.stdout, 70 | }); 71 | } 72 | 73 | private async promptUser(question: string): Promise { 74 | const rl = this.createReadlineInterface(); 75 | return new Promise((resolve) => { 76 | rl.question(question, (answer) => { 77 | rl.close(); 78 | resolve(answer.trim()); 79 | }); 80 | }); 81 | } 82 | 83 | private async openUrlInBrowser(url: string): Promise { 84 | try { 85 | const platform = process.platform; 86 | let command: string; 87 | 88 | switch (platform) { 89 | case "darwin": // macOS 90 | command = `open "${url}"`; 91 | break; 92 | case "win32": // Windows 93 | command = `start "${url}"`; 94 | break; 95 | default: // Linux and others 96 | command = `xdg-open "${url}"`; 97 | break; 98 | } 99 | 100 | await execAsync(command); 101 | return true; 102 | } catch (error) { 103 | return false; 104 | } 105 | } 106 | 107 | private generateNonce(): string { 108 | return crypto.randomBytes(16).toString("hex"); 109 | } 110 | 111 | private generateTimestamp(): string { 112 | return Math.floor(Date.now() / 1000).toString(); 113 | } 114 | 115 | private percentEncode(str: string): string { 116 | return encodeURIComponent(str) 117 | .replace( 118 | /[!'()*]/g, 119 | (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(), 120 | ); 121 | } 122 | 123 | private createSignatureBaseString( 124 | method: string, 125 | url: string, 126 | parameters: Record, 127 | ): string { 128 | const sortedParams = Object.keys(parameters) 129 | .sort() 130 | .map((key) => 131 | `${this.percentEncode(key)}=${this.percentEncode(parameters[key])}` 132 | ) 133 | .join("&"); 134 | 135 | return [ 136 | method.toUpperCase(), 137 | this.percentEncode(url), 138 | this.percentEncode(sortedParams), 139 | ].join("&"); 140 | } 141 | 142 | private createSigningKey( 143 | clientSecret: string, 144 | tokenSecret: string = "", 145 | ): string { 146 | return `${this.percentEncode(clientSecret)}&${ 147 | this.percentEncode(tokenSecret) 148 | }`; 149 | } 150 | 151 | private generateSignature( 152 | method: string, 153 | url: string, 154 | parameters: Record, 155 | clientSecret: string, 156 | tokenSecret: string = "", 157 | ): string { 158 | const baseString = this.createSignatureBaseString(method, url, parameters); 159 | const signingKey = this.createSigningKey(clientSecret, tokenSecret); 160 | 161 | return crypto 162 | .createHmac("sha1", signingKey) 163 | .update(baseString) 164 | .digest("base64"); 165 | } 166 | 167 | private createOAuthHeader( 168 | method: string, 169 | url: string, 170 | additionalParams: Record = {}, 171 | token?: string, 172 | tokenSecret?: string, 173 | regularParams: Record = {}, 174 | ): string { 175 | const timestamp = this.generateTimestamp(); 176 | const nonce = this.generateNonce(); 177 | 178 | const oauthParams: Record = { 179 | oauth_consumer_key: this.config.clientId, 180 | oauth_nonce: nonce, 181 | oauth_signature_method: "HMAC-SHA1", 182 | oauth_timestamp: timestamp, 183 | oauth_version: "1.0", 184 | ...additionalParams, 185 | }; 186 | 187 | if (token) { 188 | oauthParams.oauth_token = token; 189 | } 190 | 191 | // For signature calculation, we need ALL parameters (OAuth + regular) 192 | const allParams = { ...oauthParams, ...regularParams }; 193 | 194 | const signature = this.generateSignature( 195 | method, 196 | url, 197 | allParams, 198 | this.config.clientSecret, 199 | tokenSecret, 200 | ); 201 | 202 | oauthParams.oauth_signature = signature; 203 | 204 | const headerParts = Object.keys(oauthParams) 205 | .sort() 206 | .map((key) => 207 | `${this.percentEncode(key)}="${this.percentEncode(oauthParams[key])}"` 208 | ) 209 | .join(", "); 210 | 211 | return `OAuth ${headerParts}`; 212 | } 213 | 214 | private async makeOAuthRequest( 215 | method: string, 216 | url: string, 217 | params: Record = {}, 218 | token?: string, 219 | tokenSecret?: string, 220 | ): Promise { 221 | const timestamp = this.generateTimestamp(); 222 | const nonce = this.generateNonce(); 223 | 224 | // Build OAuth parameters 225 | const oauthParams: Record = { 226 | oauth_consumer_key: this.config.clientId, 227 | oauth_nonce: nonce, 228 | oauth_signature_method: "HMAC-SHA1", 229 | oauth_timestamp: timestamp, 230 | oauth_version: "1.0", 231 | }; 232 | 233 | if (token) { 234 | oauthParams.oauth_token = token; 235 | } 236 | 237 | // Combine OAuth and regular parameters for signature 238 | const allParams = { ...params, ...oauthParams }; 239 | 240 | // Generate signature with all parameters 241 | const signature = this.generateSignature( 242 | method, 243 | url, 244 | allParams, 245 | this.config.clientSecret, 246 | tokenSecret, 247 | ); 248 | 249 | // Add signature to the parameters 250 | allParams.oauth_signature = signature; 251 | 252 | const options: any = { 253 | method, 254 | headers: {}, 255 | }; 256 | 257 | let requestUrl = url; 258 | if (method === "GET") { 259 | requestUrl += "?" + querystring.stringify(allParams); 260 | } else if (method === "POST") { 261 | options.headers["Content-Type"] = "application/x-www-form-urlencoded"; 262 | options.body = querystring.stringify(allParams); 263 | } 264 | 265 | console.log(`Making ${method} request to: ${requestUrl}`); 266 | 267 | const response = await fetch(requestUrl, options); 268 | const text = await response.text(); 269 | 270 | console.log(`Response status: ${response.status}`); 271 | 272 | if (!response.ok) { 273 | throw new Error(`HTTP ${response.status}: ${text}`); 274 | } 275 | 276 | // Try to parse as JSON, fallback to query string 277 | try { 278 | return JSON.parse(text); 279 | } catch { 280 | return querystring.parse(text); 281 | } 282 | } 283 | 284 | private async makeApiRequest( 285 | method: string, 286 | params: Record = {}, 287 | token?: string, 288 | tokenSecret?: string, 289 | ): Promise { 290 | const timestamp = this.generateTimestamp(); 291 | const nonce = this.generateNonce(); 292 | 293 | // Build OAuth parameters 294 | const oauthParams: Record = { 295 | oauth_consumer_key: this.config.clientId, 296 | oauth_nonce: nonce, 297 | oauth_signature_method: "HMAC-SHA1", 298 | oauth_timestamp: timestamp, 299 | oauth_version: "1.0", 300 | }; 301 | 302 | if (token) { 303 | oauthParams.oauth_token = token; 304 | } 305 | 306 | // Add format=json for API requests 307 | params.format = "json"; 308 | 309 | // Combine OAuth and regular parameters for signature 310 | const allParams = { ...params, ...oauthParams }; 311 | 312 | // Generate signature with all parameters 313 | const signature = this.generateSignature( 314 | method, 315 | this.apiUrl, 316 | allParams, 317 | this.config.clientSecret, 318 | tokenSecret, 319 | ); 320 | 321 | // Add signature to the parameters 322 | allParams.oauth_signature = signature; 323 | 324 | const options: any = { 325 | method, 326 | headers: {}, 327 | }; 328 | 329 | let requestUrl = this.apiUrl; 330 | if (method === "GET") { 331 | requestUrl += "?" + querystring.stringify(allParams); 332 | } else if (method === "POST") { 333 | options.headers["Content-Type"] = "application/x-www-form-urlencoded"; 334 | options.body = querystring.stringify(allParams); 335 | } 336 | 337 | const response = await fetch(requestUrl, options); 338 | const text = await response.text(); 339 | 340 | if (!response.ok) { 341 | throw new Error(`HTTP ${response.status}: ${text}`); 342 | } 343 | 344 | // Try to parse as JSON, fallback to query string 345 | try { 346 | return JSON.parse(text); 347 | } catch { 348 | return querystring.parse(text); 349 | } 350 | } 351 | 352 | async setupCredentials(): Promise { 353 | console.log("=== FatSecret API Credentials Setup ===\n"); 354 | 355 | if (this.config.clientId && this.config.clientSecret) { 356 | console.log("Existing credentials found:"); 357 | console.log(`Client ID: ${this.config.clientId}`); 358 | console.log( 359 | `Client Secret: ${this.config.clientSecret.substring(0, 8)}...`, 360 | ); 361 | 362 | const useExisting = await this.promptUser( 363 | "\nUse existing credentials? (y/n): ", 364 | ); 365 | if ( 366 | useExisting.toLowerCase() === "y" || useExisting.toLowerCase() === "yes" 367 | ) { 368 | return; 369 | } 370 | } 371 | 372 | console.log("Please enter your FatSecret API credentials."); 373 | console.log("You can get these from: https://platform.fatsecret.com/\n"); 374 | 375 | this.config.clientId = await this.promptUser("Client ID: "); 376 | this.config.clientSecret = await this.promptUser("Client Secret: "); 377 | 378 | if (!this.config.clientId || !this.config.clientSecret) { 379 | throw new Error("Client ID and Client Secret are required"); 380 | } 381 | 382 | await this.saveConfig(); 383 | console.log("✓ Credentials saved successfully\n"); 384 | } 385 | 386 | async runOAuthFlow(): Promise { 387 | console.log("=== Starting OAuth Flow ===\n"); 388 | 389 | // Step 1: Get request token 390 | console.log("Step 1: Getting request token..."); 391 | 392 | let requestToken: string; 393 | let requestTokenSecret: string; 394 | 395 | try { 396 | const response = await this.makeOAuthRequest( 397 | "POST", 398 | this.requestTokenUrl, 399 | { 400 | oauth_callback: "oob", 401 | }, // Regular parameters 402 | undefined, // No token 403 | undefined, // No token secret 404 | ); 405 | 406 | console.log("Response from request token endpoint:", response); 407 | 408 | requestToken = response.oauth_token; 409 | requestTokenSecret = response.oauth_token_secret; 410 | 411 | if (!requestToken || !requestTokenSecret) { 412 | throw new Error("Invalid response: missing token or token secret"); 413 | } 414 | 415 | console.log("✓ Request token obtained\n"); 416 | } catch (error) { 417 | console.error("Failed to get request token:", error); 418 | throw error; 419 | } 420 | 421 | // Step 2: User authorization 422 | console.log("Step 2: User authorization"); 423 | const authUrl = `${this.authorizeUrl}?oauth_token=${requestToken}`; 424 | 425 | console.log("Opening authorization URL in your browser..."); 426 | console.log(`URL: ${authUrl}\n`); 427 | 428 | const browserOpened = await this.openUrlInBrowser(authUrl); 429 | if (!browserOpened) { 430 | console.log( 431 | "Could not open browser automatically. Please visit the URL above manually.\n", 432 | ); 433 | } 434 | 435 | console.log("Instructions:"); 436 | console.log("1. Log in to your FatSecret account (or create one)"); 437 | console.log('2. Click "Allow" to authorize this application'); 438 | console.log("3. Copy the verifier code from the page"); 439 | console.log("4. Paste it below\n"); 440 | 441 | const verifier = await this.promptUser("Enter the verifier code: "); 442 | 443 | if (!verifier) { 444 | throw new Error("Verifier code is required"); 445 | } 446 | 447 | // Step 3: Get access token 448 | console.log("\nStep 3: Getting access token..."); 449 | 450 | try { 451 | const accessResponse = await this.makeOAuthRequest( 452 | "GET", 453 | this.accessTokenUrl, 454 | { 455 | oauth_verifier: verifier, 456 | }, // Regular parameters 457 | requestToken, 458 | requestTokenSecret, 459 | ); 460 | 461 | if (!accessResponse.oauth_token || !accessResponse.oauth_token_secret) { 462 | throw new Error( 463 | "Invalid response from access token endpoint. Please try again.", 464 | ); 465 | } 466 | 467 | this.config.accessToken = accessResponse.oauth_token; 468 | this.config.accessTokenSecret = accessResponse.oauth_token_secret; 469 | this.config.userId = accessResponse.user_id; 470 | 471 | await this.saveConfig(); 472 | 473 | console.log("✓ Access token obtained"); 474 | console.log("✓ OAuth flow completed successfully!\n"); 475 | console.log(`User ID: ${this.config.userId}`); 476 | console.log("Authentication details saved to:", this.configPath); 477 | } catch (error) { 478 | console.error("Failed to get access token:", error); 479 | throw error; 480 | } 481 | } 482 | 483 | async checkStatus(): Promise { 484 | console.log("=== Authentication Status ===\n"); 485 | 486 | const hasCredentials = !!(this.config.clientId && this.config.clientSecret); 487 | const hasAccessToken = 488 | !!(this.config.accessToken && this.config.accessTokenSecret); 489 | 490 | console.log(`Credentials configured: ${hasCredentials ? "✓" : "✗"}`); 491 | console.log(`User authenticated: ${hasAccessToken ? "✓" : "✗"}`); 492 | 493 | if (hasCredentials) { 494 | console.log(`Client ID: ${this.config.clientId}`); 495 | } 496 | 497 | if (hasAccessToken) { 498 | console.log(`User ID: ${this.config.userId || "N/A"}`); 499 | } 500 | 501 | console.log(`Config file: ${this.configPath}`); 502 | } 503 | 504 | async run(): Promise { 505 | try { 506 | await this.loadConfig(); 507 | 508 | console.log("FatSecret OAuth Console Utility\n"); 509 | console.log( 510 | "This utility will help you authenticate with the FatSecret API.\n", 511 | ); 512 | 513 | // Setup credentials 514 | await this.setupCredentials(); 515 | 516 | // Run OAuth flow 517 | const runOAuth = await this.promptUser( 518 | "Do you want to authenticate a user now? (y/n): ", 519 | ); 520 | if (runOAuth.toLowerCase() === "y" || runOAuth.toLowerCase() === "yes") { 521 | await this.runOAuthFlow(); 522 | } 523 | 524 | // Show final status 525 | console.log(); 526 | await this.checkStatus(); 527 | 528 | console.log( 529 | "\nSetup complete! You can now use the FatSecret MCP server.", 530 | ); 531 | } catch (error) { 532 | console.error( 533 | "Error:", 534 | error instanceof Error ? error.message : "Unknown error", 535 | ); 536 | process.exit(1); 537 | } 538 | } 539 | } 540 | 541 | // Run if called directly 542 | if (import.meta.url === `file://${process.argv[1]}`) { 543 | const oauth = new FatSecretOAuthConsole(); 544 | oauth.run(); 545 | } 546 | 547 | export default FatSecretOAuthConsole; 548 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ErrorCode, 8 | ListToolsRequestSchema, 9 | McpError, 10 | } from "@modelcontextprotocol/sdk/types.js"; 11 | import crypto from "crypto"; 12 | import fetch from "node-fetch"; 13 | import querystring from "querystring"; 14 | import fs from "fs/promises"; 15 | import path from "path"; 16 | import os from "os"; 17 | import * as dotenv from "dotenv"; 18 | 19 | // Suppress dotenv console output by temporarily overriding console.log 20 | const originalLog = console.log; 21 | console.log = () => {}; 22 | dotenv.config(); 23 | console.log = originalLog; 24 | 25 | interface FatSecretConfig { 26 | clientId: string; 27 | clientSecret: string; 28 | accessToken?: string; 29 | accessTokenSecret?: string; 30 | userId?: string; 31 | } 32 | 33 | interface OAuthToken { 34 | oauth_token: string; 35 | oauth_token_secret: string; 36 | oauth_callback_confirmed?: string; 37 | } 38 | 39 | interface AccessToken { 40 | oauth_token: string; 41 | oauth_token_secret: string; 42 | user_id?: string; 43 | } 44 | 45 | class FatSecretMCPServer { 46 | private server: Server; 47 | private config: FatSecretConfig; 48 | private configPath: string; 49 | private readonly baseUrl = "https://platform.fatsecret.com/rest/server.api"; 50 | private readonly requestTokenUrl = "https://authentication.fatsecret.com/oauth/request_token"; 51 | private readonly authorizeUrl = "https://authentication.fatsecret.com/oauth/authorize"; 52 | private readonly accessTokenUrl = "https://authentication.fatsecret.com/oauth/access_token"; 53 | 54 | constructor() { 55 | this.server = new Server( 56 | { 57 | name: "fatsecret-mcp-server", 58 | version: "0.1.0", 59 | } 60 | ); 61 | 62 | this.configPath = path.join(os.homedir(), ".fatsecret-mcp-config.json"); 63 | this.config = { 64 | clientId: process.env.CLIENT_ID || "", 65 | clientSecret: process.env.CLIENT_SECRET || "", 66 | }; 67 | 68 | this.setupToolHandlers(); 69 | } 70 | 71 | private async loadConfig(): Promise { 72 | try { 73 | const configData = await fs.readFile(this.configPath, "utf-8"); 74 | this.config = { ...this.config, ...JSON.parse(configData) }; 75 | } catch (error) { 76 | // Config file doesn't exist, will be created when credentials are set 77 | } 78 | } 79 | 80 | private async saveConfig(): Promise { 81 | await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2)); 82 | } 83 | 84 | private generateNonce(): string { 85 | return crypto.randomBytes(16).toString("hex"); 86 | } 87 | 88 | private generateTimestamp(): string { 89 | return Math.floor(Date.now() / 1000).toString(); 90 | } 91 | 92 | private dateToFatSecretFormat(dateString?: string): string { 93 | // Convert date to days since epoch (1970-01-01) 94 | // If no date provided, use today 95 | const date = dateString ? new Date(dateString) : new Date(); 96 | const epochStart = new Date('1970-01-01'); 97 | const daysSinceEpoch = Math.floor((date.getTime() - epochStart.getTime()) / (1000 * 60 * 60 * 24)); 98 | return daysSinceEpoch.toString(); 99 | } 100 | 101 | private percentEncode(str: string): string { 102 | return encodeURIComponent(str) 103 | .replace( 104 | /[!'()*]/g, 105 | (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(), 106 | ); 107 | } 108 | 109 | private createSignatureBaseString( 110 | method: string, 111 | url: string, 112 | parameters: Record, 113 | ): string { 114 | const sortedParams = Object.keys(parameters) 115 | .sort() 116 | .map((key) => 117 | `${this.percentEncode(key)}=${this.percentEncode(parameters[key])}` 118 | ) 119 | .join("&"); 120 | 121 | return [ 122 | method.toUpperCase(), 123 | this.percentEncode(url), 124 | this.percentEncode(sortedParams), 125 | ].join("&"); 126 | } 127 | 128 | private createSigningKey( 129 | clientSecret: string, 130 | tokenSecret: string = "", 131 | ): string { 132 | return `${this.percentEncode(clientSecret)}&${ 133 | this.percentEncode(tokenSecret) 134 | }`; 135 | } 136 | 137 | private generateSignature( 138 | method: string, 139 | url: string, 140 | parameters: Record, 141 | clientSecret: string, 142 | tokenSecret: string = "", 143 | ): string { 144 | const baseString = this.createSignatureBaseString(method, url, parameters); 145 | const signingKey = this.createSigningKey(clientSecret, tokenSecret); 146 | 147 | return crypto 148 | .createHmac("sha1", signingKey) 149 | .update(baseString) 150 | .digest("base64"); 151 | } 152 | 153 | private createOAuthHeader( 154 | method: string, 155 | url: string, 156 | additionalParams: Record = {}, 157 | token?: string, 158 | tokenSecret?: string, 159 | regularParams: Record = {}, 160 | ): string { 161 | const timestamp = this.generateTimestamp(); 162 | const nonce = this.generateNonce(); 163 | 164 | const oauthParams: Record = { 165 | oauth_consumer_key: this.config.clientId, 166 | oauth_nonce: nonce, 167 | oauth_signature_method: "HMAC-SHA1", 168 | oauth_timestamp: timestamp, 169 | oauth_version: "1.0", 170 | ...additionalParams, 171 | }; 172 | 173 | if (token) { 174 | oauthParams.oauth_token = token; 175 | } 176 | 177 | // For signature calculation, we need ALL parameters (OAuth + regular) 178 | const allParams = { ...oauthParams, ...regularParams }; 179 | 180 | const signature = this.generateSignature( 181 | method, 182 | url, 183 | allParams, 184 | this.config.clientSecret, 185 | tokenSecret, 186 | ); 187 | 188 | oauthParams.oauth_signature = signature; 189 | 190 | const headerParts = Object.keys(oauthParams) 191 | .sort() 192 | .map((key) => 193 | `${this.percentEncode(key)}="${this.percentEncode(oauthParams[key])}"` 194 | ) 195 | .join(", "); 196 | 197 | return `OAuth ${headerParts}`; 198 | } 199 | 200 | private async makeOAuthRequest( 201 | method: string, 202 | url: string, 203 | params: Record = {}, 204 | token?: string, 205 | tokenSecret?: string, 206 | ): Promise { 207 | const timestamp = this.generateTimestamp(); 208 | const nonce = this.generateNonce(); 209 | 210 | // Build OAuth parameters 211 | const oauthParams: Record = { 212 | oauth_consumer_key: this.config.clientId, 213 | oauth_nonce: nonce, 214 | oauth_signature_method: "HMAC-SHA1", 215 | oauth_timestamp: timestamp, 216 | oauth_version: "1.0", 217 | }; 218 | 219 | if (token) { 220 | oauthParams.oauth_token = token; 221 | } 222 | 223 | // Combine OAuth and regular parameters for signature 224 | const allParams = { ...params, ...oauthParams }; 225 | 226 | // Generate signature with all parameters 227 | const signature = this.generateSignature( 228 | method, 229 | url, 230 | allParams, 231 | this.config.clientSecret, 232 | tokenSecret, 233 | ); 234 | 235 | // Add signature to the parameters 236 | allParams.oauth_signature = signature; 237 | 238 | const options: any = { 239 | method, 240 | headers: {}, 241 | }; 242 | 243 | let requestUrl = url; 244 | if (method === "GET") { 245 | requestUrl += "?" + querystring.stringify(allParams); 246 | } else if (method === "POST") { 247 | options.headers["Content-Type"] = "application/x-www-form-urlencoded"; 248 | options.body = querystring.stringify(allParams); 249 | } 250 | 251 | const response = await fetch(requestUrl, options); 252 | const text = await response.text(); 253 | 254 | if (!response.ok) { 255 | throw new Error(`OAuth error: ${response.status} - ${text}`); 256 | } 257 | 258 | // Try to parse as JSON, fallback to query string 259 | try { 260 | return JSON.parse(text); 261 | } catch { 262 | return querystring.parse(text); 263 | } 264 | } 265 | 266 | private async makeApiRequest( 267 | method: string, 268 | url: string, 269 | params: Record = {}, 270 | useAccessToken: boolean = true, 271 | ): Promise { 272 | const timestamp = this.generateTimestamp(); 273 | const nonce = this.generateNonce(); 274 | 275 | // Build OAuth parameters 276 | const oauthParams: Record = { 277 | oauth_consumer_key: this.config.clientId, 278 | oauth_nonce: nonce, 279 | oauth_signature_method: "HMAC-SHA1", 280 | oauth_timestamp: timestamp, 281 | oauth_version: "1.0", 282 | }; 283 | 284 | if (useAccessToken && this.config.accessToken && this.config.accessTokenSecret) { 285 | oauthParams.oauth_token = this.config.accessToken; 286 | } 287 | 288 | // Add format=json for API requests 289 | params.format = "json"; 290 | 291 | // Combine OAuth and regular parameters for signature 292 | const allParams = { ...params, ...oauthParams }; 293 | 294 | // Generate signature with all parameters 295 | const tokenSecret = useAccessToken ? this.config.accessTokenSecret : undefined; 296 | const signature = this.generateSignature( 297 | method, 298 | url, 299 | allParams, 300 | this.config.clientSecret, 301 | tokenSecret, 302 | ); 303 | 304 | // Add signature to the parameters 305 | allParams.oauth_signature = signature; 306 | 307 | const options: any = { 308 | method, 309 | headers: {}, 310 | }; 311 | 312 | let requestUrl = url; 313 | if (method === "GET") { 314 | requestUrl += "?" + querystring.stringify(allParams); 315 | } else if (method === "POST") { 316 | options.headers["Content-Type"] = "application/x-www-form-urlencoded"; 317 | options.body = querystring.stringify(allParams); 318 | } 319 | 320 | const response = await fetch(requestUrl, options); 321 | const text = await response.text(); 322 | 323 | if (!response.ok) { 324 | throw new Error(`FatSecret API error: ${response.status} - ${text}`); 325 | } 326 | 327 | // Try to parse as JSON, fallback to query string 328 | try { 329 | return JSON.parse(text); 330 | } catch { 331 | return querystring.parse(text); 332 | } 333 | } 334 | 335 | private setupToolHandlers() { 336 | this.server.setRequestHandler(ListToolsRequestSchema, async () => { 337 | return { 338 | tools: [ 339 | { 340 | name: "set_credentials", 341 | description: 342 | "Set FatSecret API credentials (Client ID and Client Secret)", 343 | inputSchema: { 344 | type: "object", 345 | properties: { 346 | clientId: { 347 | type: "string", 348 | description: "Your FatSecret Client ID", 349 | }, 350 | clientSecret: { 351 | type: "string", 352 | description: "Your FatSecret Client Secret", 353 | }, 354 | }, 355 | required: ["clientId", "clientSecret"], 356 | }, 357 | }, 358 | { 359 | name: "start_oauth_flow", 360 | description: 361 | "Start the 3-legged OAuth flow to get user authorization", 362 | inputSchema: { 363 | type: "object", 364 | properties: { 365 | callbackUrl: { 366 | type: "string", 367 | description: 'OAuth callback URL (use "oob" for out-of-band)', 368 | default: "oob", 369 | }, 370 | }, 371 | }, 372 | }, 373 | { 374 | name: "complete_oauth_flow", 375 | description: 376 | "Complete the OAuth flow with the authorization code/verifier", 377 | inputSchema: { 378 | type: "object", 379 | properties: { 380 | requestToken: { 381 | type: "string", 382 | description: "The request token from start_oauth_flow", 383 | }, 384 | requestTokenSecret: { 385 | type: "string", 386 | description: "The request token secret from start_oauth_flow", 387 | }, 388 | verifier: { 389 | type: "string", 390 | description: 391 | "The OAuth verifier from the callback or authorization page", 392 | }, 393 | }, 394 | required: ["requestToken", "requestTokenSecret", "verifier"], 395 | }, 396 | }, 397 | { 398 | name: "search_foods", 399 | description: "Search for foods in the FatSecret database", 400 | inputSchema: { 401 | type: "object", 402 | properties: { 403 | searchExpression: { 404 | type: "string", 405 | description: 406 | 'Search term for foods (e.g., "chicken breast", "apple")', 407 | }, 408 | pageNumber: { 409 | type: "number", 410 | description: "Page number for results (default: 0)", 411 | default: 0, 412 | }, 413 | maxResults: { 414 | type: "number", 415 | description: "Maximum results per page (default: 20)", 416 | default: 20, 417 | }, 418 | }, 419 | required: ["searchExpression"], 420 | }, 421 | }, 422 | { 423 | name: "get_food", 424 | description: "Get detailed information about a specific food item", 425 | inputSchema: { 426 | type: "object", 427 | properties: { 428 | foodId: { 429 | type: "string", 430 | description: "The FatSecret food ID", 431 | }, 432 | }, 433 | required: ["foodId"], 434 | }, 435 | }, 436 | { 437 | name: "search_recipes", 438 | description: "Search for recipes in the FatSecret database", 439 | inputSchema: { 440 | type: "object", 441 | properties: { 442 | searchExpression: { 443 | type: "string", 444 | description: "Search term for recipes", 445 | }, 446 | pageNumber: { 447 | type: "number", 448 | description: "Page number for results (default: 0)", 449 | default: 0, 450 | }, 451 | maxResults: { 452 | type: "number", 453 | description: "Maximum results per page (default: 20)", 454 | default: 20, 455 | }, 456 | }, 457 | required: ["searchExpression"], 458 | }, 459 | }, 460 | { 461 | name: "get_recipe", 462 | description: "Get detailed information about a specific recipe", 463 | inputSchema: { 464 | type: "object", 465 | properties: { 466 | recipeId: { 467 | type: "string", 468 | description: "The FatSecret recipe ID", 469 | }, 470 | }, 471 | required: ["recipeId"], 472 | }, 473 | }, 474 | { 475 | name: "get_user_profile", 476 | description: "Get the authenticated user's profile information", 477 | inputSchema: { 478 | type: "object", 479 | properties: {}, 480 | }, 481 | }, 482 | { 483 | name: "get_user_food_entries", 484 | description: "Get user's food diary entries for a specific date", 485 | inputSchema: { 486 | type: "object", 487 | properties: { 488 | date: { 489 | type: "string", 490 | description: "Date in YYYY-MM-DD format (default: today)", 491 | }, 492 | }, 493 | }, 494 | }, 495 | { 496 | name: "add_food_entry", 497 | description: "Add a food entry to the user's diary", 498 | inputSchema: { 499 | type: "object", 500 | properties: { 501 | foodId: { 502 | type: "string", 503 | description: "The FatSecret food ID", 504 | }, 505 | servingId: { 506 | type: "string", 507 | description: "The serving ID for the food", 508 | }, 509 | quantity: { 510 | type: "number", 511 | description: "Quantity of the serving", 512 | }, 513 | mealType: { 514 | type: "string", 515 | description: "Meal type (breakfast, lunch, dinner, snack)", 516 | enum: ["breakfast", "lunch", "dinner", "snack"], 517 | }, 518 | date: { 519 | type: "string", 520 | description: "Date in YYYY-MM-DD format (default: today)", 521 | }, 522 | }, 523 | required: ["foodId", "servingId", "quantity", "mealType"], 524 | }, 525 | }, 526 | { 527 | name: "check_auth_status", 528 | description: "Check if the user is authenticated with FatSecret", 529 | inputSchema: { 530 | type: "object", 531 | properties: {}, 532 | }, 533 | }, 534 | { 535 | name: "get_weight_month", 536 | description: "Get user's weight entries for a specific month", 537 | inputSchema: { 538 | type: "object", 539 | properties: { 540 | date: { 541 | type: "string", 542 | description: "Date in YYYY-MM-DD format to specify the month (default: current month)", 543 | }, 544 | }, 545 | }, 546 | }, 547 | ], 548 | }; 549 | }); 550 | 551 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 552 | await this.loadConfig(); 553 | 554 | switch (request.params.name) { 555 | case "set_credentials": 556 | return await this.handleSetCredentials(request.params.arguments); 557 | case "start_oauth_flow": 558 | return await this.handleStartOAuthFlow(request.params.arguments); 559 | case "complete_oauth_flow": 560 | return await this.handleCompleteOAuthFlow(request.params.arguments); 561 | case "search_foods": 562 | return await this.handleSearchFoods(request.params.arguments); 563 | case "get_food": 564 | return await this.handleGetFood(request.params.arguments); 565 | case "search_recipes": 566 | return await this.handleSearchRecipes(request.params.arguments); 567 | case "get_recipe": 568 | return await this.handleGetRecipe(request.params.arguments); 569 | case "get_user_profile": 570 | return await this.handleGetUserProfile(request.params.arguments); 571 | case "get_user_food_entries": 572 | return await this.handleGetUserFoodEntries(request.params.arguments); 573 | case "add_food_entry": 574 | return await this.handleAddFoodEntry(request.params.arguments); 575 | case "check_auth_status": 576 | return await this.handleCheckAuthStatus(request.params.arguments); 577 | case "get_weight_month": 578 | return await this.handleGetWeightMonth(request.params.arguments); 579 | default: 580 | throw new McpError( 581 | ErrorCode.MethodNotFound, 582 | `Unknown tool: ${request.params.name}`, 583 | ); 584 | } 585 | }); 586 | } 587 | 588 | private async handleSetCredentials(args: any) { 589 | this.config.clientId = args.clientId; 590 | this.config.clientSecret = args.clientSecret; 591 | await this.saveConfig(); 592 | 593 | return { 594 | content: [ 595 | { 596 | type: "text", 597 | text: 598 | "FatSecret API credentials have been set successfully. You can now start the OAuth flow to authenticate users.", 599 | }, 600 | ], 601 | }; 602 | } 603 | 604 | private async handleStartOAuthFlow(args: any) { 605 | if (!this.config.clientId || !this.config.clientSecret) { 606 | throw new McpError( 607 | ErrorCode.InvalidRequest, 608 | "Please set your FatSecret API credentials first using set_credentials", 609 | ); 610 | } 611 | 612 | const callbackUrl = args.callbackUrl || "oob"; 613 | 614 | try { 615 | const response = await this.makeOAuthRequest( 616 | "POST", 617 | this.requestTokenUrl, 618 | { oauth_callback: callbackUrl }, 619 | ); 620 | 621 | const token = response.oauth_token as string; 622 | const tokenSecret = response.oauth_token_secret as string; 623 | const authUrl = `${this.authorizeUrl}?oauth_token=${token}`; 624 | 625 | return { 626 | content: [ 627 | { 628 | type: "text", 629 | text: 630 | `OAuth flow started successfully!\n\nRequest Token: ${token}\nRequest Token Secret: ${tokenSecret}\n\nPlease visit this URL to authorize the application:\n${authUrl}\n\nAfter authorization, you'll receive a verifier code. Use the complete_oauth_flow tool with the request token, request token secret, and verifier to complete the authentication.`, 631 | }, 632 | ], 633 | }; 634 | } catch (error) { 635 | throw new McpError( 636 | ErrorCode.InternalError, 637 | `Failed to start OAuth flow: ${ 638 | error instanceof Error ? error.message : "Unknown error" 639 | }`, 640 | ); 641 | } 642 | } 643 | 644 | private async handleCompleteOAuthFlow(args: any) { 645 | if (!this.config.clientId || !this.config.clientSecret) { 646 | throw new McpError( 647 | ErrorCode.InvalidRequest, 648 | "Please set your FatSecret API credentials first", 649 | ); 650 | } 651 | 652 | try { 653 | const response = await this.makeOAuthRequest( 654 | "GET", 655 | this.accessTokenUrl, 656 | { oauth_verifier: args.verifier }, 657 | args.requestToken, 658 | args.requestTokenSecret, 659 | ); 660 | 661 | const tokenData = response as any; 662 | 663 | this.config.accessToken = tokenData.oauth_token; 664 | this.config.accessTokenSecret = tokenData.oauth_token_secret; 665 | this.config.userId = tokenData.user_id; 666 | 667 | await this.saveConfig(); 668 | 669 | return { 670 | content: [ 671 | { 672 | type: "text", 673 | text: 674 | `OAuth flow completed successfully! You are now authenticated with FatSecret.\n\nUser ID: ${this.config.userId}\n\nYou can now use user-specific tools like get_user_profile, get_user_food_entries, and add_food_entry.`, 675 | }, 676 | ], 677 | }; 678 | } catch (error) { 679 | throw new McpError( 680 | ErrorCode.InternalError, 681 | `Failed to complete OAuth flow: ${ 682 | error instanceof Error ? error.message : "Unknown error" 683 | }`, 684 | ); 685 | } 686 | } 687 | 688 | private async handleSearchFoods(args: any) { 689 | if (!this.config.clientId || !this.config.clientSecret) { 690 | throw new McpError( 691 | ErrorCode.InvalidRequest, 692 | "Please set your FatSecret API credentials first", 693 | ); 694 | } 695 | 696 | try { 697 | const params = { 698 | method: "foods.search", 699 | search_expression: args.searchExpression, 700 | page_number: args.pageNumber?.toString() || "0", 701 | max_results: args.maxResults?.toString() || "20", 702 | format: "json", 703 | }; 704 | 705 | const response = await this.makeApiRequest( 706 | "GET", 707 | this.baseUrl, 708 | params, 709 | false, 710 | ); 711 | 712 | return { 713 | content: [ 714 | { 715 | type: "text", 716 | text: JSON.stringify(response, null, 2), 717 | }, 718 | ], 719 | }; 720 | } catch (error) { 721 | throw new McpError( 722 | ErrorCode.InternalError, 723 | `Failed to search foods: ${ 724 | error instanceof Error ? error.message : "Unknown error" 725 | }`, 726 | ); 727 | } 728 | } 729 | 730 | private async handleGetFood(args: any) { 731 | if (!this.config.clientId || !this.config.clientSecret) { 732 | throw new McpError( 733 | ErrorCode.InvalidRequest, 734 | "Please set your FatSecret API credentials first", 735 | ); 736 | } 737 | 738 | try { 739 | const params = { 740 | method: "food.get", 741 | food_id: args.foodId, 742 | format: "json", 743 | }; 744 | 745 | const response = await this.makeApiRequest( 746 | "GET", 747 | this.baseUrl, 748 | params, 749 | false, 750 | ); 751 | 752 | return { 753 | content: [ 754 | { 755 | type: "text", 756 | text: JSON.stringify(response, null, 2), 757 | }, 758 | ], 759 | }; 760 | } catch (error) { 761 | throw new McpError( 762 | ErrorCode.InternalError, 763 | `Failed to get food: ${ 764 | error instanceof Error ? error.message : "Unknown error" 765 | }`, 766 | ); 767 | } 768 | } 769 | 770 | private async handleSearchRecipes(args: any) { 771 | if (!this.config.clientId || !this.config.clientSecret) { 772 | throw new McpError( 773 | ErrorCode.InvalidRequest, 774 | "Please set your FatSecret API credentials first", 775 | ); 776 | } 777 | 778 | try { 779 | const params = { 780 | method: "recipes.search", 781 | search_expression: args.searchExpression, 782 | page_number: args.pageNumber?.toString() || "0", 783 | max_results: args.maxResults?.toString() || "20", 784 | format: "json", 785 | }; 786 | 787 | const response = await this.makeApiRequest( 788 | "GET", 789 | this.baseUrl, 790 | params, 791 | false, 792 | ); 793 | 794 | return { 795 | content: [ 796 | { 797 | type: "text", 798 | text: JSON.stringify(response, null, 2), 799 | }, 800 | ], 801 | }; 802 | } catch (error) { 803 | throw new McpError( 804 | ErrorCode.InternalError, 805 | `Failed to search recipes: ${ 806 | error instanceof Error ? error.message : "Unknown error" 807 | }`, 808 | ); 809 | } 810 | } 811 | 812 | private async handleGetRecipe(args: any) { 813 | if (!this.config.clientId || !this.config.clientSecret) { 814 | throw new McpError( 815 | ErrorCode.InvalidRequest, 816 | "Please set your FatSecret API credentials first", 817 | ); 818 | } 819 | 820 | try { 821 | const params = { 822 | method: "recipe.get", 823 | recipe_id: args.recipeId, 824 | format: "json", 825 | }; 826 | 827 | const response = await this.makeApiRequest( 828 | "GET", 829 | this.baseUrl, 830 | params, 831 | false, 832 | ); 833 | 834 | return { 835 | content: [ 836 | { 837 | type: "text", 838 | text: JSON.stringify(response, null, 2), 839 | }, 840 | ], 841 | }; 842 | } catch (error) { 843 | throw new McpError( 844 | ErrorCode.InternalError, 845 | `Failed to get recipe: ${ 846 | error instanceof Error ? error.message : "Unknown error" 847 | }`, 848 | ); 849 | } 850 | } 851 | 852 | private async handleGetUserProfile(args: any) { 853 | if (!this.config.accessToken || !this.config.accessTokenSecret) { 854 | throw new McpError( 855 | ErrorCode.InvalidRequest, 856 | "User authentication required. Please complete the OAuth flow first.", 857 | ); 858 | } 859 | 860 | try { 861 | const params = { 862 | method: "profile.get", 863 | format: "json", 864 | }; 865 | 866 | const response = await this.makeApiRequest( 867 | "GET", 868 | this.baseUrl, 869 | params, 870 | true, 871 | ); 872 | 873 | return { 874 | content: [ 875 | { 876 | type: "text", 877 | text: JSON.stringify(response, null, 2), 878 | }, 879 | ], 880 | }; 881 | } catch (error) { 882 | throw new McpError( 883 | ErrorCode.InternalError, 884 | `Failed to get user profile: ${ 885 | error instanceof Error ? error.message : "Unknown error" 886 | }`, 887 | ); 888 | } 889 | } 890 | 891 | private async handleGetUserFoodEntries(args: any) { 892 | if (!this.config.accessToken || !this.config.accessTokenSecret) { 893 | throw new McpError( 894 | ErrorCode.InvalidRequest, 895 | "User authentication required. Please complete the OAuth flow first.", 896 | ); 897 | } 898 | 899 | try { 900 | const date = this.dateToFatSecretFormat(args.date); 901 | const params = { 902 | method: "food_entries.get", 903 | date: date, 904 | format: "json", 905 | }; 906 | 907 | const response = await this.makeApiRequest( 908 | "GET", 909 | this.baseUrl, 910 | params, 911 | true, 912 | ); 913 | 914 | return { 915 | content: [ 916 | { 917 | type: "text", 918 | text: JSON.stringify(response, null, 2), 919 | }, 920 | ], 921 | }; 922 | } catch (error) { 923 | throw new McpError( 924 | ErrorCode.InternalError, 925 | `Failed to get food entries: ${ 926 | error instanceof Error ? error.message : "Unknown error" 927 | }`, 928 | ); 929 | } 930 | } 931 | 932 | private async handleAddFoodEntry(args: any) { 933 | if (!this.config.accessToken || !this.config.accessTokenSecret) { 934 | throw new McpError( 935 | ErrorCode.InvalidRequest, 936 | "User authentication required. Please complete the OAuth flow first.", 937 | ); 938 | } 939 | 940 | try { 941 | const date = this.dateToFatSecretFormat(args.date); 942 | const params = { 943 | method: "food_entry.create", 944 | food_id: args.foodId, 945 | serving_id: args.servingId, 946 | quantity: args.quantity.toString(), 947 | meal: args.mealType, 948 | date: date, 949 | format: "json", 950 | }; 951 | 952 | const response = await this.makeApiRequest( 953 | "POST", 954 | this.baseUrl, 955 | params, 956 | true, 957 | ); 958 | 959 | return { 960 | content: [ 961 | { 962 | type: "text", 963 | text: `Food entry added successfully!\n\n${ 964 | JSON.stringify(response, null, 2) 965 | }`, 966 | }, 967 | ], 968 | }; 969 | } catch (error) { 970 | throw new McpError( 971 | ErrorCode.InternalError, 972 | `Failed to add food entry: ${ 973 | error instanceof Error ? error.message : "Unknown error" 974 | }`, 975 | ); 976 | } 977 | } 978 | 979 | private async handleCheckAuthStatus(args: any) { 980 | const hasCredentials = !!(this.config.clientId && this.config.clientSecret); 981 | const hasAccessToken = 982 | !!(this.config.accessToken && this.config.accessTokenSecret); 983 | 984 | let status = "Not configured"; 985 | if (hasCredentials && hasAccessToken) { 986 | status = "Fully authenticated"; 987 | } else if (hasCredentials) { 988 | status = "Credentials set, authentication needed"; 989 | } 990 | 991 | return { 992 | content: [ 993 | { 994 | type: "text", 995 | text: 996 | `Authentication Status: ${status}\n\nCredentials configured: ${hasCredentials}\nUser authenticated: ${hasAccessToken}\nUser ID: ${ 997 | this.config.userId || "N/A" 998 | }`, 999 | }, 1000 | ], 1001 | }; 1002 | } 1003 | 1004 | private async handleGetWeightMonth(args: any) { 1005 | if (!this.config.accessToken || !this.config.accessTokenSecret) { 1006 | throw new McpError( 1007 | ErrorCode.InvalidRequest, 1008 | "User authentication required. Please complete the OAuth flow first.", 1009 | ); 1010 | } 1011 | 1012 | try { 1013 | const date = this.dateToFatSecretFormat(args.date); 1014 | const params = { 1015 | method: "weights.get_month", 1016 | date: date, 1017 | format: "json", 1018 | }; 1019 | 1020 | const response = await this.makeApiRequest( 1021 | "GET", 1022 | this.baseUrl, 1023 | params, 1024 | true, 1025 | ); 1026 | 1027 | return { 1028 | content: [ 1029 | { 1030 | type: "text", 1031 | text: JSON.stringify(response, null, 2), 1032 | }, 1033 | ], 1034 | }; 1035 | } catch (error) { 1036 | throw new McpError( 1037 | ErrorCode.InternalError, 1038 | `Failed to get weight entries for month: ${ 1039 | error instanceof Error ? error.message : "Unknown error" 1040 | }`, 1041 | ); 1042 | } 1043 | } 1044 | 1045 | async run() { 1046 | const transport = new StdioServerTransport(); 1047 | await this.server.connect(transport); 1048 | console.error("FatSecret MCP server running on stdio"); 1049 | } 1050 | } 1051 | 1052 | const server = new FatSecretMCPServer(); 1053 | server.run().catch(console.error); 1054 | --------------------------------------------------------------------------------