@prsna_ai/mcp-server
Version:
Model Context Protocol server for PRSNA personality profiles and communication insights
382 lines • 14.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 schemas
const ListProfilesSchema = z.object({
limit: z.number().min(1).max(100).optional().default(20),
offset: z.number().min(0).optional().default(0)
});
const SearchProfilesSchema = z.object({
query: z.string().min(1, 'Search query is required'),
limit: z.number().min(1).max(50).optional().default(10)
});
const GetProfileContextSchema = z.object({
profileId: z.string().optional(),
name: z.string().optional()
}).refine(data => data.profileId || data.name, {
message: "Either profileId or name must be provided"
});
// 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);
}
export async function listPersonalityProfiles(args) {
try {
const { limit, offset } = ListProfilesSchema.parse(args);
logger.info('Listing personality profiles', { limit, offset });
const client = getApiClient();
const response = await client.getProfiles(limit, offset);
client.destroy();
const result = {
success: true,
profiles: response.profiles.map(profile => ({
id: profile._id,
name: profile.name,
jobTitle: profile.jobTitle,
company: profile.company,
personalitySummary: profile.personalitySummary,
dimensions: profile.dimensions,
lastUpdated: profile.updatedAt,
keyTraits: profile.keyTraits,
communicationPreferences: profile.communicationPreferences
})),
pagination: {
total: response.total,
limit: response.limit,
offset: response.offset,
hasMore: response.offset + response.limit < response.total
}
};
logger.info('Profiles listed successfully', {
returned: response.profiles.length,
total: response.total
});
return {
content: [{
type: "text",
text: safeJSONStringify(cleanObject(result), 2)
}]
};
}
catch (error) {
const errorMessage = error.message;
logger.error('Failed to list profiles', { error: errorMessage });
return {
content: [{
type: "text",
text: safeJSONStringify({
success: false,
message: 'Failed to list personality profiles',
error: errorMessage
}, 2)
}]
};
}
}
export async function searchPersonalityProfiles(args) {
try {
const { query, limit } = SearchProfilesSchema.parse(args);
logger.info('Searching personality profiles', { query, limit });
const client = getApiClient();
const response = await client.searchProfiles(query, limit);
client.destroy();
const result = {
success: true,
query,
profiles: response.profiles.map(profile => ({
id: profile._id,
name: profile.name,
jobTitle: profile.jobTitle,
company: profile.company,
personalitySummary: profile.personalitySummary,
dimensions: profile.dimensions,
relevance: calculateRelevance(profile, query),
keyTraits: profile.keyTraits,
industry: profile.industry
})),
total: response.total
};
logger.info('Profile search completed', {
query,
found: response.total
});
return {
content: [{
type: "text",
text: safeJSONStringify(cleanObject(result), 2)
}]
};
}
catch (error) {
const errorMessage = error.message;
logger.error('Profile search failed', { error: errorMessage });
return {
content: [{
type: "text",
text: safeJSONStringify({
success: false,
message: 'Failed to search personality profiles',
error: errorMessage
}, 2)
}]
};
}
}
export async function getPersonalityContext(args) {
try {
const { profileId, name } = GetProfileContextSchema.parse(args);
logger.info('Getting personality context', { profileId, name });
const client = getApiClient();
let profile;
if (profileId) {
profile = await client.getProfile(profileId);
}
else if (name) {
profile = await client.findProfileByName(name);
if (!profile) {
client.destroy();
return {
content: [{
type: "text",
text: safeJSONStringify({
success: false,
message: `No profile found for name: ${name}`
}, 2)
}]
};
}
}
client.destroy();
if (!profile) {
throw new Error('Profile not found');
}
const result = {
success: true,
profile: {
// Basic Information
id: profile._id,
name: profile.name,
jobTitle: profile.jobTitle,
company: profile.company,
industry: profile.industry,
location: profile.location,
// Personality Analysis
personalitySummary: profile.personalitySummary,
professionalContext: profile.professionalContext,
keyTraits: profile.keyTraits,
operatingStyle: profile.operatingStyle,
dominantTraitOverview: profile.dominantTraitOverview,
// Personality Dimensions
dimensions: profile.dimensions.map(dim => ({
dimension: dim.dimension,
dominantPole: dim.dominant_pole,
confidence: dim.confidence_percentage,
description: getDimensionDescription(dim.dimension, dim.dominant_pole)
})),
// Communication Guidance
communicationPreferences: profile.communicationPreferences,
communicationRecommendations: profile.communicationRecommendations,
// Additional Context
notes: profile.notes,
context: profile.context,
llmOptimizedProfile: profile.llmOptimizedProfile,
// Metadata
lastUpdated: profile.updatedAt,
linkedInUrl: profile.linkedInUrl
}
};
logger.info('Personality context retrieved', {
profileId: profile._id,
name: profile.name
});
return {
content: [{
type: "text",
text: safeJSONStringify(cleanObject(result), 2)
}]
};
}
catch (error) {
const errorMessage = error.message;
logger.error('Failed to get personality context', { error: errorMessage });
return {
content: [{
type: "text",
text: safeJSONStringify({
success: false,
message: 'Failed to get personality context',
error: errorMessage
}, 2)
}]
};
}
}
export async function myPersonalityProfile(args) {
try {
logger.info('Getting user\'s own personality profile');
const client = getApiClient();
const userProfile = await client.authenticate();
client.destroy();
const result = {
success: true,
profile: {
// Basic Information
id: userProfile._id,
name: userProfile.name,
email: userProfile.email,
// Subscription Information
subscriptionStatus: userProfile.subscriptionStatus,
subscriptionPlan: userProfile.subscriptionPlan,
// Personality Assessment Results
dimensions: userProfile.results?.map(dim => ({
dimensionId: dim.dimensionId,
dominantPole: dim.dominantPole,
score: dim.score,
description: getDimensionDescription(dim.dimensionId, dim.dominantPole)
})) || [],
// Metadata
createdAt: userProfile.createdAt,
updatedAt: userProfile.updatedAt
}
};
logger.info('User personality profile retrieved', {
userId: userProfile._id,
name: userProfile.name
});
return {
content: [{
type: "text",
text: safeJSONStringify(cleanObject(result), 2)
}]
};
}
catch (error) {
const errorMessage = error.message;
logger.error('Failed to get user personality profile', { error: errorMessage });
return {
content: [{
type: "text",
text: safeJSONStringify({
success: false,
message: 'Failed to get your personality profile',
error: errorMessage
}, 2)
}]
};
}
}
// Helper functions
function calculateRelevance(profile, query) {
const searchableText = [
profile.name,
profile.jobTitle,
profile.company,
profile.industry || ''
].join(' ').toLowerCase();
const queryTerms = query.toLowerCase().split(' ');
const matches = queryTerms.filter(term => searchableText.includes(term));
return Math.round((matches.length / queryTerms.length) * 100);
}
function getDimensionDescription(dimension, pole) {
const descriptions = {
expression: {
'Internal': 'Prefers to process thoughts internally before sharing, values reflection and careful consideration',
'External': 'Thinks out loud, processes ideas through discussion, energized by external interaction'
},
reasoning: {
'Logic': 'Makes decisions based on objective analysis, facts, and logical reasoning',
'Feeling': 'Considers people and values when making decisions, focuses on impact and harmony'
},
flexibility: {
'Structured': 'Prefers clear plans, deadlines, and organized approaches to work',
'Adaptive': 'Comfortable with ambiguity, prefers flexibility and spontaneous approaches'
},
work_style: {
'Process-Driven': 'Values following established procedures, systematic approaches, and thoroughness',
'Results-Driven': 'Focuses on outcomes, efficiency, and getting things done quickly'
},
tempo: {
'Fast-Paced': 'Prefers quick decisions, rapid execution, and high-energy environments',
'Deliberate-Paced': 'Values careful consideration, thorough analysis, and steady progress'
}
};
return descriptions[dimension]?.[pole] || `${pole} preference in ${dimension}`;
}
// Tool definitions for MCP
export const profileTools = [
{
name: "list_personality_profiles",
description: "List all saved personality profiles for the authenticated user",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Maximum number of profiles to return (1-100)",
minimum: 1,
maximum: 100,
default: 20
},
offset: {
type: "number",
description: "Number of profiles to skip for pagination",
minimum: 0,
default: 0
}
}
}
},
{
name: "search_personality_profiles",
description: "Search personality profiles by name, company, or job title",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (name, company, or job title)",
minLength: 1
},
limit: {
type: "number",
description: "Maximum number of results to return (1-50)",
minimum: 1,
maximum: 50,
default: 10
}
},
required: ["query"]
}
},
{
name: "get_personality_context",
description: "Get comprehensive personality context for a specific person",
inputSchema: {
type: "object",
properties: {
profileId: {
type: "string",
description: "Profile ID (optional if name is provided)"
},
name: {
type: "string",
description: "Person's name (optional if profileId is provided)"
}
}
}
},
{
name: "my_personality_profile",
description: "Get your own personality profile and assessment results",
inputSchema: {
type: "object",
properties: {}
}
}
];
//# sourceMappingURL=profiles.js.map