@prsna_ai/mcp-server
Version:
Model Context Protocol server for PRSNA personality profiles and communication insights
278 lines • 11.3 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 MentionPersonSchema = z.object({
name: z.string().min(1, 'Name is required')
});
// Helper to get API client
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);
}
export async function mentionPerson(args) {
try {
const { name } = MentionPersonSchema.parse(args);
logger.info('Looking up person for mention', { name });
const client = getApiClient();
const profile = await client.findProfileByName(name);
client.destroy();
if (!profile) {
return {
content: [{
type: "text",
text: safeJSONStringify({
success: false,
message: `No profile found for "${name}". Try searching with a partial name or check spelling.`,
suggestions: [
'Try searching with just the first name',
'Check for alternative spellings',
'Use the search_personality_profiles tool for broader matching'
]
}, 2)
}]
};
}
// Return condensed profile info optimized for mention context
const mentionContext = {
success: true,
profile: {
id: profile._id,
name: profile.name,
jobTitle: profile.jobTitle,
company: profile.company,
// Quick personality snapshot
personalitySnapshot: {
summary: profile.personalitySummary || 'No summary available',
keyTraits: profile.keyTraits?.slice(0, 3) || [],
dominantDimensions: extractDominantDimensions(profile.dimensions)
},
// Essential communication context
communicationEssentials: {
style: extractCommunicationStyle(profile),
quickTips: generateQuickTips(profile),
preferredApproach: extractPreferredApproach(profile)
},
// Context for immediate use
contextForMention: {
roleContext: `${profile.jobTitle} at ${profile.company}`,
whenToMention: generateWhenToMention(profile),
howToEngage: generateHowToEngage(profile)
},
// Metadata
lastUpdated: profile.updatedAt,
profileCompleteness: calculateProfileCompleteness(profile)
}
};
logger.info('Person found for mention', {
name,
foundProfile: profile.name,
profileId: profile._id
});
return {
content: [{
type: "text",
text: safeJSONStringify(cleanObject(mentionContext), 2)
}]
};
}
catch (error) {
const errorMessage = error.message;
logger.error('Failed to lookup person for mention', { name: args, error: errorMessage });
return {
content: [{
type: "text",
text: safeJSONStringify({
success: false,
message: 'Failed to lookup person',
error: errorMessage
}, 2)
}]
};
}
}
// Helper functions for generating mention-optimized context
function extractDominantDimensions(dimensions) {
return dimensions
.filter(dim => dim.confidence_percentage >= 70) // Only high-confidence dimensions
.sort((a, b) => b.confidence_percentage - a.confidence_percentage)
.slice(0, 2) // Top 2 most confident dimensions
.map(dim => ({
dimension: dim.dimension,
pole: dim.dominant_pole,
strength: dim.confidence_percentage >= 85 ? 'strong' : 'moderate'
}));
}
function extractCommunicationStyle(profile) {
const dimensions = profile.dimensions || [];
// Find key communication-related dimensions
const expression = dimensions.find((d) => d.dimension === 'expression');
const reasoning = dimensions.find((d) => d.dimension === 'reasoning');
const tempo = dimensions.find((d) => d.dimension === 'tempo');
const styles = [];
if (expression) {
styles.push(expression.dominant_pole === 'Internal' ? 'Reflective' : 'Interactive');
}
if (reasoning) {
styles.push(reasoning.dominant_pole === 'Logic' ? 'Analytical' : 'People-focused');
}
if (tempo) {
styles.push(tempo.dominant_pole === 'Fast-Paced' ? 'Quick-paced' : 'Thoughtful');
}
return styles.length > 0 ? styles.join(', ') : 'Professional';
}
function generateQuickTips(profile) {
const tips = [];
const dimensions = profile.dimensions || [];
dimensions.forEach((dim) => {
if (dim.confidence_percentage >= 75) { // Only include high-confidence tips
switch (dim.dimension) {
case 'expression':
if (dim.dominant_pole === 'Internal') {
tips.push('💭 Give processing time');
}
else {
tips.push('🗣️ Engage in discussion');
}
break;
case 'reasoning':
if (dim.dominant_pole === 'Logic') {
tips.push('📊 Present data/facts');
}
else {
tips.push('❤️ Consider people impact');
}
break;
case 'flexibility':
if (dim.dominant_pole === 'Structured') {
tips.push('📋 Provide clear agendas');
}
else {
tips.push('🔄 Stay flexible');
}
break;
case 'work_style':
if (dim.dominant_pole === 'Process-Driven') {
tips.push('⚙️ Follow procedures');
}
else {
tips.push('🎯 Focus on outcomes');
}
break;
case 'tempo':
if (dim.dominant_pole === 'Fast-Paced') {
tips.push('⚡ Keep it concise');
}
else {
tips.push('🕐 Allow time to decide');
}
break;
}
}
});
return tips.slice(0, 3); // Maximum 3 quick tips for mention context
}
function extractPreferredApproach(profile) {
const dimensions = profile.dimensions || [];
// Create a preference-based approach description
const approaches = [];
const internal = dimensions.some((d) => d.dimension === 'expression' && d.dominant_pole === 'Internal');
const structured = dimensions.some((d) => d.dimension === 'flexibility' && d.dominant_pole === 'Structured');
const logic = dimensions.some((d) => d.dimension === 'reasoning' && d.dominant_pole === 'Logic');
if (internal) {
approaches.push('Send information in advance');
}
if (structured) {
approaches.push('Use structured meetings');
}
if (logic) {
approaches.push('Lead with facts and data');
}
return approaches.length > 0
? approaches.join(', ')
: 'Standard professional communication';
}
function generateWhenToMention(profile) {
const keyTraits = profile.keyTraits || [];
const jobTitle = profile.jobTitle || '';
// Generate context-appropriate mention suggestions
const contexts = [];
if (jobTitle.toLowerCase().includes('manager') || jobTitle.toLowerCase().includes('director')) {
contexts.push('Strategic decisions and planning');
}
if (jobTitle.toLowerCase().includes('analyst') || keyTraits.some((t) => t.toLowerCase().includes('analytical'))) {
contexts.push('Data analysis and insights');
}
if (keyTraits.some((t) => t.toLowerCase().includes('collaborative') || t.toLowerCase().includes('team'))) {
contexts.push('Cross-functional collaboration');
}
return contexts.length > 0
? contexts.join(', ')
: 'General project discussions and professional matters';
}
function generateHowToEngage(profile) {
const dimensions = profile.dimensions || [];
// Generate engagement approach based on personality
const approaches = [];
const fastPaced = dimensions.some((d) => d.dimension === 'tempo' && d.dominant_pole === 'Fast-Paced');
const resultsOriented = dimensions.some((d) => d.dimension === 'work_style' && d.dominant_pole === 'Results-Driven');
const external = dimensions.some((d) => d.dimension === 'expression' && d.dominant_pole === 'External');
if (fastPaced || resultsOriented) {
approaches.push('Be direct and concise');
}
if (external) {
approaches.push('Invite discussion and input');
}
else {
approaches.push('Allow time for consideration');
}
return approaches.length > 0
? approaches.join(', ')
: 'Professional and respectful communication';
}
function calculateProfileCompleteness(profile) {
const requiredFields = ['personalitySummary', 'dimensions', 'keyTraits', 'communicationPreferences'];
const optionalFields = ['professionalContext', 'communicationRecommendations', 'operatingStyle'];
let score = 0;
const missing = [];
// Check required fields (70% of score)
requiredFields.forEach(field => {
if (profile[field] && (Array.isArray(profile[field]) ? profile[field].length > 0 : true)) {
score += 17.5; // 70% / 4 fields
}
else {
missing.push(field);
}
});
// Check optional fields (30% of score)
optionalFields.forEach(field => {
if (profile[field] && (Array.isArray(profile[field]) ? profile[field].length > 0 : true)) {
score += 10; // 30% / 3 fields
}
});
return {
score: Math.round(score),
missing: missing.map(field => field.replace(/([A-Z])/g, ' $1').toLowerCase().trim())
};
}
// Tool definition for MCP
export const mentionTool = {
name: "mention_person",
description: "Quick lookup for @mention functionality - find person by name and get essential context",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Person's name to mention",
minLength: 1
}
},
required: ["name"]
}
};
//# sourceMappingURL=mention.js.map