LinkedIn MCP Server¶
This document provides comprehensive documentation for the LinkedIn Model Context Protocol (MCP) server integration in Inttrest, enabling professional event discovery from LinkedIn posts and company pages.
🎯 Overview¶
The LinkedIn MCP server specializes in discovering professional events, networking opportunities, and business gatherings from LinkedIn content. It leverages LinkedIn's API and web scraping techniques to extract event information from posts, company pages, and professional profiles.
graph TB
subgraph "LinkedIn MCP Architecture"
A[MCP Client] --> B[LinkedIn MCP Server]
B --> C[LinkedIn API Client]
B --> D[Web Scraper Service]
B --> E[Professional Event Analyzer]
C --> F[LinkedIn API v2]
C --> G[OAuth Authentication]
C --> H[Rate Limiting]
D --> I[Selenium WebDriver]
D --> J[Session Management]
D --> K[Content Extraction]
E --> L[Event Classification]
E --> M[Professional Context]
E --> N[Network Analysis]
F --> O[Posts API]
F --> P[Companies API]
F --> Q[People API]
L --> R[AI Analysis]
M --> S[Industry Mapping]
N --> T[Professional Networks]
end
subgraph "Inttrest Integration"
U[Professional Events] --> A
V[Business Networking] --> A
W[Industry Events] --> A
R --> X[Event Database]
S --> Y[Professional Categories]
T --> Z[Network Insights]
end
style B fill:#e3f2fd
style F fill:#e8f5e8
style E fill:#fff3e0
style R fill:#fce4ec
📦 Installation & Setup¶
Prerequisites¶
- Node.js 18+ with npm/pnpm
- LinkedIn Developer Account with API access
- Chrome/Chromium for web scraping fallback
- OpenAI API Key for content analysis
Installation¶
# Navigate to LinkedIn MCP server directory
cd mcp_servers/linkedin-mcp-server
# Install dependencies
npm install
# or
pnpm install
# Install browser for web scraping
npx puppeteer browsers install chrome
# Build the server
npm run build
Configuration¶
Create a .env file in the linkedin-mcp-server directory:
# LinkedIn API Configuration
LINKEDIN_CLIENT_ID=your_linkedin_client_id
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret
LINKEDIN_REDIRECT_URI=http://localhost:3000/auth/linkedin/callback
LINKEDIN_ACCESS_TOKEN=your_linkedin_access_token
# LinkedIn Scraping (Fallback)
LINKEDIN_EMAIL=your_linkedin_email
LINKEDIN_PASSWORD=your_linkedin_password
LINKEDIN_SESSION_FILE=./sessions/linkedin_session.json
# Browser Configuration
BROWSER_HEADLESS=true
BROWSER_TIMEOUT=30000
SELENIUM_DRIVER_PATH=./drivers/chromedriver
# OpenAI Configuration
OPENAI_API_KEY=your_openai_api_key
OPENAI_MODEL=gpt-4-turbo-preview
# Rate Limiting
API_RATE_LIMIT=100
API_RATE_WINDOW=3600000 # 1 hour
SCRAPING_RATE_LIMIT=10
SCRAPING_RATE_WINDOW=60000 # 1 minute
# Cache Configuration
CACHE_TTL=1800000 # 30 minutes
CACHE_MAX_SIZE=1000
# Professional Event Settings
INDUSTRY_FOCUS=["technology", "finance", "healthcare", "marketing"]
EVENT_KEYWORDS=["conference", "seminar", "workshop", "networking"]
COMPANY_SIZE_FILTER=["startup", "small", "medium", "enterprise"]
# Logging
LOG_LEVEL=info
LOG_FILE=linkedin-mcp.log
Package Dependencies¶
{
"name": "linkedin-mcp-server",
"version": "1.0.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4",
"linkedin-api-v2": "^2.0.0",
"puppeteer": "^22.0.0",
"selenium-webdriver": "^4.15.0",
"openai": "^4.20.1",
"axios": "^1.6.0",
"node-cache": "^5.1.2",
"winston": "^3.11.0",
"zod": "^4.1.5",
"dotenv": "^16.3.1",
"cheerio": "^1.0.0-rc.12",
"jsonwebtoken": "^9.0.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/selenium-webdriver": "^4.1.15",
"typescript": "^5.0.0",
"tsx": "^4.0.0",
"vitest": "^1.0.0"
}
}
🏗️ Server Implementation¶
Core Server Structure¶
// src/linkedin-mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { LinkedInAPIClient } from './services/linkedin-api-client.js'
import { LinkedInScraper } from './services/linkedin-scraper.js'
import { ProfessionalEventAnalyzer } from './services/professional-event-analyzer.js'
import { Logger } from './utils/logger.js'
import { z } from 'zod'
const logger = new Logger('LinkedInMCP')
class LinkedInMCPServer {
private server: Server
private apiClient: LinkedInAPIClient
private scraper: LinkedInScraper
private eventAnalyzer: ProfessionalEventAnalyzer
constructor() {
this.server = new Server(
{
name: 'linkedin-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
)
this.apiClient = new LinkedInAPIClient()
this.scraper = new LinkedInScraper()
this.eventAnalyzer = new ProfessionalEventAnalyzer()
this.setupToolHandlers()
}
private setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'search_professional_events',
description: 'Search for professional events on LinkedIn',
inputSchema: {
type: 'object',
properties: {
keywords: {
type: 'array',
items: { type: 'string' },
description: 'Keywords for professional event search'
},
industry: {
type: 'string',
description: 'Industry focus for events'
},
location: {
type: 'string',
description: 'Geographic location for events'
},
event_type: {
type: 'string',
enum: ['conference', 'seminar', 'workshop', 'networking', 'webinar'],
description: 'Type of professional event'
},
company_size: {
type: 'string',
enum: ['startup', 'small', 'medium', 'enterprise'],
description: 'Company size filter'
},
date_range: {
type: 'object',
properties: {
start: { type: 'string' },
end: { type: 'string' }
},
description: 'Date range for events'
}
},
required: []
}
},
{
name: 'analyze_company_events',
description: 'Analyze a specific company for hosted events',
inputSchema: {
type: 'object',
properties: {
company_id: {
type: 'string',
description: 'LinkedIn company ID'
},
company_name: {
type: 'string',
description: 'Company name to search for'
},
include_employee_posts: {
type: 'boolean',
description: 'Include events from employee posts'
},
time_frame: {
type: 'string',
enum: ['week', 'month', 'quarter', 'year'],
description: 'Time frame for event analysis'
}
},
required: []
}
},
{
name: 'extract_event_from_post',
description: 'Extract event details from a LinkedIn post',
inputSchema: {
type: 'object',
properties: {
post_id: {
type: 'string',
description: 'LinkedIn post ID'
},
post_url: {
type: 'string',
description: 'LinkedIn post URL'
},
analyze_engagement: {
type: 'boolean',
description: 'Analyze post engagement for event popularity'
}
},
required: []
}
},
{
name: 'discover_industry_events',
description: 'Discover events by industry connections and influencers',
inputSchema: {
type: 'object',
properties: {
industry: {
type: 'string',
description: 'Target industry for event discovery'
},
influencer_posts: {
type: 'boolean',
description: 'Include posts from industry influencers'
},
company_posts: {
type: 'boolean',
description: 'Include posts from industry companies'
},
network_level: {
type: 'number',
minimum: 1,
maximum: 3,
description: 'Connection degree (1st, 2nd, 3rd)'
}
},
required: ['industry']
}
},
{
name: 'monitor_professional_networks',
description: 'Monitor professional networks for new events',
inputSchema: {
type: 'object',
properties: {
companies: {
type: 'array',
items: { type: 'string' },
description: 'Companies to monitor'
},
professionals: {
type: 'array',
items: { type: 'string' },
description: 'Professional profiles to monitor'
},
industries: {
type: 'array',
items: { type: 'string' },
description: 'Industries to monitor'
},
check_interval: {
type: 'number',
description: 'Monitoring interval in hours'
}
},
required: []
}
}
]
}
})
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
switch (name) {
case 'search_professional_events':
return await this.handleSearchProfessionalEvents(args)
case 'analyze_company_events':
return await this.handleAnalyzeCompanyEvents(args)
case 'extract_event_from_post':
return await this.handleExtractEventFromPost(args)
case 'discover_industry_events':
return await this.handleDiscoverIndustryEvents(args)
case 'monitor_professional_networks':
return await this.handleMonitorNetworks(args)
default:
throw new Error(`Unknown tool: ${name}`)
}
} catch (error) {
logger.error('Tool execution failed:', error)
throw error
}
})
}
private async handleSearchProfessionalEvents(args: any) {
const searchParams = ProfessionalEventSearchSchema.parse(args)
logger.info('Searching for professional events:', searchParams)
// Try API first, fallback to scraping
let posts: any[] = []
try {
posts = await this.apiClient.searchPosts({
keywords: searchParams.keywords,
industry: searchParams.industry,
location: searchParams.location
})
} catch (apiError) {
logger.warn('API search failed, falling back to scraping:', apiError)
posts = await this.scraper.searchEventPosts(searchParams)
}
const events = await this.eventAnalyzer.analyzePostsForEvents(posts, {
event_type: searchParams.event_type,
company_size: searchParams.company_size,
date_range: searchParams.date_range
})
return {
content: [
{
type: 'text',
text: `Found ${events.length} professional events from ${posts.length} LinkedIn posts`
}
],
_meta: {
source: 'linkedin',
search_params: searchParams,
posts_analyzed: posts.length,
events_found: events.length,
events: events
}
}
}
private async handleAnalyzeCompanyEvents(args: any) {
const companyParams = CompanyAnalysisSchema.parse(args)
logger.info('Analyzing company events:', companyParams)
let companyData: any
if (companyParams.company_id) {
companyData = await this.apiClient.getCompany(companyParams.company_id)
} else if (companyParams.company_name) {
companyData = await this.scraper.searchCompany(companyParams.company_name)
} else {
throw new Error('Either company_id or company_name is required')
}
const events = await this.eventAnalyzer.analyzeCompanyForEvents(
companyData,
{
include_employee_posts: companyParams.include_employee_posts,
time_frame: companyParams.time_frame
}
)
return {
content: [
{
type: 'text',
text: `Analyzed ${companyData.name} and found ${events.length} company events`
}
],
_meta: {
source: 'linkedin',
company: companyData,
events_found: events.length,
events: events
}
}
}
private async handleExtractEventFromPost(args: any) {
const postParams = PostExtractionSchema.parse(args)
logger.info('Extracting event from LinkedIn post:', postParams)
let postData: any
if (postParams.post_id) {
postData = await this.apiClient.getPost(postParams.post_id)
} else if (postParams.post_url) {
postData = await this.scraper.scrapePost(postParams.post_url)
} else {
throw new Error('Either post_id or post_url is required')
}
const event = await this.eventAnalyzer.extractEventFromPost(
postData,
{ analyze_engagement: postParams.analyze_engagement }
)
return {
content: [
{
type: 'text',
text: event ? `Extracted professional event: ${event.title}` : 'No event detected in this post'
}
],
_meta: {
source: 'linkedin',
post: postData,
event_detected: !!event,
event: event
}
}
}
private async handleDiscoverIndustryEvents(args: any) {
const industryParams = IndustryDiscoverySchema.parse(args)
logger.info('Discovering industry events:', industryParams)
const events = await this.eventAnalyzer.discoverIndustryEvents(industryParams)
return {
content: [
{
type: 'text',
text: `Discovered ${events.length} events in ${industryParams.industry} industry`
}
],
_meta: {
source: 'linkedin',
industry: industryParams.industry,
discovery_params: industryParams,
events_found: events.length,
events: events
}
}
}
private async handleMonitorNetworks(args: any) {
const monitorParams = NetworkMonitoringSchema.parse(args)
logger.info('Setting up professional network monitoring:', monitorParams)
const monitoringSetup = await this.setupNetworkMonitoring(monitorParams)
return {
content: [
{
type: 'text',
text: `Monitoring setup for ${monitorParams.companies?.length || 0} companies and ${monitorParams.professionals?.length || 0} professionals`
}
],
_meta: {
source: 'linkedin',
monitoring_setup: monitoringSetup,
params: monitorParams
}
}
}
private async setupNetworkMonitoring(params: any): Promise<any> {
// Implementation for setting up monitoring
return {
status: 'monitoring_active',
targets: {
companies: params.companies || [],
professionals: params.professionals || [],
industries: params.industries || []
},
check_interval_hours: params.check_interval || 24,
next_check: new Date(Date.now() + (params.check_interval || 24) * 60 * 60 * 1000).toISOString()
}
}
async start() {
try {
await this.apiClient.initialize()
await this.scraper.initialize()
const transport = new StdioServerTransport()
await this.server.connect(transport)
logger.info('LinkedIn MCP Server started successfully')
} catch (error) {
logger.error('Failed to start LinkedIn MCP Server:', error)
throw error
}
}
async stop() {
await this.scraper.close()
logger.info('LinkedIn MCP Server stopped')
}
}
// Schema definitions
const ProfessionalEventSearchSchema = z.object({
keywords: z.array(z.string()).optional(),
industry: z.string().optional(),
location: z.string().optional(),
event_type: z.enum(['conference', 'seminar', 'workshop', 'networking', 'webinar']).optional(),
company_size: z.enum(['startup', 'small', 'medium', 'enterprise']).optional(),
date_range: z.object({
start: z.string(),
end: z.string()
}).optional()
})
const CompanyAnalysisSchema = z.object({
company_id: z.string().optional(),
company_name: z.string().optional(),
include_employee_posts: z.boolean().optional(),
time_frame: z.enum(['week', 'month', 'quarter', 'year']).optional()
})
const PostExtractionSchema = z.object({
post_id: z.string().optional(),
post_url: z.string().optional(),
analyze_engagement: z.boolean().optional()
})
const IndustryDiscoverySchema = z.object({
industry: z.string(),
influencer_posts: z.boolean().optional(),
company_posts: z.boolean().optional(),
network_level: z.number().min(1).max(3).optional()
})
const NetworkMonitoringSchema = z.object({
companies: z.array(z.string()).optional(),
professionals: z.array(z.string()).optional(),
industries: z.array(z.string()).optional(),
check_interval: z.number().optional()
})
// Start the server
if (import.meta.url === `file://${process.argv[1]}`) {
const server = new LinkedInMCPServer()
process.on('SIGINT', async () => {
await server.stop()
process.exit(0)
})
server.start().catch(console.error)
}
export { LinkedInMCPServer }
LinkedIn API Client¶
// src/services/linkedin-api-client.ts
import axios, { AxiosInstance } from 'axios'
import jwt from 'jsonwebtoken'
import NodeCache from 'node-cache'
import { Logger } from '../utils/logger.js'
interface LinkedInConfig {
clientId: string
clientSecret: string
redirectUri: string
accessToken?: string
}
interface SearchParams {
keywords?: string[]
industry?: string
location?: string
companyId?: string
}
export class LinkedInAPIClient {
private client: AxiosInstance
private config: LinkedInConfig
private cache: NodeCache
private logger: Logger
private accessToken: string | null = null
constructor() {
this.logger = new Logger('LinkedInAPI')
this.config = this.loadConfig()
this.client = axios.create({
baseURL: 'https://api.linkedin.com/v2',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'X-Restli-Protocol-Version': '2.0.0'
}
})
this.cache = new NodeCache({
stdTTL: parseInt(process.env.CACHE_TTL || '1800'), // 30 minutes
maxKeys: parseInt(process.env.CACHE_MAX_SIZE || '1000')
})
this.setupInterceptors()
}
private loadConfig(): LinkedInConfig {
const requiredEnvVars = ['LINKEDIN_CLIENT_ID', 'LINKEDIN_CLIENT_SECRET']
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`)
}
}
return {
clientId: process.env.LINKEDIN_CLIENT_ID!,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET!,
redirectUri: process.env.LINKEDIN_REDIRECT_URI!,
accessToken: process.env.LINKEDIN_ACCESS_TOKEN
}
}
private setupInterceptors() {
this.client.interceptors.request.use(
(config) => {
if (this.accessToken) {
config.headers.Authorization = `Bearer ${this.accessToken}`
}
this.logger.debug(`API Request: ${config.method?.toUpperCase()} ${config.url}`)
return config
},
(error) => {
this.logger.error('API Request Error:', error)
return Promise.reject(error)
}
)
this.client.interceptors.response.use(
(response) => {
this.logger.debug(`API Response: ${response.status} ${response.config.url}`)
return response
},
(error) => {
this.logger.error('API Response Error:', {
status: error.response?.status,
statusText: error.response?.statusText,
url: error.config?.url
})
return Promise.reject(error)
}
)
}
async initialize(): Promise<void> {
if (this.config.accessToken) {
this.accessToken = this.config.accessToken
// Validate token
try {
await this.getCurrentUser()
this.logger.info('LinkedIn API initialized with existing token')
return
} catch (error) {
this.logger.warn('Existing token invalid, need to refresh')
}
}
// If no valid token, would need OAuth flow
this.logger.warn('No valid LinkedIn access token available')
}
async getCurrentUser(): Promise<any> {
const cacheKey = 'current_user'
const cached = this.cache.get(cacheKey)
if (cached) {
return cached
}
try {
const response = await this.client.get('/me')
const user = response.data
this.cache.set(cacheKey, user, 3600) // Cache for 1 hour
return user
} catch (error) {
this.logger.error('Failed to get current user:', error)
throw error
}
}
async searchPosts(params: SearchParams): Promise<any[]> {
const cacheKey = `posts:${JSON.stringify(params)}`
const cached = this.cache.get(cacheKey)
if (cached) {
this.logger.debug('Returning cached posts')
return cached
}
try {
// LinkedIn's search API is limited, this is a simplified example
const queryParams: any = {
count: 50,
start: 0
}
if (params.keywords?.length) {
queryParams.keywords = params.keywords.join(' ')
}
// Note: Actual LinkedIn API has different endpoints and parameters
const response = await this.client.get('/shares', {
params: queryParams
})
const posts = response.data.elements || []
this.cache.set(cacheKey, posts)
this.logger.info(`Found ${posts.length} posts`)
return posts
} catch (error) {
this.logger.error('Post search failed:', error)
throw error
}
}
async getCompany(companyId: string): Promise<any> {
const cacheKey = `company:${companyId}`
const cached = this.cache.get(cacheKey)
if (cached) {
return cached
}
try {
const response = await this.client.get(`/companies/${companyId}`)
const company = response.data
this.cache.set(cacheKey, company, 3600) // Cache for 1 hour
return company
} catch (error) {
this.logger.error('Failed to get company:', error)
throw error
}
}
async getCompanyPosts(companyId: string, options: any = {}): Promise<any[]> {
const cacheKey = `company_posts:${companyId}:${JSON.stringify(options)}`
const cached = this.cache.get(cacheKey)
if (cached) {
return cached
}
try {
const response = await this.client.get(`/companies/${companyId}/updates`, {
params: {
count: options.count || 25,
start: options.start || 0,
'event-type': 'status-update'
}
})
const posts = response.data.values || []
this.cache.set(cacheKey, posts)
return posts
} catch (error) {
this.logger.error('Failed to get company posts:', error)
throw error
}
}
async getPost(postId: string): Promise<any> {
const cacheKey = `post:${postId}`
const cached = this.cache.get(cacheKey)
if (cached) {
return cached
}
try {
const response = await this.client.get(`/shares/${postId}`)
const post = response.data
this.cache.set(cacheKey, post, 1800) // Cache for 30 minutes
return post
} catch (error) {
this.logger.error('Failed to get post:', error)
throw error
}
}
async searchCompanies(query: string): Promise<any[]> {
const cacheKey = `company_search:${query}`
const cached = this.cache.get(cacheKey)
if (cached) {
return cached
}
try {
const response = await this.client.get('/companySearch', {
params: {
keywords: query,
count: 25
}
})
const companies = response.data.companies?.values || []
this.cache.set(cacheKey, companies)
return companies
} catch (error) {
this.logger.error('Company search failed:', error)
throw error
}
}
async getIndustryInsights(industry: string): Promise<any> {
// This would use LinkedIn's industry insights API
// Placeholder implementation
return {
industry: industry,
top_companies: [],
trending_topics: [],
recent_posts: []
}
}
}
Professional Event Analyzer¶
// src/services/professional-event-analyzer.ts
import OpenAI from 'openai'
import { Logger } from '../utils/logger.js'
interface ProfessionalEvent {
id: string
title: string
description: string
type: 'conference' | 'seminar' | 'workshop' | 'networking' | 'webinar' | 'other'
industry: string
start_date?: string
end_date?: string
location?: {
name: string
address?: string
is_virtual: boolean
}
organizer: {
name: string
type: 'company' | 'individual' | 'organization'
linkedin_id?: string
industry?: string
company_size?: string
}
target_audience: {
seniority_levels: string[]
job_functions: string[]
industries: string[]
}
registration_info: {
is_free: boolean
price?: string
registration_url?: string
capacity?: number
registration_deadline?: string
}
networking_value: {
attendee_quality_score: number
industry_relevance_score: number
career_growth_potential: number
}
source_post: {
url: string
linkedin_id: string
engagement: {
likes: number
comments: number
shares: number
engagement_rate: number
}
}
confidence_score: number
extracted_at: string
}
export class ProfessionalEventAnalyzer {
private openai: OpenAI
private logger: Logger
constructor() {
this.logger = new Logger('ProfessionalEventAnalyzer')
if (!process.env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY is required for content analysis')
}
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
})
}
async analyzePostsForEvents(posts: any[], filters: any = {}): Promise<ProfessionalEvent[]> {
this.logger.info(`Analyzing ${posts.length} LinkedIn posts for professional events`)
const events: ProfessionalEvent[] = []
for (const post of posts) {
try {
const event = await this.extractEventFromPost(post, { deep_analysis: true })
if (event && this.matchesFilters(event, filters)) {
events.push(event)
}
} catch (error) {
this.logger.debug(`Failed to analyze post ${post.id}:`, error)
}
}
// Sort by confidence and networking value
events.sort((a, b) => {
const scoreA = a.confidence_score * a.networking_value.attendee_quality_score
const scoreB = b.confidence_score * b.networking_value.attendee_quality_score
return scoreB - scoreA
})
this.logger.info(`Extracted ${events.length} professional events`)
return events
}
async analyzeCompanyForEvents(companyData: any, options: any = {}): Promise<ProfessionalEvent[]> {
this.logger.info(`Analyzing company ${companyData.name} for events`)
// Get company posts
let posts = companyData.posts || []
if (options.include_employee_posts) {
// Would need to fetch employee posts
// This is a simplified implementation
}
// Filter by time frame
if (options.time_frame) {
posts = this.filterPostsByTimeFrame(posts, options.time_frame)
}
const events = await this.analyzePostsForEvents(posts)
// Add company context to events
return events.map(event => ({
...event,
organizer: {
...event.organizer,
name: companyData.name,
type: 'company' as const,
linkedin_id: companyData.id,
industry: companyData.industry,
company_size: this.categorizeCompanySize(companyData.employeeCount)
}
}))
}
async extractEventFromPost(post: any, options: any = {}): Promise<ProfessionalEvent | null> {
try {
// Quick keyword filtering for professional events
if (!this.containsProfessionalEventKeywords(post.text || post.content || '')) {
return null
}
if (options.deep_analysis) {
return await this.performProfessionalAIAnalysis(post, options)
} else {
return await this.performBasicProfessionalAnalysis(post)
}
} catch (error) {
this.logger.error(`Failed to extract event from post ${post.id}:`, error)
return null
}
}
async discoverIndustryEvents(params: any): Promise<ProfessionalEvent[]> {
this.logger.info(`Discovering events in ${params.industry} industry`)
const events: ProfessionalEvent[] = []
// This would implement industry-specific event discovery
// Placeholder implementation
return events
}
private containsProfessionalEventKeywords(text: string): boolean {
const professionalEventKeywords = [
// Event types
'conference', 'summit', 'symposium', 'convention', 'expo', 'trade show',
'seminar', 'workshop', 'masterclass', 'training', 'certification',
'webinar', 'virtual event', 'online session', 'live stream',
'networking', 'mixer', 'meetup', 'panel discussion', 'roundtable',
// Professional contexts
'professional development', 'career growth', 'industry insights',
'thought leadership', 'best practices', 'innovation', 'digital transformation',
// Registration/attendance
'register now', 'rsvp', 'limited seats', 'early bird', 'save the date',
'join us', 'don\'t miss', 'exclusive event', 'invitation only',
// Business contexts
'c-suite', 'executives', 'leaders', 'professionals', 'entrepreneurs',
'startup', 'enterprise', 'fortune 500', 'industry leaders'
]
const textLower = text.toLowerCase()
return professionalEventKeywords.some(keyword => textLower.includes(keyword))
}
private async performBasicProfessionalAnalysis(post: any): Promise<ProfessionalEvent | null> {
const content = post.text || post.content || ''
// Extract professional event indicators
const eventType = this.inferEventType(content)
const industry = this.inferIndustry(content)
const audienceLevel = this.inferAudienceLevel(content)
if (!eventType || !industry) {
return null
}
return {
id: `basic_${post.id}`,
title: this.extractProfessionalTitle(content),
description: content,
type: eventType,
industry: industry,
target_audience: {
seniority_levels: audienceLevel ? [audienceLevel] : [],
job_functions: this.inferJobFunctions(content),
industries: [industry]
},
organizer: {
name: post.author?.name || 'Professional',
type: this.inferOrganizerType(post)
},
networking_value: {
attendee_quality_score: this.calculateAttendeeQualityScore(post),
industry_relevance_score: 0.7,
career_growth_potential: 0.6
},
registration_info: {
is_free: this.inferIsFree(content)
},
source_post: {
url: post.url || '',
linkedin_id: post.id,
engagement: this.extractEngagement(post)
},
confidence_score: 0.6,
extracted_at: new Date().toISOString()
}
}
private async performProfessionalAIAnalysis(post: any, options: any): Promise<ProfessionalEvent | null> {
try {
const prompt = this.buildProfessionalAnalysisPrompt(post)
const response = await this.openai.chat.completions.create({
model: process.env.OPENAI_MODEL || 'gpt-4-turbo-preview',
messages: [
{
role: 'system',
content: 'You are an expert at analyzing LinkedIn posts for professional events, networking opportunities, and business gatherings. Extract detailed professional event information.'
},
{
role: 'user',
content: prompt
}
],
functions: [
{
name: 'extract_professional_event',
description: 'Extract professional event information from LinkedIn post',
parameters: {
type: 'object',
properties: {
is_professional_event: {
type: 'boolean',
description: 'Whether this is a professional/business event'
},
event_title: {
type: 'string',
description: 'Professional event title'
},
event_type: {
type: 'string',
enum: ['conference', 'seminar', 'workshop', 'networking', 'webinar', 'other'],
description: 'Type of professional event'
},
industry: {
type: 'string',
description: 'Primary industry focus'
},
target_seniority: {
type: 'array',
items: { type: 'string' },
description: 'Target seniority levels (entry, mid, senior, executive)'
},
target_functions: {
type: 'array',
items: { type: 'string' },
description: 'Target job functions'
},
networking_value: {
type: 'number',
description: 'Networking value score 0-1'
},
career_growth_potential: {
type: 'number',
description: 'Career growth potential score 0-1'
},
organizer_type: {
type: 'string',
enum: ['company', 'individual', 'organization'],
description: 'Type of event organizer'
},
is_virtual: {
type: 'boolean',
description: 'Whether the event is virtual'
},
registration_required: {
type: 'boolean',
description: 'Whether registration is required'
},
is_free: {
type: 'boolean',
description: 'Whether the event is free'
},
confidence_score: {
type: 'number',
description: 'Confidence score 0-1'
}
},
required: ['is_professional_event', 'confidence_score']
}
}
],
function_call: { name: 'extract_professional_event' }
})
const functionCall = response.choices[0]?.message?.function_call
if (!functionCall || functionCall.name !== 'extract_professional_event') {
return null
}
const extractedData = JSON.parse(functionCall.arguments)
if (!extractedData.is_professional_event || extractedData.confidence_score < 0.6) {
return null
}
return {
id: `ai_professional_${post.id}`,
title: extractedData.event_title || this.extractProfessionalTitle(post.content),
description: post.content || post.text || '',
type: extractedData.event_type || 'other',
industry: extractedData.industry || 'general',
location: {
name: extractedData.is_virtual ? 'Virtual Event' : 'TBD',
is_virtual: extractedData.is_virtual || false
},
target_audience: {
seniority_levels: extractedData.target_seniority || [],
job_functions: extractedData.target_functions || [],
industries: [extractedData.industry || 'general']
},
organizer: {
name: post.author?.name || 'Professional',
type: extractedData.organizer_type || 'individual'
},
networking_value: {
attendee_quality_score: extractedData.networking_value || 0.5,
industry_relevance_score: 0.8,
career_growth_potential: extractedData.career_growth_potential || 0.6
},
registration_info: {
is_free: extractedData.is_free || false
},
source_post: {
url: post.url || '',
linkedin_id: post.id,
engagement: this.extractEngagement(post)
},
confidence_score: extractedData.confidence_score,
extracted_at: new Date().toISOString()
}
} catch (error) {
this.logger.error('Professional AI analysis failed:', error)
return this.performBasicProfessionalAnalysis(post)
}
}
private buildProfessionalAnalysisPrompt(post: any): string {
return `
Analyze this LinkedIn post for professional event information:
Post Content: ${post.text || post.content || ''}
Author: ${post.author?.name || 'Unknown'}
Author Title: ${post.author?.headline || 'Unknown'}
Company: ${post.author?.company || 'Unknown'}
Engagement: ${post.likes || 0} likes, ${post.comments || 0} comments, ${post.shares || 0} shares
Please analyze if this is promoting a professional event and extract:
- Event type and industry focus
- Target professional audience (seniority, functions)
- Networking and career growth value
- Registration and attendance details
- Overall professional relevance
Focus on business value, networking opportunities, and professional development aspects.
`
}
private inferEventType(content: string): ProfessionalEvent['type'] | null {
const contentLower = content.toLowerCase()
if (contentLower.includes('conference') || contentLower.includes('summit')) return 'conference'
if (contentLower.includes('seminar') || contentLower.includes('symposium')) return 'seminar'
if (contentLower.includes('workshop') || contentLower.includes('training')) return 'workshop'
if (contentLower.includes('networking') || contentLower.includes('mixer')) return 'networking'
if (contentLower.includes('webinar') || contentLower.includes('virtual')) return 'webinar'
return null
}
private inferIndustry(content: string): string {
const industryKeywords = {
technology: ['tech', 'software', 'ai', 'machine learning', 'data', 'cloud', 'cybersecurity'],
finance: ['finance', 'fintech', 'banking', 'investment', 'crypto', 'blockchain'],
healthcare: ['healthcare', 'medical', 'pharma', 'biotech', 'health'],
marketing: ['marketing', 'advertising', 'brand', 'digital marketing', 'seo'],
consulting: ['consulting', 'strategy', 'management', 'advisory'],
manufacturing: ['manufacturing', 'industrial', 'automation', 'supply chain'],
education: ['education', 'learning', 'training', 'academic', 'university'],
real_estate: ['real estate', 'property', 'construction', 'architecture']
}
const contentLower = content.toLowerCase()
for (const [industry, keywords] of Object.entries(industryKeywords)) {
if (keywords.some(keyword => contentLower.includes(keyword))) {
return industry
}
}
return 'general'
}
private inferAudienceLevel(content: string): string | null {
const contentLower = content.toLowerCase()
if (contentLower.includes('c-suite') || contentLower.includes('ceo') || contentLower.includes('executive')) {
return 'executive'
}
if (contentLower.includes('senior') || contentLower.includes('director') || contentLower.includes('vp')) {
return 'senior'
}
if (contentLower.includes('manager') || contentLower.includes('lead')) {
return 'mid'
}
if (contentLower.includes('junior') || contentLower.includes('entry') || contentLower.includes('graduate')) {
return 'entry'
}
return null
}
private inferJobFunctions(content: string): string[] {
const functions: string[] = []
const contentLower = content.toLowerCase()
const functionKeywords = {
'engineering': ['engineer', 'developer', 'programmer', 'technical'],
'sales': ['sales', 'business development', 'account management'],
'marketing': ['marketing', 'brand', 'content', 'digital'],
'hr': ['hr', 'human resources', 'talent', 'recruiting'],
'finance': ['finance', 'accounting', 'controller', 'cfo'],
'operations': ['operations', 'project management', 'logistics'],
'product': ['product manager', 'product owner', 'ux', 'ui']
}
for (const [func, keywords] of Object.entries(functionKeywords)) {
if (keywords.some(keyword => contentLower.includes(keyword))) {
functions.push(func)
}
}
return functions
}
private inferOrganizerType(post: any): 'company' | 'individual' | 'organization' {
if (post.author?.company || post.isCompanyPost) {
return 'company'
}
// Check if it's from a professional organization
const authorName = post.author?.name?.toLowerCase() || ''
if (authorName.includes('association') || authorName.includes('institute') ||
authorName.includes('foundation') || authorName.includes('society')) {
return 'organization'
}
return 'individual'
}
private calculateAttendeeQualityScore(post: any): number {
let score = 0.5 // Base score
// Higher score for company posts
if (post.isCompanyPost) score += 0.2
// Higher score for verified accounts
if (post.author?.isVerified) score += 0.1
// Higher score based on engagement
const engagement = this.extractEngagement(post)
if (engagement.engagement_rate > 0.05) score += 0.1
if (engagement.engagement_rate > 0.1) score += 0.1
return Math.min(score, 1.0)
}
private inferIsFree(content: string): boolean {
const contentLower = content.toLowerCase()
return contentLower.includes('free') ||
contentLower.includes('no cost') ||
contentLower.includes('complimentary')
}
private extractEngagement(post: any): any {
const likes = post.likes || post.reactions || 0
const comments = post.comments || 0
const shares = post.shares || post.reposts || 0
const total = likes + comments + shares
// Estimate reach for engagement rate calculation
const estimatedReach = (post.author?.followers || 1000) * 0.1 // Assume 10% reach
const engagementRate = estimatedReach > 0 ? total / estimatedReach : 0
return {
likes,
comments,
shares,
engagement_rate: Math.min(engagementRate, 1.0)
}
}
private extractProfessionalTitle(content: string): string {
// Extract first line that looks like a title
const lines = content.split('\n').filter(line => line.trim())
if (lines.length > 0) {
const firstLine = lines[0].trim()
return firstLine.slice(0, 100)
}
return 'Professional Event'
}
private matchesFilters(event: ProfessionalEvent, filters: any): boolean {
if (filters.event_type && event.type !== filters.event_type) {
return false
}
if (filters.company_size && event.organizer.company_size !== filters.company_size) {
return false
}
if (filters.date_range && event.start_date) {
const eventDate = new Date(event.start_date)
const startDate = new Date(filters.date_range.start)
const endDate = new Date(filters.date_range.end)
if (eventDate < startDate || eventDate > endDate) {
return false
}
}
return true
}
private filterPostsByTimeFrame(posts: any[], timeFrame: string): any[] {
const now = new Date()
let cutoffDate: Date
switch (timeFrame) {
case 'week':
cutoffDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
break
case 'month':
cutoffDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
break
case 'quarter':
cutoffDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
break
case 'year':
cutoffDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000)
break
default:
return posts
}
return posts.filter(post => {
const postDate = new Date(post.createdAt || post.timestamp)
return postDate >= cutoffDate
})
}
private categorizeCompanySize(employeeCount: number): string {
if (employeeCount <= 10) return 'startup'
if (employeeCount <= 50) return 'small'
if (employeeCount <= 1000) return 'medium'
return 'enterprise'
}
}
This comprehensive LinkedIn MCP server documentation covers professional event discovery, company analysis, and industry-specific networking opportunities! 💼