UNPKG

@prsna_ai/mcp-server

Version:

Model Context Protocol server for PRSNA personality profiles and communication insights

183 lines (180 loc) 8.13 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 SearchLinkedInSchema = z.object({ name: z.string().min(1, 'Name is required'), company: z.string().optional(), location: z.string().optional(), title: 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 a single search result function formatResult(result, index) { const prefix = index !== undefined ? `${index + 1}. ` : ''; return `${prefix}**${result.title}**\n ${result.url}\n ${result.snippet}`; } export async function searchLinkedInByName(args) { try { const { name, company, location, title } = SearchLinkedInSchema.parse(args); logger.info('Searching LinkedIn profiles by name', { name, company, location, title }); const client = getApiClient(); const response = await client.searchLinkedInByName(name, company, location, title); client.destroy(); // Build formatted text response based on result count const lines = []; if (response.resultCount === 0) { // No results lines.push(`**No LinkedIn Profiles Found**`); lines.push(''); lines.push(`Could not find any LinkedIn profiles matching "${name}".`); lines.push(''); lines.push(`**Suggestions:**`); lines.push(`- Check the spelling of the name`); lines.push(`- Try a different name variation`); lines.push(`- If you have the LinkedIn URL, use \`analyze_linkedin_profile\` directly`); } else if (response.resultCount === 1 && response.result) { // Single result - require confirmation lines.push(`**Found 1 Potential Match**`); lines.push(''); lines.push(formatResult(response.result)); lines.push(''); lines.push(`---`); lines.push(`**ACTION REQUIRED:** Ask the user to confirm this is the correct person.`); lines.push(`- If YES: proceed with \`analyze_linkedin_profile\` using the URL above`); lines.push(`- If NO: ask the user to provide the correct LinkedIn URL directly`); } else if (response.resultCount >= 2 && response.resultCount <= 5 && response.results) { // 2-5 results - user must choose lines.push(`**Found ${response.resultCount} Potential Matches**`); lines.push(''); for (let i = 0; i < response.results.length; i++) { lines.push(formatResult(response.results[i], i)); lines.push(''); } lines.push(`---`); lines.push(`**ACTION REQUIRED:** Present these options to the user and ask which one is correct.`); lines.push(`- If they choose one: proceed with \`analyze_linkedin_profile\` using that URL`); lines.push(`- If none are correct: ask the user to provide the LinkedIn URL directly`); } else if (response.resultCount >= 6) { // Too many results - still show top results for user to review lines.push(`**Found ${response.resultCount} Matches**`); lines.push(''); lines.push(`Here are the top results:`); lines.push(''); const topResults = response.topResults || response.results?.slice(0, 5) || []; for (let i = 0; i < topResults.length; i++) { lines.push(formatResult(topResults[i], i)); lines.push(''); } lines.push(`---`); lines.push(`**ACTION REQUIRED:** Present these options to the user and ask:`); lines.push(`- Is one of these the correct person?`); lines.push(`- If none match, ask for more details (company, location, title) to narrow the search`); lines.push(`- Or ask the user to provide the LinkedIn URL directly`); } const textResponse = lines.join('\n'); // Build structured result for JSON const result = { success: response.success, message: response.message, resultCount: response.resultCount, result: response.result, results: response.results, topResults: response.topResults, suggestion: response.suggestion, action: response.action }; logger.info('LinkedIn search returned', { name, resultCount: response.resultCount }); return { content: [ { type: "text", text: textResponse }, { type: "text", text: safeJSONStringify(cleanObject(result), 2) } ] }; } catch (error) { const errorMessage = error.message; logger.error('Failed to search LinkedIn profiles', { error: errorMessage }); // Provide helpful error messages let userMessage = 'Failed to search LinkedIn profiles'; if (errorMessage.includes('quota exceeded')) { userMessage = 'Search quota exceeded. Please try again later or provide the LinkedIn URL directly.'; } else if (errorMessage.includes('not configured')) { userMessage = 'LinkedIn search is not configured. Please provide the LinkedIn URL directly using analyze_linkedin_profile.'; } else if (errorMessage.includes('Name is required')) { userMessage = 'Please provide a name to search for.'; } return { content: [{ type: "text", text: safeJSONStringify({ success: false, message: userMessage, error: errorMessage }, 2) }] }; } } // Tool definition for MCP export const linkedinSearchTool = { name: "search_linkedin_by_name", description: `Search LinkedIn to find a person's profile by their name. Use this tool FIRST when a user asks about someone who may NOT already be in their saved PRSNA profiles. WHEN TO USE THIS TOOL: - User asks to "find" or "look up" someone by name - User wants to analyze/learn about someone new (not already saved) - User mentions a person's name and wants their personality profile - ALWAYS try this tool before telling the user "profile not found" IMPORTANT WORKFLOW: 1. This tool searches LinkedIn and returns potential profile matches 2. You MUST present ALL results to the user and ask them to confirm which profile is correct 3. NEVER automatically analyze a profile - always wait for user confirmation 4. If the user says "none of these", ask them to provide the LinkedIn URL directly 5. Once confirmed, use analyze_linkedin_profile with the chosen URL For better results, provide company, location, or job title if known.`, inputSchema: { type: "object", properties: { name: { type: "string", description: "Full name of the person to search for (required)" }, company: { type: "string", description: "Company name to narrow down search results (optional)" }, location: { type: "string", description: "Location (city, state, or country) to narrow down search results (optional)" }, title: { type: "string", description: "Job title to narrow down search results (optional)" } }, required: ["name"] } }; //# sourceMappingURL=linkedin-search.js.map