Skip to main content
Glama
wassengerhq

Wassenger WhatsApp MCP Server

by wassengerhq
coordination.ts10.4 kB
import { checkLockfile, createLockfile, deleteLockfile, getConfigFilePath, LockfileData } from './mcp-auth-config' import { EventEmitter } from 'events' import { Server } from 'http' import express from 'express' import { AddressInfo } from 'net' import { unlinkSync } from 'fs' import { log, debugLog, DEBUG, setupOAuthCallbackServerWithLongPoll } from './utils' export type AuthCoordinator = { initializeAuth: () => Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }> } /** * Checks if a process with the given PID is running * @param pid The process ID to check * @returns True if the process is running, false otherwise */ export async function isPidRunning(pid: number): Promise<boolean> { try { process.kill(pid, 0) // Doesn't kill the process, just checks if it exists if (DEBUG) debugLog(`Process ${pid} is running`) return true } catch (err) { if (DEBUG) debugLog(`Process ${pid} is not running`, err) return false } } /** * Checks if a lockfile is valid (process running and endpoint accessible) * @param lockData The lockfile data * @returns True if the lockfile is valid, false otherwise */ export async function isLockValid(lockData: LockfileData): Promise<boolean> { if (DEBUG) debugLog('Checking if lockfile is valid', lockData) // Check if the lockfile is too old (over 30 minutes) const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) { log('Lockfile is too old') if (DEBUG) debugLog('Lockfile is too old', { age: Date.now() - lockData.timestamp, maxAge: MAX_LOCK_AGE, }) return false } // Check if the process is still running if (!(await isPidRunning(lockData.pid))) { log('Process from lockfile is not running') if (DEBUG) debugLog('Process from lockfile is not running', { pid: lockData.pid }) return false } // Check if the endpoint is accessible try { if (DEBUG) debugLog('Checking if endpoint is accessible', { port: lockData.port }) const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 1000) const response = await fetch(`http://127.0.0.1:${lockData.port}/wait-for-auth?poll=false`, { signal: controller.signal, }) clearTimeout(timeout) const isValid = response.status === 200 || response.status === 202 if (DEBUG) debugLog(`Endpoint check result: ${isValid ? 'valid' : 'invalid'}`, { status: response.status }) return isValid } catch (error) { log(`Error connecting to auth server: ${(error as Error).message}`) if (DEBUG) debugLog('Error connecting to auth server', error) return false } } /** * Waits for authentication from another server instance * @param port The port to connect to * @returns True if authentication completed successfully, false otherwise */ export async function waitForAuthentication(port: number): Promise<boolean> { log(`Waiting for authentication from the server on port ${port}...`) try { let attempts = 0 while (true) { attempts++ const url = `http://127.0.0.1:${port}/wait-for-auth` log(`Querying: ${url}`) if (DEBUG) debugLog(`Poll attempt ${attempts}`) try { const response = await fetch(url) if (DEBUG) debugLog(`Poll response status: ${response.status}`) if (response.status === 200) { // Auth completed, but we don't return the code anymore log(`Authentication completed by other instance`) return true } else if (response.status === 202) { // Continue polling log(`Authentication still in progress`) if (DEBUG) debugLog(`Will retry in 1s`) await new Promise((resolve) => setTimeout(resolve, 1000)) } else { log(`Unexpected response status: ${response.status}`) return false } } catch (fetchError) { if (DEBUG) debugLog(`Fetch error during poll`, fetchError) // If we can't connect, we'll try again after a delay await new Promise((resolve) => setTimeout(resolve, 2000)) } } } catch (error) { log(`Error waiting for authentication: ${(error as Error).message}`) if (DEBUG) debugLog(`Error waiting for authentication`, error) return false } } /** * Creates a lazy auth coordinator that will only initiate auth when needed * @param serverUrlHash The hash of the server URL * @param callbackPort The port to use for the callback server * @param events The event emitter to use for signaling * @returns An AuthCoordinator object with an initializeAuth method */ export function createLazyAuthCoordinator(serverUrlHash: string, callbackPort: number, events: EventEmitter): AuthCoordinator { let authState: { server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean } | null = null return { initializeAuth: async () => { // If auth has already been initialized, return the existing state if (authState) { if (DEBUG) debugLog('Auth already initialized, reusing existing state') return authState } log('Initializing auth coordination on-demand') if (DEBUG) debugLog('Initializing auth coordination on-demand', { serverUrlHash, callbackPort }) // Initialize auth using the existing coordinateAuth logic authState = await coordinateAuth(serverUrlHash, callbackPort, events) if (DEBUG) debugLog('Auth coordination completed', { skipBrowserAuth: authState.skipBrowserAuth }) return authState }, } } /** * Coordinates authentication between multiple instances of the client/proxy * @param serverUrlHash The hash of the server URL * @param callbackPort The port to use for the callback server * @param events The event emitter to use for signaling * @returns An object with the server, waitForAuthCode function, and a flag indicating if browser auth can be skipped */ export async function coordinateAuth( serverUrlHash: string, callbackPort: number, events: EventEmitter, ): Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean }> { if (DEBUG) debugLog('Coordinating authentication', { serverUrlHash, callbackPort }) // Check for a lockfile (disabled on Windows for the time being) const lockData = process.platform === 'win32' ? null : await checkLockfile(serverUrlHash) if (DEBUG) { if (process.platform === 'win32') { debugLog('Skipping lockfile check on Windows') } else { debugLog('Lockfile check result', { found: !!lockData, lockData }) } } // If there's a valid lockfile, try to use the existing auth process if (lockData && (await isLockValid(lockData))) { log(`Another instance is handling authentication on port ${lockData.port} (pid: ${lockData.pid})`) try { // Try to wait for the authentication to complete if (DEBUG) debugLog('Waiting for authentication from other instance') const authCompleted = await waitForAuthentication(lockData.port) if (authCompleted) { log('Authentication completed by another instance. Using tokens from disk') // Setup a dummy server - the client will use tokens directly from disk const dummyServer = express().listen(0) // Listen on any available port const dummyPort = (dummyServer.address() as AddressInfo).port if (DEBUG) debugLog('Started dummy server', { port: dummyPort }) // This shouldn't actually be called in normal operation, but provide it for API compatibility const dummyWaitForAuthCode = () => { log('WARNING: waitForAuthCode called in secondary instance - this is unexpected') // Return a promise that never resolves - the client should use the tokens from disk instead return new Promise<string>(() => {}) } return { server: dummyServer, waitForAuthCode: dummyWaitForAuthCode, skipBrowserAuth: true, } } else { log('Taking over authentication process...') } } catch (error) { log(`Error waiting for authentication: ${error}`) if (DEBUG) debugLog('Error waiting for authentication', error) } // If we get here, the other process didn't complete auth successfully if (DEBUG) debugLog('Other instance did not complete auth successfully, deleting lockfile') await deleteLockfile(serverUrlHash) } else if (lockData) { // Invalid lockfile, delete it log('Found invalid lockfile, deleting it') await deleteLockfile(serverUrlHash) } // Create our own lockfile if (DEBUG) debugLog('Setting up OAuth callback server', { port: callbackPort }) const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll({ port: callbackPort, path: '/oauth/callback', events, }) // Get the actual port the server is running on const address = server.address() as AddressInfo const actualPort = address.port if (DEBUG) debugLog('OAuth callback server running', { port: actualPort }) log(`Creating lockfile for server ${serverUrlHash} with process ${process.pid} on port ${actualPort}`) await createLockfile(serverUrlHash, process.pid, actualPort) // Make sure lockfile is deleted on process exit const cleanupHandler = async () => { try { log(`Cleaning up lockfile for server ${serverUrlHash}`) await deleteLockfile(serverUrlHash) } catch (error) { log(`Error cleaning up lockfile: ${error}`) if (DEBUG) debugLog('Error cleaning up lockfile', error) } } process.once('exit', () => { try { // Synchronous version for 'exit' event since we can't use async here const configPath = getConfigFilePath(serverUrlHash, 'lock.json') unlinkSync(configPath) if (DEBUG) console.error(`[DEBUG] Removed lockfile on exit: ${configPath}`) } catch (error) { if (DEBUG) console.error(`[DEBUG] Error removing lockfile on exit:`, error) } }) // Also handle SIGINT separately process.once('SIGINT', async () => { if (DEBUG) debugLog('Received SIGINT signal, cleaning up') await cleanupHandler() }) if (DEBUG) debugLog('Auth coordination complete, returning primary instance handlers') return { server, waitForAuthCode, skipBrowserAuth: false, } }

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/wassengerhq/mcp-wassenger'

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