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