Instagram MCP Server¶
This document provides comprehensive documentation for the Instagram Model Context Protocol (MCP) server integration in Inttrest, enabling event discovery from Instagram posts and stories.
🎯 Overview¶
The Instagram MCP server leverages web scraping and automation to discover events from Instagram posts, stories, and profiles. It provides intelligent content analysis to extract event information from social media posts.
graph TB
subgraph "Instagram MCP Architecture"
A[MCP Client] --> B[Instagram MCP Server]
B --> C[Browser Service]
B --> D[Profile Scraper]
B --> E[Content Analyzer]
C --> F[Puppeteer Browser]
C --> G[Session Management]
C --> H[Anti-Detection]
D --> I[Post Extraction]
D --> J[Story Analysis]
D --> K[Bio Parsing]
E --> L[AI Content Analysis]
E --> M[Event Detection]
E --> N[Location Extraction]
L --> O[OpenAI API]
M --> P[Event Classification]
N --> Q[Geocoding Service]
end
subgraph "Inttrest Integration"
R[AI Chat Interface] --> A
S[Event Discovery] --> A
T[Social Feed] --> A
P --> U[Event Database]
Q --> V[Map Integration]
N --> W[Real-time Updates]
end
style B fill:#e3f2fd
style C fill:#e8f5e8
style E fill:#fff3e0
style O fill:#fce4ec
📦 Installation & Setup¶
Prerequisites¶
- Node.js 18+ with npm/pnpm
- Chrome/Chromium browser for Puppeteer
- OpenAI API Key for content analysis
- Instagram Account (optional, for authenticated scraping)
Installation¶
# Navigate to Instagram MCP server directory
cd mcp_servers/instagram-server-next-mcp
# Install dependencies
npm install
# or
pnpm install
# Install Puppeteer browser
npx puppeteer browsers install chrome
# Build the server
npm run build
Configuration¶
Create a .env file in the instagram-server-next-mcp directory:
# Instagram Configuration
INSTAGRAM_USERNAME=your_instagram_username
INSTAGRAM_PASSWORD=your_instagram_password
INSTAGRAM_SESSION_FILE=./sessions/instagram_session.json
# Browser Configuration
BROWSER_HEADLESS=true
BROWSER_TIMEOUT=30000
BROWSER_VIEWPORT_WIDTH=1920
BROWSER_VIEWPORT_HEIGHT=1080
# OpenAI Configuration for Content Analysis
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_MODEL=gpt-4-turbo-preview
# Proxy Configuration (optional)
PROXY_HOST=
PROXY_PORT=
PROXY_USERNAME=
PROXY_PASSWORD=
# Rate Limiting
RATE_LIMIT_REQUESTS=10
RATE_LIMIT_WINDOW=60000 # 1 minute
CONCURRENT_REQUESTS=3
# Cache Configuration
CACHE_TTL=1800000 # 30 minutes
CACHE_MAX_SIZE=500
# Logging
LOG_LEVEL=info
LOG_FILE=instagram-mcp.log
# Anti-Detection
USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
STEALTH_MODE=true
RANDOM_DELAYS=true
Package Dependencies¶
{
"name": "instagram-mcp-server",
"version": "1.0.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4",
"puppeteer": "^22.0.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"openai": "^4.20.1",
"cheerio": "^1.0.0-rc.12",
"node-cache": "^5.1.2",
"winston": "^3.11.0",
"zod": "^4.1.5",
"dotenv": "^16.3.1",
"axios": "^1.6.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/puppeteer": "^7.0.4",
"typescript": "^5.0.0",
"tsx": "^4.0.0",
"vitest": "^1.0.0"
}
}
🏗️ Server Implementation¶
Core Server Structure¶
// src/instagram-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 { BrowserService } from './services/browser-service.js'
import { ProfileScraper } from './services/profile-scraper.js'
import { ContentAnalyzer } from './services/content-analyzer.js'
import { Logger } from './utils/logger.js'
import { z } from 'zod'
const logger = new Logger('InstagramMCP')
class InstagramMCPServer {
private server: Server
private browserService: BrowserService
private profileScraper: ProfileScraper
private contentAnalyzer: ContentAnalyzer
constructor() {
this.server = new Server(
{
name: 'instagram-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
)
this.browserService = new BrowserService()
this.profileScraper = new ProfileScraper(this.browserService)
this.contentAnalyzer = new ContentAnalyzer()
this.setupToolHandlers()
}
private setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'search_instagram_events',
description: 'Search for events on Instagram using hashtags and locations',
inputSchema: {
type: 'object',
properties: {
hashtags: {
type: 'array',
items: { type: 'string' },
description: 'Hashtags to search for event-related content'
},
location: {
type: 'string',
description: 'Location to search for events'
},
date_range: {
type: 'object',
properties: {
start: { type: 'string' },
end: { type: 'string' }
},
description: 'Date range for event search'
},
max_posts: {
type: 'number',
minimum: 1,
maximum: 100,
description: 'Maximum number of posts to analyze'
}
},
required: []
}
},
{
name: 'analyze_instagram_profile',
description: 'Analyze an Instagram profile for event-related content',
inputSchema: {
type: 'object',
properties: {
username: {
type: 'string',
description: 'Instagram username to analyze'
},
include_stories: {
type: 'boolean',
description: 'Whether to include story analysis'
},
max_posts: {
type: 'number',
minimum: 1,
maximum: 50,
description: 'Maximum number of recent posts to analyze'
}
},
required: ['username']
}
},
{
name: 'extract_event_from_post',
description: 'Extract event information from a specific Instagram post',
inputSchema: {
type: 'object',
properties: {
post_url: {
type: 'string',
description: 'URL of the Instagram post'
},
deep_analysis: {
type: 'boolean',
description: 'Whether to perform deep AI analysis of the content'
}
},
required: ['post_url']
}
},
{
name: 'monitor_instagram_accounts',
description: 'Monitor specific Instagram accounts for new event posts',
inputSchema: {
type: 'object',
properties: {
usernames: {
type: 'array',
items: { type: 'string' },
description: 'List of usernames to monitor'
},
keywords: {
type: 'array',
items: { type: 'string' },
description: 'Keywords to look for in posts'
},
check_interval: {
type: 'number',
description: 'Interval in minutes to check for new posts'
}
},
required: ['usernames']
}
}
]
}
})
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
switch (name) {
case 'search_instagram_events':
return await this.handleSearchInstagramEvents(args)
case 'analyze_instagram_profile':
return await this.handleAnalyzeProfile(args)
case 'extract_event_from_post':
return await this.handleExtractEventFromPost(args)
case 'monitor_instagram_accounts':
return await this.handleMonitorAccounts(args)
default:
throw new Error(`Unknown tool: ${name}`)
}
} catch (error) {
logger.error('Tool execution failed:', error)
throw error
}
})
}
private async handleSearchInstagramEvents(args: any) {
const searchParams = InstagramSearchSchema.parse(args)
logger.info('Searching Instagram events:', searchParams)
const posts = await this.profileScraper.searchHashtagPosts(
searchParams.hashtags || ['event', 'party', 'concert'],
searchParams.location,
searchParams.max_posts || 20
)
const events = await this.contentAnalyzer.analyzePostsForEvents(posts)
return {
content: [
{
type: 'text',
text: `Found ${events.length} potential events from ${posts.length} Instagram posts`
}
],
_meta: {
source: 'instagram',
search_params: searchParams,
posts_analyzed: posts.length,
events_found: events.length,
events: events
}
}
}
private async handleAnalyzeProfile(args: any) {
const profileParams = ProfileAnalysisSchema.parse(args)
logger.info('Analyzing Instagram profile:', profileParams.username)
const profileData = await this.profileScraper.scrapeProfile(
profileParams.username,
profileParams.max_posts || 20,
profileParams.include_stories || false
)
const events = await this.contentAnalyzer.analyzeProfileForEvents(profileData)
return {
content: [
{
type: 'text',
text: `Analyzed profile @${profileParams.username} and found ${events.length} potential events`
}
],
_meta: {
source: 'instagram',
profile: profileData.profile,
posts_analyzed: profileData.posts.length,
events_found: events.length,
events: events
}
}
}
private async handleExtractEventFromPost(args: any) {
const postParams = PostExtractionSchema.parse(args)
logger.info('Extracting event from post:', postParams.post_url)
const postData = await this.profileScraper.scrapePost(postParams.post_url)
const event = await this.contentAnalyzer.extractEventFromPost(
postData,
postParams.deep_analysis || false
)
return {
content: [
{
type: 'text',
text: event ? `Extracted event: ${event.title}` : 'No event detected in this post'
}
],
_meta: {
source: 'instagram',
post_url: postParams.post_url,
event_detected: !!event,
event: event
}
}
}
private async handleMonitorAccounts(args: any) {
const monitorParams = AccountMonitoringSchema.parse(args)
logger.info('Setting up monitoring for accounts:', monitorParams.usernames)
// This would typically set up a background job
const results = await this.profileScraper.monitorAccounts(
monitorParams.usernames,
monitorParams.keywords || [],
monitorParams.check_interval || 60
)
return {
content: [
{
type: 'text',
text: `Monitoring setup for ${monitorParams.usernames.length} accounts`
}
],
_meta: {
source: 'instagram',
monitoring_setup: results,
accounts: monitorParams.usernames
}
}
}
async start() {
await this.browserService.initialize()
const transport = new StdioServerTransport()
await this.server.connect(transport)
logger.info('Instagram MCP Server started successfully')
}
async stop() {
await this.browserService.close()
logger.info('Instagram MCP Server stopped')
}
}
// Schema definitions
const InstagramSearchSchema = z.object({
hashtags: z.array(z.string()).optional(),
location: z.string().optional(),
date_range: z.object({
start: z.string(),
end: z.string()
}).optional(),
max_posts: z.number().min(1).max(100).optional()
})
const ProfileAnalysisSchema = z.object({
username: z.string(),
include_stories: z.boolean().optional(),
max_posts: z.number().min(1).max(50).optional()
})
const PostExtractionSchema = z.object({
post_url: z.string().url(),
deep_analysis: z.boolean().optional()
})
const AccountMonitoringSchema = z.object({
usernames: z.array(z.string()),
keywords: z.array(z.string()).optional(),
check_interval: z.number().optional()
})
// Start the server
if (import.meta.url === `file://${process.argv[1]}`) {
const server = new InstagramMCPServer()
process.on('SIGINT', async () => {
await server.stop()
process.exit(0)
})
server.start().catch(console.error)
}
export { InstagramMCPServer }
Browser Service¶
// src/services/browser-service.ts
import puppeteer, { Browser, Page } from 'puppeteer'
import puppeteerExtra from 'puppeteer-extra'
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
import { Logger } from '../utils/logger.js'
puppeteerExtra.use(StealthPlugin())
interface BrowserConfig {
headless: boolean
timeout: number
viewport: {
width: number
height: number
}
userAgent?: string
proxy?: {
host: string
port: number
username?: string
password?: string
}
}
export class BrowserService {
private browser: Browser | null = null
private config: BrowserConfig
private logger: Logger
constructor() {
this.logger = new Logger('BrowserService')
this.config = this.loadConfig()
}
private loadConfig(): BrowserConfig {
return {
headless: process.env.BROWSER_HEADLESS !== 'false',
timeout: parseInt(process.env.BROWSER_TIMEOUT || '30000'),
viewport: {
width: parseInt(process.env.BROWSER_VIEWPORT_WIDTH || '1920'),
height: parseInt(process.env.BROWSER_VIEWPORT_HEIGHT || '1080')
},
userAgent: process.env.USER_AGENT,
proxy: process.env.PROXY_HOST ? {
host: process.env.PROXY_HOST,
port: parseInt(process.env.PROXY_PORT || '8080'),
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD
} : undefined
}
}
async initialize(): Promise<void> {
try {
const launchOptions: any = {
headless: this.config.headless,
defaultViewport: this.config.viewport,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-extensions'
]
}
if (this.config.proxy) {
launchOptions.args.push(
`--proxy-server=${this.config.proxy.host}:${this.config.proxy.port}`
)
}
this.browser = await puppeteerExtra.launch(launchOptions)
this.logger.info('Browser initialized successfully')
} catch (error) {
this.logger.error('Failed to initialize browser:', error)
throw error
}
}
async createPage(): Promise<Page> {
if (!this.browser) {
await this.initialize()
}
const page = await this.browser!.newPage()
// Set user agent
if (this.config.userAgent) {
await page.setUserAgent(this.config.userAgent)
}
// Set timeout
page.setDefaultTimeout(this.config.timeout)
// Block unnecessary resources to speed up loading
await page.setRequestInterception(true)
page.on('request', (request) => {
const resourceType = request.resourceType()
if (['image', 'font', 'media'].includes(resourceType)) {
request.abort()
} else {
request.continue()
}
})
// Add stealth measures
await this.addStealthMeasures(page)
this.logger.debug('Created new page with stealth configuration')
return page
}
private async addStealthMeasures(page: Page): Promise<void> {
// Override navigator properties
await page.evaluateOnNewDocument(() => {
// Remove webdriver property
delete (navigator as any).webdriver
// Override plugins
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
})
// Override languages
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en']
})
// Override permissions
const originalQuery = window.navigator.permissions.query
window.navigator.permissions.query = (parameters) =>
parameters.name === 'notifications'
? Promise.resolve({ state: Notification.permission } as any)
: originalQuery(parameters)
})
// Random delays
if (process.env.RANDOM_DELAYS === 'true') {
await this.randomDelay(1000, 3000)
}
}
async randomDelay(min: number = 1000, max: number = 3000): Promise<void> {
const delay = Math.floor(Math.random() * (max - min + 1)) + min
await new Promise(resolve => setTimeout(resolve, delay))
}
async navigateToInstagram(page: Page): Promise<void> {
try {
await page.goto('https://www.instagram.com/', {
waitUntil: 'networkidle2',
timeout: this.config.timeout
})
await this.randomDelay(2000, 4000)
this.logger.debug('Navigated to Instagram')
} catch (error) {
this.logger.error('Failed to navigate to Instagram:', error)
throw error
}
}
async loginToInstagram(page: Page): Promise<void> {
const username = process.env.INSTAGRAM_USERNAME
const password = process.env.INSTAGRAM_PASSWORD
if (!username || !password) {
this.logger.info('No Instagram credentials provided, continuing without login')
return
}
try {
// Check if already logged in
const isLoggedIn = await page.$('svg[aria-label="Home"]')
if (isLoggedIn) {
this.logger.info('Already logged in to Instagram')
return
}
// Click login button
await page.waitForSelector('a[href="/accounts/login/"]', { timeout: 10000 })
await page.click('a[href="/accounts/login/"]')
await this.randomDelay(2000, 3000)
// Fill in credentials
await page.waitForSelector('input[name="username"]')
await page.type('input[name="username"]', username, { delay: 100 })
await this.randomDelay(500, 1000)
await page.type('input[name="password"]', password, { delay: 100 })
await this.randomDelay(1000, 2000)
// Submit login
await page.click('button[type="submit"]')
await page.waitForNavigation({ waitUntil: 'networkidle2' })
// Handle potential security checks
await this.handleSecurityChecks(page)
this.logger.info('Successfully logged in to Instagram')
} catch (error) {
this.logger.error('Failed to login to Instagram:', error)
throw error
}
}
private async handleSecurityChecks(page: Page): Promise<void> {
try {
// Check for "Save Your Login Info" dialog
const saveLoginButton = await page.$('button:has-text("Not Now")')
if (saveLoginButton) {
await saveLoginButton.click()
await this.randomDelay(1000, 2000)
}
// Check for "Turn on Notifications" dialog
const notificationButton = await page.$('button:has-text("Not Now")')
if (notificationButton) {
await notificationButton.click()
await this.randomDelay(1000, 2000)
}
// Check for suspicious login activity
const suspiciousActivity = await page.$('text="We Detected Unusual Activity"')
if (suspiciousActivity) {
this.logger.warn('Instagram detected unusual activity')
// Handle this case based on your needs
}
} catch (error) {
this.logger.debug('No security checks to handle')
}
}
async closePage(page: Page): Promise<void> {
try {
await page.close()
this.logger.debug('Page closed')
} catch (error) {
this.logger.error('Failed to close page:', error)
}
}
async close(): Promise<void> {
if (this.browser) {
await this.browser.close()
this.browser = null
this.logger.info('Browser closed')
}
}
async restartBrowser(): Promise<void> {
await this.close()
await this.initialize()
this.logger.info('Browser restarted')
}
}
Profile Scraper¶
// src/services/profile-scraper.ts
import { Page } from 'puppeteer'
import { BrowserService } from './browser-service.js'
import { Logger } from '../utils/logger.js'
import * as cheerio from 'cheerio'
interface InstagramPost {
id: string
url: string
caption: string
timestamp: string
likes: number
comments: number
media_urls: string[]
hashtags: string[]
mentions: string[]
location?: {
name: string
id?: string
}
}
interface InstagramProfile {
username: string
display_name: string
bio: string
follower_count: number
following_count: number
post_count: number
profile_pic_url: string
is_verified: boolean
is_business: boolean
external_url?: string
}
interface ProfileData {
profile: InstagramProfile
posts: InstagramPost[]
stories?: any[]
}
export class ProfileScraper {
private browserService: BrowserService
private logger: Logger
constructor(browserService: BrowserService) {
this.browserService = browserService
this.logger = new Logger('ProfileScraper')
}
async scrapeProfile(
username: string,
maxPosts: number = 20,
includeStories: boolean = false
): Promise<ProfileData> {
const page = await this.browserService.createPage()
try {
await this.browserService.navigateToInstagram(page)
await this.browserService.loginToInstagram(page)
// Navigate to profile
await page.goto(`https://www.instagram.com/${username}/`, {
waitUntil: 'networkidle2'
})
// Extract profile information
const profile = await this.extractProfileInfo(page, username)
// Extract posts
const posts = await this.extractPosts(page, maxPosts)
// Extract stories if requested
let stories: any[] = []
if (includeStories) {
stories = await this.extractStories(page, username)
}
this.logger.info(`Scraped profile @${username}: ${posts.length} posts`)
return {
profile,
posts,
stories: includeStories ? stories : undefined
}
} catch (error) {
this.logger.error(`Failed to scrape profile @${username}:`, error)
throw error
} finally {
await this.browserService.closePage(page)
}
}
async scrapePost(postUrl: string): Promise<InstagramPost> {
const page = await this.browserService.createPage()
try {
await this.browserService.navigateToInstagram(page)
await this.browserService.loginToInstagram(page)
await page.goto(postUrl, { waitUntil: 'networkidle2' })
const post = await this.extractSinglePost(page, postUrl)
this.logger.info(`Scraped post: ${postUrl}`)
return post
} catch (error) {
this.logger.error(`Failed to scrape post ${postUrl}:`, error)
throw error
} finally {
await this.browserService.closePage(page)
}
}
async searchHashtagPosts(
hashtags: string[],
location?: string,
maxPosts: number = 20
): Promise<InstagramPost[]> {
const page = await this.browserService.createPage()
const allPosts: InstagramPost[] = []
try {
await this.browserService.navigateToInstagram(page)
await this.browserService.loginToInstagram(page)
for (const hashtag of hashtags) {
this.logger.info(`Searching hashtag: #${hashtag}`)
const hashtagUrl = `https://www.instagram.com/explore/tags/${hashtag}/`
await page.goto(hashtagUrl, { waitUntil: 'networkidle2' })
const posts = await this.extractHashtagPosts(page, maxPosts / hashtags.length)
allPosts.push(...posts)
await this.browserService.randomDelay(2000, 4000)
}
// Filter by location if specified
if (location) {
return this.filterPostsByLocation(allPosts, location)
}
this.logger.info(`Found ${allPosts.length} posts from hashtag search`)
return allPosts.slice(0, maxPosts)
} catch (error) {
this.logger.error('Failed to search hashtag posts:', error)
throw error
} finally {
await this.browserService.closePage(page)
}
}
private async extractProfileInfo(page: Page, username: string): Promise<InstagramProfile> {
try {
// Wait for profile data to load
await page.waitForSelector('header section', { timeout: 10000 })
const profileData = await page.evaluate(() => {
// Extract from page script tags or DOM elements
const scripts = Array.from(document.querySelectorAll('script'))
for (const script of scripts) {
if (script.textContent?.includes('window._sharedData')) {
const sharedData = script.textContent.match(/window\._sharedData = ({.*?});/)
if (sharedData) {
try {
const data = JSON.parse(sharedData[1])
const user = data.entry_data?.ProfilePage?.[0]?.graphql?.user
if (user) return user
} catch (e) {
// Continue to DOM extraction
}
}
}
}
// Fallback to DOM extraction
const header = document.querySelector('header section')
if (!header) return null
return {
username: document.querySelector('h2')?.textContent || username,
display_name: document.querySelector('h1')?.textContent || '',
bio: document.querySelector('div[data-testid="user-bio"]')?.textContent || '',
follower_count: this.extractCountFromElement('a[href*="/followers/"] span'),
following_count: this.extractCountFromElement('a[href*="/following/"] span'),
post_count: this.extractCountFromElement('div:first-child span'),
profile_pic_url: document.querySelector('img[alt*="profile picture"]')?.src || '',
is_verified: !!document.querySelector('[data-testid="verified-icon"]'),
is_business: !!document.querySelector('[data-testid="business-icon"]')
}
})
return {
username,
display_name: profileData?.full_name || profileData?.display_name || '',
bio: profileData?.biography || '',
follower_count: profileData?.edge_followed_by?.count || 0,
following_count: profileData?.edge_follow?.count || 0,
post_count: profileData?.edge_owner_to_timeline_media?.count || 0,
profile_pic_url: profileData?.profile_pic_url || '',
is_verified: profileData?.is_verified || false,
is_business: profileData?.is_business_account || false,
external_url: profileData?.external_url
}
} catch (error) {
this.logger.error('Failed to extract profile info:', error)
return {
username,
display_name: '',
bio: '',
follower_count: 0,
following_count: 0,
post_count: 0,
profile_pic_url: '',
is_verified: false,
is_business: false
}
}
}
private async extractPosts(page: Page, maxPosts: number): Promise<InstagramPost[]> {
const posts: InstagramPost[] = []
try {
// Scroll to load posts
let loadedPosts = 0
while (loadedPosts < maxPosts) {
const postElements = await page.$$('article a[href*="/p/"]')
if (postElements.length === 0) break
for (let i = loadedPosts; i < Math.min(postElements.length, maxPosts); i++) {
try {
const postUrl = await postElements[i].evaluate(el => el.href)
const post = await this.extractPostFromGrid(page, postUrl, i)
if (post) posts.push(post)
loadedPosts++
} catch (error) {
this.logger.debug(`Failed to extract post at index ${i}:`, error)
}
}
if (loadedPosts < maxPosts) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await this.browserService.randomDelay(2000, 3000)
}
}
} catch (error) {
this.logger.error('Failed to extract posts:', error)
}
return posts
}
private async extractPostFromGrid(page: Page, postUrl: string, index: number): Promise<InstagramPost | null> {
try {
// Extract basic info from grid view
const postData = await page.evaluate((url, idx) => {
const postElement = document.querySelectorAll('article a[href*="/p/"]')[idx]
if (!postElement) return null
const img = postElement.querySelector('img')
const timeElement = postElement.querySelector('time')
return {
url: url,
media_urls: img ? [img.src] : [],
timestamp: timeElement ? timeElement.getAttribute('datetime') : new Date().toISOString()
}
}, postUrl, index)
if (!postData) return null
// Extract post ID from URL
const postIdMatch = postUrl.match(/\/p\/([^\/]+)\//)
const postId = postIdMatch ? postIdMatch[1] : `post_${Date.now()}_${index}`
return {
id: postId,
url: postUrl,
caption: '', // Will be filled by detailed extraction if needed
timestamp: postData.timestamp,
likes: 0,
comments: 0,
media_urls: postData.media_urls,
hashtags: [],
mentions: [],
location: undefined
}
} catch (error) {
this.logger.debug(`Failed to extract post from grid:`, error)
return null
}
}
private async extractSinglePost(page: Page, postUrl: string): Promise<InstagramPost> {
try {
await page.waitForSelector('article', { timeout: 10000 })
const postData = await page.evaluate(() => {
const article = document.querySelector('article')
if (!article) throw new Error('Post not found')
// Extract caption
const captionElement = article.querySelector('div[data-testid="post-caption"] span')
const caption = captionElement?.textContent || ''
// Extract hashtags and mentions from caption
const hashtags = (caption.match(/#\w+/g) || []).map(tag => tag.slice(1))
const mentions = (caption.match(/@\w+/g) || []).map(mention => mention.slice(1))
// Extract media URLs
const images = Array.from(article.querySelectorAll('img[src*="scontent"]'))
const videos = Array.from(article.querySelectorAll('video source'))
const media_urls = [
...images.map(img => img.src),
...videos.map(video => video.src)
]
// Extract engagement metrics
const likesElement = article.querySelector('button[data-testid="like-button"] + span')
const likes = this.parseEngagementCount(likesElement?.textContent || '0')
// Extract timestamp
const timeElement = article.querySelector('time')
const timestamp = timeElement?.getAttribute('datetime') || new Date().toISOString()
// Extract location
const locationElement = article.querySelector('a[href*="/locations/"]')
const location = locationElement ? {
name: locationElement.textContent || '',
id: locationElement.href.match(/\/locations\/(\d+)\//)?.[1]
} : undefined
return {
caption,
hashtags,
mentions,
media_urls,
likes,
timestamp,
location
}
})
const postIdMatch = postUrl.match(/\/p\/([^\/]+)\//)
const postId = postIdMatch ? postIdMatch[1] : `post_${Date.now()}`
return {
id: postId,
url: postUrl,
caption: postData.caption,
timestamp: postData.timestamp,
likes: postData.likes,
comments: 0, // Comments require separate extraction
media_urls: postData.media_urls,
hashtags: postData.hashtags,
mentions: postData.mentions,
location: postData.location
}
} catch (error) {
this.logger.error('Failed to extract single post:', error)
throw error
}
}
private async extractHashtagPosts(page: Page, maxPosts: number): Promise<InstagramPost[]> {
const posts: InstagramPost[] = []
try {
// Wait for posts to load
await page.waitForSelector('article a[href*="/p/"]', { timeout: 10000 })
const postLinks = await page.$$eval('article a[href*="/p/"]', links =>
links.slice(0, maxPosts).map(link => link.href)
)
for (const postUrl of postLinks) {
try {
// Extract basic info without navigating to each post
const postIdMatch = postUrl.match(/\/p\/([^\/]+)\//)
const postId = postIdMatch ? postIdMatch[1] : `hashtag_post_${Date.now()}`
posts.push({
id: postId,
url: postUrl,
caption: '',
timestamp: new Date().toISOString(),
likes: 0,
comments: 0,
media_urls: [],
hashtags: [],
mentions: [],
location: undefined
})
} catch (error) {
this.logger.debug(`Failed to process hashtag post ${postUrl}:`, error)
}
}
} catch (error) {
this.logger.error('Failed to extract hashtag posts:', error)
}
return posts
}
private async extractStories(page: Page, username: string): Promise<any[]> {
try {
// Stories require special handling and may not always be available
// This is a simplified implementation
const storiesButton = await page.$(`a[href="/${username}/"] img[alt*="story"]`)
if (!storiesButton) {
this.logger.debug('No stories available for this user')
return []
}
// Stories extraction would require more complex implementation
// due to Instagram's story viewing restrictions
return []
} catch (error) {
this.logger.debug('Failed to extract stories:', error)
return []
}
}
private filterPostsByLocation(posts: InstagramPost[], location: string): InstagramPost[] {
const locationLower = location.toLowerCase()
return posts.filter(post => {
if (post.location?.name?.toLowerCase().includes(locationLower)) {
return true
}
if (post.caption?.toLowerCase().includes(locationLower)) {
return true
}
return false
})
}
async monitorAccounts(
usernames: string[],
keywords: string[],
checkInterval: number
): Promise<any> {
// This would implement a monitoring system
// For now, return setup confirmation
this.logger.info(`Setting up monitoring for ${usernames.length} accounts`)
return {
status: 'monitoring_setup',
accounts: usernames,
keywords: keywords,
interval_minutes: checkInterval,
next_check: new Date(Date.now() + checkInterval * 60000).toISOString()
}
}
}
Content Analyzer¶
// src/services/content-analyzer.ts
import OpenAI from 'openai'
import { Logger } from '../utils/logger.js'
import { InstagramPost, ProfileData } from './profile-scraper.js'
interface ExtractedEvent {
id: string
title: string
description: string
start_date?: string
end_date?: string
location?: {
name: string
address?: string
coordinates?: {
lat: number
lng: number
}
}
category: string
organizer: {
name: string
instagram: string
}
ticket_info?: {
is_free: boolean
price?: string
booking_url?: string
}
source_post: {
url: string
instagram_id: string
}
confidence_score: number
extracted_at: string
}
export class ContentAnalyzer {
private openai: OpenAI
private logger: Logger
constructor() {
this.logger = new Logger('ContentAnalyzer')
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: InstagramPost[]): Promise<ExtractedEvent[]> {
this.logger.info(`Analyzing ${posts.length} posts for events`)
const events: ExtractedEvent[] = []
for (const post of posts) {
try {
const event = await this.extractEventFromPost(post, true)
if (event) {
events.push(event)
}
} catch (error) {
this.logger.debug(`Failed to analyze post ${post.id}:`, error)
}
}
this.logger.info(`Extracted ${events.length} events from ${posts.length} posts`)
return events
}
async analyzeProfileForEvents(profileData: ProfileData): Promise<ExtractedEvent[]> {
this.logger.info(`Analyzing profile @${profileData.profile.username} for events`)
// First, check if this is an event-related profile
const isEventProfile = await this.isEventRelatedProfile(profileData.profile)
if (!isEventProfile) {
this.logger.debug(`Profile @${profileData.profile.username} doesn't appear to be event-related`)
return []
}
return this.analyzePostsForEvents(profileData.posts)
}
async extractEventFromPost(
post: InstagramPost,
deepAnalysis: boolean = false
): Promise<ExtractedEvent | null> {
try {
// Quick keyword-based filtering first
if (!this.containsEventKeywords(post.caption)) {
return null
}
if (deepAnalysis) {
return await this.performAIAnalysis(post)
} else {
return await this.performBasicAnalysis(post)
}
} catch (error) {
this.logger.error(`Failed to extract event from post ${post.id}:`, error)
return null
}
}
private async isEventRelatedProfile(profile: any): Promise<boolean> {
const eventKeywords = [
'event', 'events', 'party', 'concert', 'festival', 'show', 'performance',
'venue', 'club', 'bar', 'restaurant', 'theater', 'museum', 'gallery',
'conference', 'workshop', 'seminar', 'meetup', 'gathering', 'celebration'
]
const profileText = `${profile.bio} ${profile.display_name}`.toLowerCase()
return eventKeywords.some(keyword => profileText.includes(keyword))
}
private containsEventKeywords(text: string): boolean {
const eventKeywords = [
'event', 'tonight', 'tomorrow', 'this weekend', 'next week',
'join us', 'come to', 'happening', 'live music', 'performance',
'show', 'concert', 'party', 'celebration', 'festival',
'workshop', 'seminar', 'meetup', 'conference',
'tickets', 'rsvp', 'register', 'book now',
'when:', 'where:', 'time:', 'date:', 'location:'
]
const textLower = text.toLowerCase()
return eventKeywords.some(keyword => textLower.includes(keyword))
}
private async performBasicAnalysis(post: InstagramPost): Promise<ExtractedEvent | null> {
// Basic pattern matching for event information
const caption = post.caption
// Extract potential dates
const datePatterns = [
/(\d{1,2}\/\d{1,2}\/\d{4})/g,
/(\d{1,2}-\d{1,2}-\d{4})/g,
/(monday|tuesday|wednesday|thursday|friday|saturday|sunday)/gi,
/(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d{1,2}/gi
]
let extractedDate: string | undefined
for (const pattern of datePatterns) {
const match = caption.match(pattern)
if (match) {
extractedDate = match[0]
break
}
}
// Extract potential times
const timePattern = /(\d{1,2}:\d{2}\s*(am|pm|AM|PM)?)/g
const timeMatch = caption.match(timePattern)
const extractedTime = timeMatch ? timeMatch[0] : undefined
// Extract potential location
const locationPatterns = [
/at\s+([A-Z][A-Za-z\s]+)/g,
/location:\s*([A-Za-z\s]+)/gi,
/where:\s*([A-Za-z\s]+)/gi
]
let extractedLocation: string | undefined
for (const pattern of locationPatterns) {
const match = caption.match(pattern)
if (match && match[1]) {
extractedLocation = match[1].trim()
break
}
}
// If we found event indicators, create a basic event
if (extractedDate || extractedTime || extractedLocation) {
return {
id: `extracted_${post.id}`,
title: this.extractTitle(caption),
description: caption,
start_date: this.constructDateTime(extractedDate, extractedTime),
location: extractedLocation ? {
name: extractedLocation
} : post.location,
category: this.inferCategory(caption),
organizer: {
name: 'Instagram User', // Would need profile info
instagram: post.url.split('/')[3] || ''
},
source_post: {
url: post.url,
instagram_id: post.id
},
confidence_score: 0.6,
extracted_at: new Date().toISOString()
}
}
return null
}
private async performAIAnalysis(post: InstagramPost): Promise<ExtractedEvent | null> {
try {
const prompt = this.buildAnalysisPrompt(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 extracting event information from Instagram posts. Analyze the content and extract structured event data if an event is being promoted.'
},
{
role: 'user',
content: prompt
}
],
functions: [
{
name: 'extract_event',
description: 'Extract event information from Instagram post',
parameters: {
type: 'object',
properties: {
is_event: {
type: 'boolean',
description: 'Whether this post is promoting an event'
},
event_title: {
type: 'string',
description: 'Title or name of the event'
},
event_description: {
type: 'string',
description: 'Description of the event'
},
start_date: {
type: 'string',
description: 'Event start date in ISO format'
},
end_date: {
type: 'string',
description: 'Event end date in ISO format (if different from start)'
},
location_name: {
type: 'string',
description: 'Name of the event location'
},
location_address: {
type: 'string',
description: 'Address of the event location'
},
category: {
type: 'string',
description: 'Event category (music, art, food, sports, etc.)'
},
is_free: {
type: 'boolean',
description: 'Whether the event is free'
},
ticket_price: {
type: 'string',
description: 'Ticket price if mentioned'
},
booking_info: {
type: 'string',
description: 'How to book or get tickets'
},
confidence_score: {
type: 'number',
description: 'Confidence score from 0 to 1'
}
},
required: ['is_event', 'confidence_score']
}
}
],
function_call: { name: 'extract_event' }
})
const functionCall = response.choices[0]?.message?.function_call
if (!functionCall || functionCall.name !== 'extract_event') {
return null
}
const extractedData = JSON.parse(functionCall.arguments)
if (!extractedData.is_event || extractedData.confidence_score < 0.5) {
return null
}
return {
id: `ai_extracted_${post.id}`,
title: extractedData.event_title || this.extractTitle(post.caption),
description: extractedData.event_description || post.caption,
start_date: extractedData.start_date,
end_date: extractedData.end_date,
location: {
name: extractedData.location_name || post.location?.name || '',
address: extractedData.location_address
},
category: extractedData.category || 'other',
organizer: {
name: 'Instagram User',
instagram: post.url.split('/')[3] || ''
},
ticket_info: {
is_free: extractedData.is_free || false,
price: extractedData.ticket_price,
booking_url: extractedData.booking_info
},
source_post: {
url: post.url,
instagram_id: post.id
},
confidence_score: extractedData.confidence_score,
extracted_at: new Date().toISOString()
}
} catch (error) {
this.logger.error('AI analysis failed:', error)
return this.performBasicAnalysis(post)
}
}
private buildAnalysisPrompt(post: InstagramPost): string {
return `
Analyze this Instagram post for event information:
Caption: ${post.caption}
Hashtags: ${post.hashtags.join(', ')}
${post.location ? `Location: ${post.location.name}` : ''}
Posted: ${post.timestamp}
Media URLs: ${post.media_urls.slice(0, 3).join(', ')}
Please determine if this post is promoting an event and extract all relevant details including:
- Event title and description
- Date and time
- Location details
- Event category
- Ticket information
- How to attend/book
Provide a confidence score based on how clear the event information is.
`
}
private extractTitle(caption: string): string {
// Extract potential title from the first line or sentence
const lines = caption.split('\n').filter(line => line.trim())
if (lines.length > 0) {
const firstLine = lines[0].trim()
// Remove hashtags and mentions from title
return firstLine.replace(/#\w+/g, '').replace(/@\w+/g, '').trim().slice(0, 100)
}
return 'Instagram Event'
}
private constructDateTime(date?: string, time?: string): string | undefined {
if (!date && !time) return undefined
try {
const now = new Date()
let eventDate = now
if (date) {
eventDate = new Date(date)
if (isNaN(eventDate.getTime())) {
eventDate = now
}
}
if (time) {
const timeMatch = time.match(/(\d{1,2}):(\d{2})\s*(am|pm|AM|PM)?/)
if (timeMatch) {
let hours = parseInt(timeMatch[1])
const minutes = parseInt(timeMatch[2])
const ampm = timeMatch[3]?.toLowerCase()
if (ampm === 'pm' && hours !== 12) {
hours += 12
} else if (ampm === 'am' && hours === 12) {
hours = 0
}
eventDate.setHours(hours, minutes, 0, 0)
}
}
return eventDate.toISOString()
} catch (error) {
return undefined
}
}
private inferCategory(caption: string): string {
const categoryKeywords = {
music: ['concert', 'music', 'band', 'dj', 'live music', 'performance'],
food: ['food', 'restaurant', 'dining', 'taste', 'menu', 'chef'],
art: ['art', 'gallery', 'exhibition', 'artist', 'painting', 'sculpture'],
sports: ['game', 'match', 'sports', 'team', 'tournament', 'athletic'],
business: ['conference', 'workshop', 'seminar', 'networking', 'business'],
entertainment: ['show', 'comedy', 'theater', 'movie', 'entertainment'],
social: ['party', 'celebration', 'gathering', 'meetup', 'social']
}
const captionLower = caption.toLowerCase()
for (const [category, keywords] of Object.entries(categoryKeywords)) {
if (keywords.some(keyword => captionLower.includes(keyword))) {
return category
}
}
return 'other'
}
}
This comprehensive Instagram MCP server documentation covers web scraping, content analysis, and AI-powered event extraction from Instagram! 📸