Skip to main content
Glama
setup.ts8.51 kB
#!/usr/bin/env node import { createInterface } from 'readline/promises'; import { writeFile, readFile } from 'fs/promises'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const envPath = join(__dirname, '..', '.env'); const rl = createInterface({ input: process.stdin, output: process.stdout, }); async function prompt(question: string): Promise<string> { const answer = await rl.question(question); return answer.trim(); } async function exchangeToken( clientId: string, clientSecret: string, code: string ): Promise<{ access_token: string; refresh_token: string; expires_at: number }> { const response = await fetch('https://www.strava.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code, grant_type: 'authorization_code', }), }); if (!response.ok) { const error = await response.json().catch(() => ({ message: response.statusText })); throw new Error(`Failed to exchange token: ${error.message || response.statusText}`); } return response.json(); } async function loadExistingEnv(): Promise<Record<string, string>> { try { const content = await readFile(envPath, 'utf-8'); const env: Record<string, string> = {}; content.split('\n').forEach((line) => { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const [key, ...valueParts] = trimmed.split('='); if (key && valueParts.length > 0) { env[key.trim()] = valueParts.join('=').trim(); } } }); return env; } catch { return {}; } } async function writeEnvFile(env: Record<string, string>): Promise<void> { const lines = [ '# Strava API Credentials', '# Get these from: https://www.strava.com/settings/api', `STRAVA_CLIENT_ID=${env.STRAVA_CLIENT_ID}`, `STRAVA_CLIENT_SECRET=${env.STRAVA_CLIENT_SECRET}`, '', '# OAuth Tokens', '# These are obtained after completing the OAuth flow', `STRAVA_ACCESS_TOKEN=${env.STRAVA_ACCESS_TOKEN}`, `STRAVA_REFRESH_TOKEN=${env.STRAVA_REFRESH_TOKEN}`, `STRAVA_EXPIRES_AT=${env.STRAVA_EXPIRES_AT}`, '', '# OAuth Scope (for reference)', `# STRAVA_SCOPE=${env.STRAVA_SCOPE || 'read,activity:read_all,profile:read_all'}`, '', ]; await writeFile(envPath, lines.join('\n'), 'utf-8'); } async function main(): Promise<void> { console.log('='.repeat(60)); console.log('Strava MCP Server - OAuth Setup'); console.log('='.repeat(60)); console.log(''); // Load existing environment const existingEnv = await loadExistingEnv(); // Get Client ID let clientId = existingEnv.STRAVA_CLIENT_ID || ''; if (!clientId) { console.log('First, you need to create a Strava application:'); console.log('1. Visit: https://www.strava.com/settings/api'); console.log('2. Create a new application'); console.log('3. For "Authorization Callback Domain" use: localhost'); console.log(''); clientId = await prompt('Enter your Strava Client ID: '); } else { console.log(`Using existing Client ID: ${clientId}`); const useExisting = await prompt('Keep this Client ID? (y/n): '); if (useExisting.toLowerCase() !== 'y') { clientId = await prompt('Enter your Strava Client ID: '); } } // Get Client Secret let clientSecret = existingEnv.STRAVA_CLIENT_SECRET || ''; if (!clientSecret) { clientSecret = await prompt('Enter your Strava Client Secret: '); } else { console.log('Using existing Client Secret: ********'); const useExisting = await prompt('Keep this Client Secret? (y/n): '); if (useExisting.toLowerCase() !== 'y') { clientSecret = await prompt('Enter your Strava Client Secret: '); } } // Get scope selection console.log(''); console.log('='.repeat(60)); console.log('Permission Scope Selection'); console.log('='.repeat(60)); console.log(''); console.log('Choose the permissions you want to grant:'); console.log(''); console.log('1. Minimal (recommended for troubleshooting)'); console.log(' - Basic read access only'); console.log(' Scope: read,activity:read'); console.log(''); console.log('2. Read-only (full)'); console.log(' - View all activities, athlete data, routes, segments, clubs, gear'); console.log(' - Cannot create, update, or delete activities'); console.log(' Scope: read,activity:read_all,profile:read_all'); console.log(''); console.log('3. Read + Write'); console.log(' - Everything in Read-only'); console.log(' - Can create, update, and delete activities'); console.log(' Scope: read,activity:read_all,activity:write,profile:read_all'); console.log(''); const scopeChoice = await prompt('Enter your choice (1, 2, or 3): '); let scope: string; if (scopeChoice === '1') { scope = 'read,activity:read'; console.log('✓ Selected: Minimal permissions'); } else if (scopeChoice === '2') { scope = 'read,activity:read_all,profile:read_all'; console.log('✓ Selected: Read-only (full) permissions'); } else if (scopeChoice === '3') { scope = 'read,activity:read_all,activity:write,profile:read_all'; console.log('✓ Selected: Read + Write permissions'); } else { console.log('Invalid choice. Defaulting to Minimal permissions.'); scope = 'read,activity:read'; } console.log(''); console.log('='.repeat(60)); console.log('OAuth Authorization'); console.log('='.repeat(60)); console.log(''); console.log('IMPORTANT: Make sure your Strava app Authorization Callback Domain'); console.log('matches one of the following:'); console.log(' - localhost'); console.log(' - localhost:8080'); console.log(''); console.log('You can verify this at: https://www.strava.com/settings/api'); console.log(''); // Generate authorization URL - try without protocol first const redirectUri = 'http://localhost'; const authUrl = `https://www.strava.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&approval_prompt=auto`; console.log('Step 1: Visit this URL in your browser:'); console.log(''); console.log(authUrl); console.log(''); console.log('Step 2: Authorize the application'); console.log(''); console.log('Step 3: You will be redirected to a URL like:'); console.log('http://localhost/?state=&code=XXXXXX&scope=...'); console.log(''); console.log('NOTE: The page will show "This site can\'t be reached" - that\'s OK!'); console.log('Just copy the URL from your browser address bar.'); console.log(''); const code = await prompt('Paste the authorization code from the URL (the part after "code="): '); console.log(''); console.log('Exchanging authorization code for tokens...'); try { const tokens = await exchangeToken(clientId, clientSecret, code); console.log(''); console.log('✓ Successfully obtained tokens!'); console.log(''); // Save to .env file await writeEnvFile({ STRAVA_CLIENT_ID: clientId, STRAVA_CLIENT_SECRET: clientSecret, STRAVA_ACCESS_TOKEN: tokens.access_token, STRAVA_REFRESH_TOKEN: tokens.refresh_token, STRAVA_EXPIRES_AT: tokens.expires_at.toString(), STRAVA_SCOPE: scope, }); console.log(`✓ Credentials saved to: ${envPath}`); console.log(''); console.log('='.repeat(60)); console.log('Setup Complete!'); console.log('='.repeat(60)); console.log(''); console.log('You can now:'); console.log('1. Build the server: npm run build'); console.log('2. Configure it in Claude Desktop'); console.log(''); console.log('See README.md for Claude Desktop configuration instructions.'); console.log(''); } catch (error) { console.error(''); console.error('✗ Failed to exchange token:'); console.error(error instanceof Error ? error.message : String(error)); console.error(''); console.error('Please check:'); console.error('- Your Client ID and Secret are correct'); console.error('- The authorization code is valid (they expire quickly!)'); console.error('- You copied the entire code from the URL'); process.exit(1); } finally { rl.close(); } } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

Latest Blog Posts

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/gcoombe/strava-mcp'

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