Skip to main content
Glama
auth.ts•8.34 kB
import axios from 'axios'; import fs from 'fs-extra'; import path from 'path'; export interface OAuthConfig { clientId: string; clientSecret: string; redirectUri: string; scope: string; authBaseUrl: string; apiBaseUrl: string; } export interface TokenResponse { access_token: string; refresh_token?: string; token_type: string; expires_in: number; scope: string; } export interface TokenInfo { accessToken: string; refreshToken?: string; expiresAt: number; scope: string; } export class EGWAuthManager { private config: OAuthConfig; private tokenFile: string; private currentToken: TokenInfo | null = null; constructor(config: OAuthConfig, tokenFile?: string) { this.config = config; this.tokenFile = tokenFile || path.join(process.cwd(), 'data', 'tokens.json'); } /** * Generate OAuth authorization URL for interactive auth */ getAuthorizationUrl(state?: string): string { const params = new URLSearchParams({ response_type: 'code', client_id: this.config.clientId, redirect_uri: this.config.redirectUri, scope: this.config.scope, ...(state && { state }) }); return `${this.config.authBaseUrl}/connect/authorize?${params.toString()}`; } /** * Exchange authorization code for access token */ async exchangeCodeForToken(code: string): Promise<TokenInfo> { try { const response = await axios.post<TokenResponse>(`${this.config.authBaseUrl}/connect/token`, new URLSearchParams({ grant_type: 'authorization_code', client_id: this.config.clientId, client_secret: this.config.clientSecret, redirect_uri: this.config.redirectUri, code: code }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'EGW-Research-Tool/1.0.0' } }); const tokenInfo: TokenInfo = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token, expiresAt: Date.now() + (response.data.expires_in * 1000), scope: response.data.scope }; await this.saveToken(tokenInfo); this.currentToken = tokenInfo; return tokenInfo; } catch (error) { throw new Error(`Failed to exchange code for token: ${error}`); } } /** * Get valid access token (refresh if needed) */ async getValidToken(): Promise<string> { // Load token if not in memory if (!this.currentToken) { await this.loadToken(); } // Check if token exists and is valid if (!this.currentToken) { // Try client credentials auth automatically console.log('šŸ” No token found, attempting client credentials authentication...'); await this.clientCredentialsAuth(); } // Check if token is expired (with 5-minute buffer) if (this.currentToken && Date.now() >= (this.currentToken.expiresAt - 300000)) { if (this.currentToken.refreshToken) { console.log('šŸ”„ Token expired, refreshing...'); await this.refreshToken(); } else { console.log('šŸ” Token expired, re-authenticating with client credentials...'); await this.clientCredentialsAuth(); } } if (!this.currentToken) { throw new Error('Failed to obtain valid authentication token'); } return this.currentToken.accessToken; } /** * Refresh access token using refresh token */ async refreshToken(): Promise<TokenInfo> { if (!this.currentToken?.refreshToken) { throw new Error('No refresh token available'); } try { const response = await axios.post<TokenResponse>(`${this.config.authBaseUrl}/connect/token`, new URLSearchParams({ grant_type: 'refresh_token', client_id: this.config.clientId, client_secret: this.config.clientSecret, refresh_token: this.currentToken.refreshToken }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'EGW-Research-Tool/1.0.0' } }); const tokenInfo: TokenInfo = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token || this.currentToken.refreshToken, expiresAt: Date.now() + (response.data.expires_in * 1000), scope: response.data.scope }; await this.saveToken(tokenInfo); this.currentToken = tokenInfo; return tokenInfo; } catch (error) { throw new Error(`Failed to refresh token: ${error}`); } } /** * Authenticate using client credentials flow */ async clientCredentialsAuth(): Promise<TokenInfo> { try { console.log('šŸ” Authenticating with client credentials...'); const response = await axios.post<TokenResponse>(`${this.config.authBaseUrl}/connect/token`, new URLSearchParams({ grant_type: 'client_credentials', client_id: this.config.clientId, client_secret: this.config.clientSecret, scope: this.config.scope }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'EGW-Research-Tool/1.0.0' } }); const tokenInfo: TokenInfo = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token, expiresAt: Date.now() + (response.data.expires_in * 1000), scope: response.data.scope }; await this.saveToken(tokenInfo); this.currentToken = tokenInfo; console.log('āœ… Client credentials authentication successful'); console.log(`šŸ•’ Token expires: ${new Date(tokenInfo.expiresAt).toISOString()}`); console.log(`šŸ”‘ Scopes: ${tokenInfo.scope}`); return tokenInfo; } catch (error) { throw new Error(`Failed to authenticate with client credentials: ${error}`); } } /** * Save token to file */ private async saveToken(token: TokenInfo): Promise<void> { await fs.ensureDir(path.dirname(this.tokenFile)); await fs.writeJson(this.tokenFile, token, { spaces: 2 }); } /** * Load token from file */ private async loadToken(): Promise<void> { try { if (await fs.pathExists(this.tokenFile)) { const savedToken = await fs.readJson(this.tokenFile); // Check if token is still valid (with 5-minute buffer) if (savedToken.expiresAt && Date.now() < (savedToken.expiresAt - 300000)) { this.currentToken = savedToken; console.log('āœ… Loaded valid token from file'); } else { console.log('āš ļø Saved token is expired'); } } } catch (error) { console.warn('Failed to load saved token:', error); this.currentToken = null; } } /** * Clear saved token */ async clearToken(): Promise<void> { this.currentToken = null; if (await fs.pathExists(this.tokenFile)) { await fs.remove(this.tokenFile); } } /** * Check if authenticated */ async isAuthenticated(): Promise<boolean> { try { await this.getValidToken(); return true; } catch { return false; } } /** * Get token info for debugging */ getTokenInfo(): TokenInfo | null { return this.currentToken; } } // Create default auth manager instance export const createAuthManager = (): EGWAuthManager => { // Check for environment variables const clientId = process.env.EGW_CLIENT_ID; const clientSecret = process.env.EGW_CLIENT_SECRET; if (!clientId || !clientSecret) { throw new Error( 'Missing EGW API credentials. Please set EGW_CLIENT_ID and EGW_CLIENT_SECRET environment variables.' ); } const config: OAuthConfig = { clientId, clientSecret, redirectUri: process.env.EGW_REDIRECT_URI || 'egw://egwwritings.oauthresponse', scope: process.env.EGW_SCOPE || 'writings search studycenter subscriptions user_info', authBaseUrl: process.env.EGW_AUTH_BASE_URL || 'https://cpanel.egwwritings.org', apiBaseUrl: process.env.EGW_API_BASE_URL || 'https://a.egwwritings.org' }; const tokenFile = process.env.EGW_TOKEN_FILE || path.join(process.cwd(), 'data', 'tokens.json'); return new EGWAuthManager(config, tokenFile); };

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/pythondev-pro/egw_writings_mcp_server'

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