UNPKG

@prsna_ai/mcp-server

Version:

Model Context Protocol server for PRSNA personality profiles and communication insights

235 lines 9.88 kB
import { z } from 'zod'; import { logger } from '../utils/logger.js'; import { PRSNAApiClient } from '../api/client.js'; import { safeJSONStringify, cleanObject } from '../utils/json.js'; // Input schema const AnalyzeLinkedInSchema = z.object({ linkedInUrl: z.string().min(1, 'LinkedIn URL is required'), save: z.boolean().optional().default(true), update: z.boolean().optional().default(false), notes: z.string().optional() }); // Helper to get API client with token from environment function getApiClient() { const token = process.env.PRSNA_API_TOKEN; if (!token) { throw new Error('PRSNA_API_TOKEN environment variable is required. Please authenticate first.'); } return new PRSNAApiClient(token); } // Helper to format dimensions for display function formatDimension(dim) { const dimensionNames = { expression: 'Expression', reasoning: 'Reasoning', flexibility: 'Flexibility', work_style: 'Work Style', tempo: 'Tempo' }; const name = dimensionNames[dim.dimension] || dim.dimension; return `${name}: ${dim.dominant_pole} (${dim.confidence_percentage}% confidence)`; } export async function analyzeLinkedInProfile(args) { try { const { linkedInUrl, save, update, notes } = AnalyzeLinkedInSchema.parse(args); logger.info('Analyzing LinkedIn profile', { linkedInUrl, save, update }); const client = getApiClient(); const response = await client.analyzeLinkedInProfile(linkedInUrl, save, update, notes); client.destroy(); // Handle case where profile already exists if (response.exists && response.existingProfile) { const result = { success: true, exists: true, message: response.message, existingProfile: { id: response.existingProfile.id, name: response.existingProfile.name, jobTitle: response.existingProfile.jobTitle, company: response.existingProfile.company, analyzedAt: response.existingProfile.analyzedAt }, hint: 'To re-analyze this profile with fresh data, call again with update=true' }; // Build formatted text response const textResponse = [ `📋 **Profile Already Saved**`, ``, `**${response.existingProfile.name}**`, `${response.existingProfile.jobTitle} at ${response.existingProfile.company}`, ``, `Last analyzed: ${new Date(response.existingProfile.analyzedAt).toLocaleDateString()}`, ``, `This profile is already in your PRSNA database. To re-analyze with fresh data, use update=true.` ].join('\n'); return { content: [ { type: "text", text: textResponse }, { type: "text", text: safeJSONStringify(cleanObject(result), 2) } ] }; } // Handle successful analysis if (response.profile) { const profile = response.profile; // Build formatted text response const lines = [ `✅ **LinkedIn Profile Analyzed${response.saved ? ' & Saved' : ''}**`, ``, `## ${profile.name}`, `**${profile.jobTitle}** at **${profile.company}**`, profile.location ? `📍 ${profile.location}` : null, profile.industry ? `🏢 ${profile.industry}` : null, ``, `---`, ``, `## Personality Profile`, ``, profile.personalitySummary, ``, `### Key Traits`, ...(profile.keyTraits || []).map(trait => `• ${trait}`), ``, `### Personality Dimensions`, ...(profile.dimensions || []).map(dim => `• ${formatDimension(dim)}`), ``, `### Operating Style`, profile.operatingStyle, ``, `---`, ``, `## Communication Guide`, ``, `### Preferences`, profile.communicationPreferences, ``, `### Recommendations` ].filter(Boolean); // Add communication recommendations if (profile.communicationRecommendations && profile.communicationRecommendations.length > 0) { for (const rec of profile.communicationRecommendations) { lines.push(`**${rec.context}** (${rec.priority} priority)`); lines.push(`${rec.recommendation}`); lines.push(''); } } // Add rate limit info if available if (response.rateLimitInfo) { lines.push(`---`); lines.push(`📊 Analysis usage: ${response.rateLimitInfo.used}/${response.rateLimitInfo.limit} this month (${response.rateLimitInfo.remaining} remaining)`); } if (response.saved && profile.id) { lines.push(``, `✓ Profile saved to your PRSNA database (ID: ${profile.id})`); } const textResponse = lines.join('\n'); // Build structured result const result = { success: true, saved: response.saved, profile: { id: profile.id, name: profile.name, jobTitle: profile.jobTitle, company: profile.company, linkedInUrl: profile.linkedInUrl, industry: profile.industry, location: profile.location, dimensions: profile.dimensions, personalitySummary: profile.personalitySummary, keyTraits: profile.keyTraits, operatingStyle: profile.operatingStyle, communicationPreferences: profile.communicationPreferences, communicationRecommendations: profile.communicationRecommendations }, rateLimitInfo: response.rateLimitInfo, message: response.message }; logger.info('LinkedIn analysis returned', { profileName: profile.name, saved: response.saved, profileId: profile.id }); return { content: [ { type: "text", text: textResponse }, { type: "text", text: safeJSONStringify(cleanObject(result), 2) } ] }; } // Fallback for unexpected response return { content: [{ type: "text", text: safeJSONStringify({ success: false, message: response.message || 'Unexpected response from analysis' }, 2) }] }; } catch (error) { const errorMessage = error.message; logger.error('Failed to analyze LinkedIn profile', { error: errorMessage }); // Provide helpful error messages let userMessage = 'Failed to analyze LinkedIn profile'; if (errorMessage.includes('PROFILE_NOT_FOUND') || errorMessage.includes('not found')) { userMessage = 'LinkedIn profile not found. Please check the URL and try again.'; } else if (errorMessage.includes('RATE_LIMIT') || errorMessage.includes('rate limit')) { userMessage = 'Monthly analysis limit reached. Upgrade your plan for more analyses.'; } else if (errorMessage.includes('Invalid LinkedIn URL')) { userMessage = 'Invalid LinkedIn URL. Please provide a valid URL (e.g., https://linkedin.com/in/username)'; } return { content: [{ type: "text", text: safeJSONStringify({ success: false, message: userMessage, error: errorMessage }, 2) }] }; } } // Tool definition for MCP export const linkedinTool = { name: "analyze_linkedin_profile", description: "Analyze a LinkedIn profile URL to generate personality insights. Fetches the profile data, runs PRSNA's 5-dimension personality analysis, and optionally saves to your database for future reference. Analysis takes 10-30 seconds.", inputSchema: { type: "object", properties: { linkedInUrl: { type: "string", description: "LinkedIn profile URL (e.g., https://linkedin.com/in/username)" }, save: { type: "boolean", description: "Save the profile to your PRSNA database for future reference (default: true)" }, update: { type: "boolean", description: "Re-analyze and update if the profile already exists in your database (default: false)" }, notes: { type: "string", description: "Optional personal notes about this person to save with their profile" } }, required: ["linkedInUrl"] } }; //# sourceMappingURL=linkedin.js.map