Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
oauth-helper.mjsโ€ข12.8 kB
#!/usr/bin/env node /** * OAuth Helper Process - Standalone OAuth polling script * * This script runs independently of the MCP server to handle OAuth device flow polling. * It's spawned as a detached process when authentication is initiated, polls GitHub * for the OAuth token, stores it securely, and then exits. * * Usage: node oauth-helper.mjs <device_code> <interval> <expires_in> <client_id> * * This solves the MCP server lifecycle issue where the server may shut down * between tool calls, breaking background OAuth polling. */ import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import fs from 'fs/promises'; import fsSync from 'fs'; import { homedir } from 'os'; // Get the directory of this script const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Constants const DEFAULT_POLL_INTERVAL = 5; const DEFAULT_EXPIRES_IN = 900; // 15 minutes const MAX_TOKEN_SIZE = 10000; // Maximum reasonable token size // Parse command line arguments const args = process.argv.slice(2); if (args.length < 4) { console.error('Usage: oauth-helper.mjs <device_code> <interval> <expires_in> <client_id>'); process.exit(1); } const [deviceCode, intervalStr, expiresInStr, clientId] = args; const pollInterval = parseInt(intervalStr, 10) || DEFAULT_POLL_INTERVAL; const expiresIn = parseInt(expiresInStr, 10) || DEFAULT_EXPIRES_IN; // Validate client ID is provided (no hardcoded fallback) if (!clientId || clientId === 'undefined') { console.error('OAUTH_HELPER_43: Missing or undefined client ID'); console.error('โš ๏ธ GitHub OAuth Configuration Missing\n'); console.error('The server administrator needs to configure GitHub OAuth.'); console.error('Please contact your administrator to set up the DOLLHOUSE_GITHUB_CLIENT_ID.'); console.error('\nFor administrators: Set the environment variable before starting the server.'); await log('OAUTH_HELPER_43: Process exiting - missing client ID'); process.exit(1); } // Log file for debugging (optional, can be disabled in production) const LOG_FILE = join(homedir(), '.dollhouse', 'oauth-helper.log'); const LOG_ENABLED = process.env.DOLLHOUSE_OAUTH_DEBUG === 'true'; async function log(message) { if (!LOG_ENABLED) return; try { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; // Ensure directory exists with secure permissions const logDir = dirname(LOG_FILE); await fs.mkdir(logDir, { recursive: true, mode: 0o700 }).catch(() => {}); // Check if log file exists let fileExists = false; try { await fs.access(LOG_FILE); fileExists = true; } catch { fileExists = false; } // Append to log file await fs.appendFile(LOG_FILE, logMessage); // Set secure permissions on first write if (!fileExists) { await fs.chmod(LOG_FILE, 0o600); } } catch (error) { // Silently fail if logging doesn't work } } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function pollGitHub(deviceCode, clientId) { const TOKEN_URL = 'https://github.com/login/oauth/access_token'; try { const response = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: clientId, device_code: deviceCode, grant_type: 'urn:ietf:params:oauth:grant-type:device_code' }) }); const data = await response.json(); return data; } catch (error) { await log(`Network error polling GitHub: ${error.message}`); throw error; } } async function storeToken(token) { // Validate token size to prevent DoS if (!token || token.length > MAX_TOKEN_SIZE) { await log('Invalid token size'); throw new Error('Invalid token received'); } try { // Import the compiled TokenManager const { TokenManager } = await import('./dist/security/tokenManager.js'); // Store the token using the secure storage mechanism await TokenManager.storeGitHubToken(token); await log('Token stored successfully using TokenManager'); return true; } catch (error) { await log(`Failed to store token using TokenManager: ${error.message}`); // Fallback: Write to a temporary file for the MCP server to pick up try { const tempTokenFile = join(homedir(), '.dollhouse', '.auth', 'pending_token.txt'); const tempDir = dirname(tempTokenFile); // Create directory with secure permissions await fs.mkdir(tempDir, { recursive: true, mode: 0o700 }); // Verify directory permissions const dirStats = await fs.stat(tempDir); const dirMode = dirStats.mode & parseInt('777', 8); if (dirMode !== parseInt('700', 8)) { await fs.chmod(tempDir, 0o700); } // Write token with secure permissions await fs.writeFile(tempTokenFile, token, { mode: 0o600 }); // Verify file permissions await fs.chmod(tempTokenFile, 0o600); await log(`Token written to fallback file with secure permissions`); return true; } catch (fallbackError) { await log(`Fallback storage also failed: ${fallbackError.message}`); throw fallbackError; } } } function cleanupPidFileSync() { try { const pidFile = join(homedir(), '.dollhouse', '.auth', 'oauth-helper.pid'); if (fsSync.existsSync(pidFile)) { fsSync.unlinkSync(pidFile); } } catch (error) { // Ignore cleanup errors } } async function cleanupPidFile() { try { const pidFile = join(homedir(), '.dollhouse', '.auth', 'oauth-helper.pid'); await fs.unlink(pidFile).catch(() => {}); await log('PID file cleaned up'); } catch (error) { // Ignore cleanup errors } } async function writePidFile() { try { const pidFile = join(homedir(), '.dollhouse', '.auth', 'oauth-helper.pid'); const pidDir = dirname(pidFile); await fs.mkdir(pidDir, { recursive: true, mode: 0o700 }); await fs.writeFile(pidFile, process.pid.toString(), { mode: 0o600 }); await log(`PID file written: ${pidFile}`); } catch (error) { await log(`Failed to write PID file: ${error.message}`); } } async function main() { await log(`[START] OAuth helper started - PID: ${process.pid}`); await log(`[CONFIG] Device code: ${deviceCode.substring(0, 2)}****`); // More aggressive truncation await log(`[CONFIG] Poll interval: ${pollInterval}s, Expires in: ${expiresIn}s`); await log(`[CONFIG] Node version: ${process.version}`); await log(`[CONFIG] Platform: ${process.platform}`); // Never log client ID // Write PID file for tracking await writePidFile(); // Write initial heartbeat let lastHeartbeat = Date.now(); const heartbeatInterval = setInterval(async () => { await log(`[HEARTBEAT] Process alive - Memory: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`); lastHeartbeat = Date.now(); }, 30000); // Every 30 seconds const startTime = Date.now(); const timeout = startTime + (expiresIn * 1000); let attempts = 0; let consecutiveErrors = 0; const MAX_CONSECUTIVE_ERRORS = 5; // Set up cleanup on exit - use synchronous cleanup for exit event process.on('exit', () => { cleanupPidFileSync(); }); // Use beforeExit for async cleanup when possible process.on('beforeExit', async () => { await log('OAuth helper completing cleanup'); await cleanupPidFile(); }); process.on('SIGINT', () => { cleanupPidFileSync(); process.exit(0); }); process.on('SIGTERM', () => { cleanupPidFileSync(); process.exit(0); }); while (Date.now() < timeout) { attempts++; const timeElapsed = Math.round((Date.now() - startTime) / 1000); await log(`[POLL] Attempt ${attempts} at ${timeElapsed}s elapsed...`); try { const response = await pollGitHub(deviceCode, clientId); if (response.error) { switch (response.error) { case 'authorization_pending': // User hasn't authorized yet, keep polling await log('[STATUS] Authorization pending, user has not authorized yet...'); break; case 'slow_down': // GitHub is asking us to slow down await log(`[RATE_LIMIT] GitHub requested slower polling - increasing interval to ${pollInterval * 1.5}s`); await sleep(pollInterval * 1500); continue; case 'expired_token': await log('OAUTH_HELPER_264: Device code expired - authentication window closed'); console.error('OAUTH_EXPIRED: Device code expired at line 264 - authentication window closed'); clearInterval(heartbeatInterval); await cleanupPidFile(); process.exit(1); case 'access_denied': await log('OAUTH_HELPER_270: User denied authorization request'); console.error('OAUTH_ACCESS_DENIED: User denied authorization at line 270'); clearInterval(heartbeatInterval); await cleanupPidFile(); process.exit(1); default: await log(`OAUTH_HELPER_276: Unknown error from GitHub: ${response.error}`); await log(`[ERROR] Error description: ${response.error_description}`); console.error(`OAUTH_UNKNOWN_RESPONSE: Unknown error '${response.error}' at line 276`); } } else if (response.access_token) { // Success! We got the token await log('[SUCCESS] โœ… Token received from GitHub!'); consecutiveErrors = 0; // Reset error counter // Store the token const stored = await storeToken(response.access_token); if (stored) { await log('[SUCCESS] โœ… OAuth authentication completed successfully'); await log(`[STATS] Total attempts: ${attempts}, Time elapsed: ${Math.round((Date.now() - startTime) / 1000)}s`); console.log('โœ… GitHub authentication successful! Token has been stored.'); clearInterval(heartbeatInterval); await cleanupPidFile(); process.exit(0); } else { await log('[ERROR] โŒ Failed to store token'); console.error('โŒ Failed to store authentication token'); clearInterval(heartbeatInterval); await cleanupPidFile(); process.exit(1); } } else { // Reset error counter on successful communication consecutiveErrors = 0; } } catch (error) { await log(`[ERROR] Polling error: ${error.message}`); // Classify error types const isNetworkError = error.message && ( error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT') || error.message.includes('ENOTFOUND') || error.message.includes('EAI_AGAIN') || error.message.includes('fetch failed') ); if (isNetworkError) { consecutiveErrors++; await log(`OAUTH_HELPER_319: Network error ${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}`); if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { await log('OAUTH_HELPER_323: Too many consecutive network errors, exiting'); console.error(`OAUTH_NETWORK_FAILURE: Too many network errors (${MAX_CONSECUTIVE_ERRORS}) at line 323 - check internet connection`); clearInterval(heartbeatInterval); await cleanupPidFile(); process.exit(1); } } else { // Non-network error, likely fatal await log(`OAUTH_HELPER_330: Non-recoverable error: ${error.message}`); console.error(`OAUTH_FATAL_ERROR: Non-recoverable error at line 330 - ${error.message}`); clearInterval(heartbeatInterval); await cleanupPidFile(); process.exit(1); } } // Wait before next poll await sleep(pollInterval * 1000); } // Timeout reached await log('OAUTH_HELPER_342: OAuth authorization timed out'); await log(`[STATS] Total attempts: ${attempts}, Time elapsed: ${Math.round((Date.now() - startTime) / 1000)}s`); console.error(`OAUTH_TIMEOUT: Authorization timed out at line 342 after ${Math.round((Date.now() - startTime) / 1000)}s - user did not authorize in time`); clearInterval(heartbeatInterval); await cleanupPidFile(); process.exit(1); } // Run the main function main().catch(async (error) => { await log(`Fatal error: ${error.message}`); console.error('Fatal error in OAuth helper:', error); await cleanupPidFile(); process.exit(1); });

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/DollhouseMCP/DollhouseMCP'

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