@prsna_ai/mcp-server
Version:
Model Context Protocol server for PRSNA personality profiles and communication insights
341 lines • 12.3 kB
JavaScript
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