UNPKG

@prsna_ai/mcp-server

Version:

Model Context Protocol server for PRSNA personality profiles and communication insights

341 lines 12.3 kB
import fetch from 'node-fetch'; import { logger } from '../utils/logger.js'; import { CONFIG, ENDPOINTS } from '../utils/constants.js'; import { CacheManager } from '../cache/cache.js'; export class PRSNAApiClient { baseUrl; token; cache; constructor(token, baseUrl) { if (!token) { throw new Error('PRSNA API token is required'); } this.baseUrl = baseUrl || CONFIG.API.BASE_URL; this.token = token; this.cache = new CacheManager(); logger.info('PRSNA API client initialized', { baseUrl: this.baseUrl, hasToken: !!token }); } /** * Authenticate and validate the token */ async authenticate() { const cacheKey = 'auth:user'; const cached = this.cache.get(cacheKey); if (cached) { logger.debug('Using cached user profile'); return cached; } try { logger.info('Authenticating with PRSNA API using MCP token'); // Use MCP token validation endpoint const response = await this.validateMCPToken(); if (!response.user) { throw new Error('Invalid authentication response: missing user profile'); } // Cache the user profile for 24 hours this.cache.set(cacheKey, response.user, CONFIG.CACHE.TTL.USER); logger.info('Authentication successful', { userId: response.user._id, name: response.user.name }); return response.user; } catch (error) { logger.error('Authentication failed', { error: error.message }); throw error; } } /** * Validate MCP token using the dedicated endpoint */ async validateMCPToken() { const url = `${this.baseUrl}${ENDPOINTS.MCP_TOKEN_VALIDATE}`; const options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'PRSNA-MCP/1.0.0' }, body: JSON.stringify({ token: this.token }), timeout: CONFIG.API.TIMEOUT }; try { logger.debug('Validating MCP token'); const response = await fetch(url, options); if (!response.ok) { const errorData = await response.text(); let errorMessage; try { const parsed = JSON.parse(errorData); errorMessage = parsed.error || parsed.message || `HTTP ${response.status}`; } catch { errorMessage = `HTTP ${response.status}: ${errorData}`; } throw new Error(`Token validation failed: ${errorMessage}`); } const data = await response.json(); if (!data.success || !data.user) { throw new Error('Invalid token validation response'); } return data; } catch (error) { logger.error('MCP token validation failed', { error: error.message }); throw error; } } /** * Validate token and get user profile */ async validateToken() { try { const user = await this.authenticate(); return { isValid: true, user }; } catch (error) { logger.error('Token validation failed', { error: error.message }); return { isValid: false }; } } /** * Get all saved profiles for the authenticated user */ async getProfiles(limit = 20, offset = 0) { const cacheKey = `profiles:${limit}:${offset}`; const cached = this.cache.get(cacheKey); if (cached) { logger.debug('Using cached profiles'); return { profiles: cached, total: cached.length, limit, offset }; } try { logger.info('Fetching saved profiles', { limit, offset }); const profiles = await this.makeRequest('GET', ENDPOINTS.SAVED_PEOPLE); // Apply pagination client-side since API doesn't support it const paginatedProfiles = profiles.slice(offset, offset + limit); // Cache the profiles for 1 hour this.cache.set(cacheKey, paginatedProfiles, CONFIG.CACHE.TTL.PROFILES); logger.info('Profiles fetched successfully', { total: profiles.length, returned: paginatedProfiles.length }); return { profiles: paginatedProfiles, total: profiles.length, limit, offset }; } catch (error) { logger.error('Failed to fetch profiles', { error: error.message }); throw error; } } /** * Get a specific profile by ID */ async getProfile(id) { const cacheKey = `profile:${id}`; const cached = this.cache.get(cacheKey); if (cached) { logger.debug('Using cached profile', { profileId: id }); return cached; } try { logger.info('Fetching profile by ID', { profileId: id }); const profile = await this.makeRequest('GET', `${ENDPOINTS.SAVED_PERSON}/${id}`); // Cache the profile for 1 hour this.cache.set(cacheKey, profile, CONFIG.CACHE.TTL.PROFILES); logger.info('Profile fetched successfully', { profileId: id, name: profile.name }); return profile; } catch (error) { logger.error('Failed to fetch profile', { profileId: id, error: error.message }); throw error; } } /** * Search profiles by query string */ async searchProfiles(query, limit = 10) { const cacheKey = `search:${query}:${limit}`; const cached = this.cache.get(cacheKey); if (cached) { logger.debug('Using cached search results', { query }); return { profiles: cached, query, total: cached.length }; } try { logger.info('Searching profiles', { query, limit }); // Get all profiles and filter client-side since no dedicated search API exists const allProfiles = await this.makeRequest('GET', ENDPOINTS.SAVED_PEOPLE); const searchTerms = query.toLowerCase().split(' '); const filteredProfiles = allProfiles.filter(profile => { const searchableText = [ profile.name, profile.jobTitle, profile.company, profile.industry || '' ].join(' ').toLowerCase(); return searchTerms.some(term => searchableText.includes(term)); }).slice(0, limit); // Cache search results for 15 minutes this.cache.set(cacheKey, filteredProfiles, CONFIG.CACHE.TTL.SEARCH); logger.info('Search completed', { query, totalProfiles: allProfiles.length, matchedProfiles: filteredProfiles.length }); return { profiles: filteredProfiles, query, total: filteredProfiles.length }; } catch (error) { logger.error('Profile search failed', { query, error: error.message }); throw error; } } /** * Find a profile by name (for @mention functionality) */ async findProfileByName(name) { try { logger.info('Finding profile by name', { name }); const searchResult = await this.searchProfiles(name, 1); if (searchResult.profiles.length === 0) { logger.info('No profile found for name', { name }); return null; } const profile = searchResult.profiles[0]; logger.info('Profile found by name', { name, foundProfile: profile.name, profileId: profile._id }); return profile; } catch (error) { logger.error('Failed to find profile by name', { name, error: error.message }); throw error; } } /** * Make HTTP request with authentication and error handling */ async makeRequest(method, endpoint, body) { const url = `${this.baseUrl}${endpoint}`; const options = { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}`, 'User-Agent': 'PRSNA-MCP/1.0.0' }, timeout: CONFIG.API.TIMEOUT }; if (body) { options.body = JSON.stringify(body); } let lastError = null; // Retry logic for (let attempt = 1; attempt <= CONFIG.API.RETRY_ATTEMPTS; attempt++) { try { logger.debug('Making API request', { method, url, attempt, maxAttempts: CONFIG.API.RETRY_ATTEMPTS }); const response = await fetch(url, options); if (!response.ok) { const errorData = await response.text(); let errorMessage; try { const parsed = JSON.parse(errorData); errorMessage = parsed.error || parsed.message || `HTTP ${response.status}`; } catch { errorMessage = errorData || `HTTP ${response.status}`; } const apiError = { code: response.status.toString(), message: errorMessage, details: { status: response.status, statusText: response.statusText } }; if (response.status === 401) { // Clear cached auth on authentication errors this.cache.delete('auth:user'); apiError.message = 'Authentication failed. Please check your PRSNA API token.'; } throw new Error(apiError.message); } const data = await response.json(); logger.debug('API request successful', { method, url, status: response.status }); return data; } catch (error) { lastError = error; if (attempt < CONFIG.API.RETRY_ATTEMPTS) { const delay = CONFIG.API.RETRY_DELAY * Math.pow(2, attempt - 1); logger.warn('API request failed, retrying', { attempt, maxAttempts: CONFIG.API.RETRY_ATTEMPTS, delay, error: error.message }); await new Promise(resolve => setTimeout(resolve, delay)); } else { logger.error('API request failed after all retries', { method, url, attempts: CONFIG.API.RETRY_ATTEMPTS, error: error.message }); } } } throw lastError || new Error('Request failed without error details'); } /** * Get cache statistics */ getCacheStats() { return this.cache.getStats(); } /** * Clear all cached data */ clearCache() { this.cache.clear(); logger.info('API client cache cleared'); } /** * Destroy the client and cleanup resources */ destroy() { this.cache.destroy(); logger.info('PRSNA API client destroyed'); } } //# sourceMappingURL=client.js.map