Skip to main content
Glama

Instagram Engagement MCP

index.ts34.8 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import { IgApiClient } from 'instagram-private-api'; import * as dotenv from 'dotenv'; // Load environment variables dotenv.config(); // Instagram API credentials const INSTAGRAM_USERNAME = process.env.INSTAGRAM_USERNAME; const INSTAGRAM_PASSWORD = process.env.INSTAGRAM_PASSWORD; // Validate required environment variables if (!INSTAGRAM_USERNAME || !INSTAGRAM_PASSWORD) { console.error('[Error] Missing required environment variables: INSTAGRAM_USERNAME and INSTAGRAM_PASSWORD'); process.exit(1); } // Type definitions for our tools interface AnalyzeCommentsArgs { postUrl: string; maxComments?: number; } interface CompareAccountsArgs { accounts: string[]; metrics?: string[]; } interface ExtractDemographicsArgs { accountOrPostUrl: string; sampleSize?: number; } interface IdentifyLeadsArgs { accountOrPostUrl: string; criteria?: { minComments?: number; minFollowers?: number; keywords?: string[]; }; } interface GenerateReportArgs { account: string; startDate?: string; endDate?: string; } // Utility function to validate post URL const isValidPostUrl = (url: string): boolean => { return /^https:\/\/(www\.)?instagram\.com\/p\/[A-Za-z0-9_-]+\/?/.test(url); }; // Utility function to extract post ID from URL const extractPostIdFromUrl = (url: string): string => { const match = url.match(/\/p\/([A-Za-z0-9_-]+)/); return match ? match[1] : ''; }; // Utility function to validate Instagram username const isValidUsername = (username: string): boolean => { return /^[A-Za-z0-9._]+$/.test(username); }; class InstagramEngagementServer { private server: Server; private ig: IgApiClient; private isLoggedIn: boolean = false; constructor() { console.error('[Setup] Initializing Instagram Engagement MCP server...'); this.server = new Server( { name: 'instagram-engagement-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.ig = new IgApiClient(); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private async loginToInstagram(): Promise<boolean> { if (this.isLoggedIn) return true; // Add retry logic or more robust error handling if needed try { console.error('[Auth] Attempting to log in to Instagram...'); this.ig.state.generateDevice(INSTAGRAM_USERNAME!); // Optional: Add proxy support if needed // this.ig.state.proxyUrl = process.env.IG_PROXY; await this.ig.account.login(INSTAGRAM_USERNAME!, INSTAGRAM_PASSWORD!); this.isLoggedIn = true; console.error('[Auth] Successfully logged in to Instagram'); return true; } catch (error: any) { console.error('[Auth Error] Failed to log in to Instagram:', error.message || error); // Consider specific error handling (e.g., ChallengeRequiredError) return false; } } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'analyze_post_comments', description: 'Analyze comments on an Instagram post to identify sentiment, themes, and potential leads', inputSchema: { type: 'object', properties: { postUrl: { type: 'string', description: 'URL of the Instagram post to analyze', }, maxComments: { type: 'number', description: 'Maximum number of comments to analyze (default: 100)', }, }, required: ['postUrl'], }, }, { name: 'compare_accounts', description: 'Compare engagement metrics across different Instagram accounts', inputSchema: { type: 'object', properties: { accounts: { type: 'array', items: { type: 'string', }, description: 'List of Instagram account handles to compare', }, metrics: { type: 'array', items: { type: 'string', enum: ['followers', 'engagement', 'posts', 'comments', 'likes'], }, description: 'Metrics to compare (default: all)', }, }, required: ['accounts'], }, }, { name: 'extract_demographics', description: 'Extract demographic insights from users engaged with a post or account', inputSchema: { type: 'object', properties: { accountOrPostUrl: { type: 'string', description: 'Instagram account handle or post URL to analyze', }, sampleSize: { type: 'number', description: 'Number of users to sample for demographic analysis (default: 50)', }, }, required: ['accountOrPostUrl'], }, }, { name: 'identify_leads', description: 'Identify potential leads based on engagement patterns', inputSchema: { type: 'object', properties: { accountOrPostUrl: { type: 'string', description: 'Instagram account handle or post URL to analyze', }, criteria: { type: 'object', properties: { minComments: { type: 'number', description: 'Minimum number of comments from a user', }, minFollowers: { type: 'number', description: 'Minimum number of followers a user should have', }, keywords: { type: 'array', items: { type: 'string', }, description: 'Keywords to look for in user comments or bio', }, }, description: 'Criteria for identifying leads', }, }, required: ['accountOrPostUrl'], }, }, { name: 'generate_engagement_report', description: 'Generate a comprehensive engagement report for an Instagram account', inputSchema: { type: 'object', properties: { account: { type: 'string', description: 'Instagram account handle', }, startDate: { type: 'string', description: 'Start date for the report (YYYY-MM-DD)', }, endDate: { type: 'string', description: 'End date for the report (YYYY-MM-DD)', }, }, required: ['account'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { console.error(`[Tool] Request to execute tool: ${request.params.name}`); // Ensure we're logged in to Instagram const loggedIn = await this.loginToInstagram(); if (!loggedIn) { throw new McpError( ErrorCode.InternalError, 'Failed to authenticate with Instagram' ); } const args = request.params.arguments || {}; try { switch (request.params.name) { case 'analyze_post_comments': return await this.handleAnalyzePostComments(args as unknown as AnalyzeCommentsArgs); case 'compare_accounts': return await this.handleCompareAccounts(args as unknown as CompareAccountsArgs); case 'extract_demographics': return await this.handleExtractDemographics(args as unknown as ExtractDemographicsArgs); case 'identify_leads': return await this.handleIdentifyLeads(args as unknown as IdentifyLeadsArgs); case 'generate_engagement_report': return await this.handleGenerateReport(args as unknown as GenerateReportArgs); default: throw new McpError(ErrorCode.MethodNotFound, `Tool ${request.params.name} not found`); } } catch (error: any) { console.error(`[Tool Error] Error executing tool ${request.params.name}:`, error.message || error); if (error instanceof McpError) { throw error; } // Map specific API errors to MCP errors if (error.name === 'IgNotFoundError') { throw new McpError(ErrorCode.InvalidParams, `Instagram resource not found: ${error.message}`); } if (error.name === 'IgLoginRequiredError' || error.name === 'IgCheckpointError') { this.isLoggedIn = false; // Force re-login on next attempt throw new McpError(ErrorCode.InternalError, `Instagram login required or challenge encountered: ${error.message}`); } if (error.name === 'IgRequestsLimitError') { throw new McpError(ErrorCode.InternalError, `Instagram rate limit hit: ${error.message}`); } // Generic internal error for other cases throw new McpError(ErrorCode.InternalError, `An unexpected error occurred while executing the tool: ${error.message || 'Unknown error'}`); } }); } private async handleAnalyzePostComments(args: AnalyzeCommentsArgs) { console.error('[Tool] handleAnalyzePostComments called with args:', args); const { postUrl, maxComments = 100 } = args; if (!isValidPostUrl(postUrl)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid post URL format.'); } const mediaId = await this.getMediaIdFromUrl(postUrl); if (!mediaId) { throw new McpError(ErrorCode.InvalidParams, 'Could not extract media ID from post URL.'); } console.error(`[Tool] Analyzing comments for media ID: ${mediaId}`); try { const commentsFeed = this.ig.feed.mediaComments(mediaId); let comments: any[] = []; let commentCount = 0; // Basic pagination handling do { const items = await commentsFeed.items(); comments = comments.concat(items); commentCount += items.length; console.error(`[Tool] Fetched ${items.length} comments (total: ${commentCount})`); if (commentCount >= maxComments) break; await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 500)); // Small delay } while (commentsFeed.isMoreAvailable()); comments = comments.slice(0, maxComments); // Trim to maxComments console.error(`[Tool] Analyzing ${comments.length} comments.`); // Basic analysis (replace with more sophisticated logic if needed) const analysis = { totalCommentsFetched: comments.length, // Placeholder for sentiment/themes - requires NLP library sentiment: 'neutral', topThemes: ['general', 'engagement'], potentialLeads: comments.filter(c => c.text.includes('interested') || c.text.includes('DM')).map(c => ({ username: c.user.username, comment: c.text.substring(0, 100), // Truncate long comments })), sampleComments: comments.slice(0, 5).map(c => ({ username: c.user.username, text: c.text.substring(0, 100), timestamp: new Date(c.created_at_utc * 1000).toISOString(), })), }; return { results: analysis }; } catch (error: any) { console.error(`[API Error] Failed to analyze comments for ${mediaId}:`, error.message || error); // Re-throw as McpError or handle specifically if (error.name === 'IgNotFoundError') { throw new McpError(ErrorCode.InvalidParams, `Post with media ID ${mediaId} not found or access denied.`); } throw new McpError(ErrorCode.InternalError, `Failed to fetch or analyze comments: ${error.message}`); } } private async handleCompareAccounts(args: CompareAccountsArgs) { console.error('[Tool] handleCompareAccounts called with args:', args); const { accounts, metrics = ['followers', 'engagement', 'posts'] } = args; if (!accounts || accounts.length === 0) { throw new McpError(ErrorCode.InvalidParams, 'At least one account handle must be provided.'); } if (accounts.some(acc => !isValidUsername(acc))) { throw new McpError(ErrorCode.InvalidParams, 'One or more account handles are invalid.'); } const comparisonResults: any = {}; for (const username of accounts) { console.error(`[Tool] Fetching data for account: ${username}`); try { const userId = await this.ig.user.getIdByUsername(username); const userInfo = await this.ig.user.info(userId); // Basic metrics calculation const followerCount = userInfo.follower_count; const followingCount = userInfo.following_count; const postCount = userInfo.media_count; // Placeholder for engagement - requires fetching recent posts and calculating average likes/comments let engagementRate = 0; if (followerCount > 0) { // Fetch recent posts - limited scope for example const postsFeed = this.ig.feed.user(userId); const recentPosts = await postsFeed.items(); if (recentPosts.length > 0) { const totalLikes = recentPosts.reduce((sum, post) => sum + (post.like_count || 0), 0); const totalComments = recentPosts.reduce((sum, post) => sum + (post.comment_count || 0), 0); const avgLikes = totalLikes / recentPosts.length; const avgComments = totalComments / recentPosts.length; engagementRate = ((avgLikes + avgComments) / followerCount) * 100; } } comparisonResults[username] = { userId: userId, fullName: userInfo.full_name, isPrivate: userInfo.is_private, isVerified: userInfo.is_verified, followers: metrics.includes('followers') ? followerCount : undefined, following: metrics.includes('following') ? followingCount : undefined, // Added following as potential metric posts: metrics.includes('posts') ? postCount : undefined, engagementRate: metrics.includes('engagement') ? parseFloat(engagementRate.toFixed(2)) : undefined, // Simplified engagement // 'likes' and 'comments' metrics would typically be per post, not overall account. }; console.error(`[Tool] Successfully fetched data for ${username}`); await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 400)); // Small delay } catch (error: any) { console.error(`[API Error] Failed to get info for account ${username}:`, error.message || error); comparisonResults[username] = { error: `Failed to fetch data: ${error.message}` }; if (error.name === 'IgNotFoundError') { comparisonResults[username] = { error: 'Account not found.' }; } } } return { results: comparisonResults }; } private async handleExtractDemographics(args: ExtractDemographicsArgs) { console.error('[Tool] handleExtractDemographics called with args:', args); const { accountOrPostUrl, sampleSize = 50 } = args; // Determine if it's a post URL or username let targetId: string; let targetType: 'account' | 'post'; if (isValidPostUrl(accountOrPostUrl)) { targetType = 'post'; const mediaId = await this.getMediaIdFromUrl(accountOrPostUrl); if (!mediaId) { throw new McpError(ErrorCode.InvalidParams, 'Could not extract media ID from post URL.'); } targetId = mediaId; console.error(`[Tool] Extracting demographics from post: ${targetId}`); } else if (isValidUsername(accountOrPostUrl)) { targetType = 'account'; try { const userId = await this.ig.user.getIdByUsername(accountOrPostUrl); targetId = String(userId); // Convert number userId to string for targetId console.error(`[Tool] Extracting demographics from account followers: ${accountOrPostUrl} (ID: ${targetId})`); } catch(e: any) { if (e.name === 'IgNotFoundError') { throw new McpError(ErrorCode.InvalidParams, `Account ${accountOrPostUrl} not found.`); } throw new McpError(ErrorCode.InternalError, `Failed to get user ID for ${accountOrPostUrl}: ${e.message}`); } } else { throw new McpError(ErrorCode.InvalidParams, 'Invalid input. Provide a valid Instagram username or post URL.'); } try { let users: any[] = []; if (targetType === 'post') { // Get likers or commenters as sample console.error(`[Tool Debug] Attempting to fetch likers for media ID: ${targetId}`); const likersResponse = await this.ig.media.likers(targetId); let fetchedUsers: any[] = likersResponse.users || []; // Access users from the response object // Manual pagination simulation (if needed and possible, likers might not support feed pagination) // The private API might not offer easy pagination for likers beyond the initial batch. // For simplicity, we'll just use the first batch returned. /* let fetchedUsers: any[] = []; do { // Feed results are usually directly items const items = await likersFeed.items(); fetchedUsers = fetchedUsers.concat(items); if (fetchedUsers.length >= sampleSize) break; await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300)); } while (likersFeed.isMoreAvailable()); */ users = fetchedUsers.slice(0, sampleSize); console.error(`[Tool] Fetched ${users.length} likers from post ${targetId}`); } else { // targetType === 'account' const userIdNum = parseInt(targetId, 10); const followersFeed = this.ig.feed.accountFollowers(userIdNum); let fetchedUsers: any[] = []; do { const items = await followersFeed.items(); fetchedUsers = fetchedUsers.concat(items); if (fetchedUsers.length >= sampleSize) break; await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300)); } while (followersFeed.isMoreAvailable()); users = fetchedUsers.slice(0, sampleSize); console.error(`[Tool] Fetched ${users.length} followers from account ${accountOrPostUrl}`); } if (users.length === 0) { return { results: { message: 'No users found to analyze (post might have no likes/comments, or account has no followers/is private).', demographics: {} } }; } // Placeholder for actual demographic analysis const demographics = { sampleAnalyzed: users.length, commonLocationsGuess: ['Unknown'], genderDistributionGuess: { male: 0.4, female: 0.4, unknown: 0.2 }, accountTypes: { private: users.filter(u => u.is_private).length / users.length, verified: users.filter(u => u.is_verified).length / users.length, }, sampleUserProfiles: users.slice(0, 5).map(u => ({ username: u.username, fullName: u.full_name, isPrivate: u.is_private, })) }; return { results: { demographics } }; } catch (error: any) { console.error(`[API Error] Failed to extract demographics for ${accountOrPostUrl}:`, error.message || error); if (error.name === 'IgNotFoundError') { throw new McpError(ErrorCode.InvalidParams, `${targetType === 'post' ? 'Post' : 'Account'} not found or access denied.`); } throw new McpError(ErrorCode.InternalError, `Failed to fetch users for demographic analysis: ${error.message}`); } } private async handleIdentifyLeads(args: IdentifyLeadsArgs) { console.error('[Tool] handleIdentifyLeads called with args:', args); const { accountOrPostUrl, criteria = {} } = args; const { minComments, minFollowers, keywords } = criteria; let targetId: string; let targetType: 'account' | 'post'; let sourceDescription: string; if (isValidPostUrl(accountOrPostUrl)) { targetType = 'post'; const mediaId = await this.getMediaIdFromUrl(accountOrPostUrl); if (!mediaId) { throw new McpError(ErrorCode.InvalidParams, 'Could not extract media ID from post URL.'); } targetId = mediaId; sourceDescription = `comments on post ${targetId}`; console.error(`[Tool] Identifying leads from post: ${targetId}`); } else if (isValidUsername(accountOrPostUrl)) { targetType = 'account'; try { const userId = await this.ig.user.getIdByUsername(accountOrPostUrl); targetId = String(userId); // Convert number userId to string for targetId sourceDescription = `followers of account ${accountOrPostUrl}`; console.error(`[Tool] Identifying leads from account followers: ${accountOrPostUrl} (ID: ${targetId})`); } catch(e: any) { if (e.name === 'IgNotFoundError') { throw new McpError(ErrorCode.InvalidParams, `Account ${accountOrPostUrl} not found.`); } throw new McpError(ErrorCode.InternalError, `Failed to get user ID for ${accountOrPostUrl}: ${e.message}`); } } else { throw new McpError(ErrorCode.InvalidParams, 'Invalid input. Provide a valid Instagram username or post URL.'); } try { let potentialLeads: any[] = []; // Use Map with string key (user PK converted to string) let usersToAnalyze: Map<string, any> = new Map(); if (targetType === 'post') { const commentsFeed = this.ig.feed.mediaComments(targetId); const comments = await commentsFeed.items(); console.error(`[Tool] Fetched ${comments.length} comments for lead analysis.`); comments.forEach(comment => { const userPkStr = String(comment.user.pk); if (!usersToAnalyze.has(userPkStr)) { usersToAnalyze.set(userPkStr, { ...comment.user, comments: [comment.text] }); } else { usersToAnalyze.get(userPkStr).comments.push(comment.text); } }); } else { // targetType === 'account' const userIdNum = parseInt(targetId, 10); const followersFeed = this.ig.feed.accountFollowers(userIdNum); const followers = await followersFeed.items(); console.error(`[Tool] Fetched ${followers.length} followers for lead analysis.`); followers.forEach(follower => { const followerPkStr = String(follower.pk); if (!usersToAnalyze.has(followerPkStr)) { usersToAnalyze.set(followerPkStr, { ...follower, comments: [] }); } }); } console.error(`[Tool] Analyzing ${usersToAnalyze.size} unique users from ${sourceDescription}.`); for (const user of usersToAnalyze.values()) { let meetsCriteria = true; let reasons: string[] = []; let userInfo = user; if ( (minFollowers && !userInfo.follower_count) || (keywords && !userInfo.biography)) { try { if (user.pk) { userInfo = await this.ig.user.info(user.pk); await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200)); } else { console.warn(`[Tool] Skipping detailed check for user without pk: ${user.username || 'Unknown'}`); continue; } } catch (infoError: any) { console.warn(`[Tool] Could not fetch full info for user ${user.username || user.pk}: ${infoError.message}`); if (minFollowers && !userInfo.follower_count) { continue; } } } if (minFollowers && (userInfo.follower_count === undefined || userInfo.follower_count < minFollowers)) { meetsCriteria = false; } else if (minFollowers) { reasons.push(`Followers: ${userInfo.follower_count} (>=${minFollowers})`); } const userCommentCount = user.comments?.length || 0; if (minComments && userCommentCount < minComments) { meetsCriteria = false; } else if (minComments) { reasons.push(`Comments: ${userCommentCount} (>=${minComments})`); } if (keywords && keywords.length > 0) { const bio = userInfo.biography || ''; const commentsText = (user.comments || []).join(' ').toLowerCase(); const bioLower = bio.toLowerCase(); const foundKeywords = keywords.filter(kw => bioLower.includes(kw.toLowerCase()) || commentsText.includes(kw.toLowerCase())); if (foundKeywords.length === 0) { meetsCriteria = false; } else { reasons.push(`Keywords found: [${foundKeywords.join(', ')}]`); } } if (meetsCriteria) { potentialLeads.push({ username: userInfo.username, userId: userInfo.pk, fullName: userInfo.full_name, followerCount: userInfo.follower_count, isPrivate: userInfo.is_private, reasons: reasons, sampleComment: user.comments?.[0]?.substring(0, 100) }); } } console.error(`[Tool] Identified ${potentialLeads.length} potential leads.`); return { results: { leads: potentialLeads.slice(0, 50) } }; } catch (error: any) { console.error(`[API Error] Failed to identify leads for ${accountOrPostUrl}:`, error.message || error); if (error.name === 'IgNotFoundError') { throw new McpError(ErrorCode.InvalidParams, `${targetType === 'post' ? 'Post' : 'Account'} not found or access denied.`); } throw new McpError(ErrorCode.InternalError, `Failed to process lead identification: ${error.message}`); } } private async handleGenerateReport(args: GenerateReportArgs) { console.error('[Tool] handleGenerateReport called with args:', args); const { account, startDate, endDate } = args; if (!isValidUsername(account)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid account handle.'); } try { const userId = await this.ig.user.getIdByUsername(account); const userInfo = await this.ig.user.info(userId); console.error(`[Tool] Generating report for account: ${account} (ID: ${userId})`); const postsFeed = this.ig.feed.user(userId); let allPosts: any[] = []; let recentPosts: any[] = []; let postCount = 0; const maxPostsToFetch = 200; do { const items = await postsFeed.items(); allPosts = allPosts.concat(items); postCount += items.length; if (postCount >= maxPostsToFetch) break; console.error(`[Tool] Fetched ${items.length} posts (total: ${postCount}) for report`); await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300)); } while (postsFeed.isMoreAvailable() && postCount < maxPostsToFetch); const start = startDate ? new Date(startDate).getTime() / 1000 : null; const end = endDate ? new Date(endDate).getTime() / 1000 + (24 * 60 * 60 -1) : null; recentPosts = allPosts.filter(post => { const postTimestamp = post.taken_at; const isAfterStart = start ? postTimestamp >= start : true; const isBeforeEnd = end ? postTimestamp <= end : true; return isAfterStart && isBeforeEnd; }); console.error(`[Tool] Found ${recentPosts.length} posts within date range (out of ${allPosts.length} fetched).`); if (recentPosts.length === 0) { return { results: { message: `No posts found for ${account} in the specified period.`, accountInfo: { username: account, followers: userInfo.follower_count }, period: { startDate, endDate }, summary: { totalPosts: 0, totalLikes: 0, totalComments: 0, avgEngagementRate: 0 } }}; } const totalLikes = recentPosts.reduce((sum, post) => sum + (post.like_count || 0), 0); const totalComments = recentPosts.reduce((sum, post) => sum + (post.comment_count || 0), 0); const avgLikesPerPost = totalLikes / recentPosts.length; const avgCommentsPerPost = totalComments / recentPosts.length; const followerCount = userInfo.follower_count; const avgEngagementRate = followerCount > 0 ? ((avgLikesPerPost + avgCommentsPerPost) / followerCount) * 100 : 0; const topPostsByLikes = [...recentPosts] .sort((a, b) => (b.like_count || 0) - (a.like_count || 0)) .slice(0, 3) .map(p => ({ url: `https://www.instagram.com/p/${p.code}/`, likes: p.like_count, comments: p.comment_count, caption: p.caption?.text.substring(0, 50) + '...' || '[No Caption]', timestamp: new Date(p.taken_at * 1000).toISOString() })); const report = { accountInfo: { username: account, fullName: userInfo.full_name, followers: followerCount, following: userInfo.following_count, totalPosts: userInfo.media_count, bio: userInfo.biography, isPrivate: userInfo.is_private, }, period: { startDate: startDate || 'N/A', endDate: endDate || 'N/A', postsAnalyzed: recentPosts.length, }, summary: { totalLikes: totalLikes, totalComments: totalComments, avgLikesPerPost: parseFloat(avgLikesPerPost.toFixed(2)), avgCommentsPerPost: parseFloat(avgCommentsPerPost.toFixed(2)), avgEngagementRate: parseFloat(avgEngagementRate.toFixed(2)), }, topPostsByLikes: topPostsByLikes, }; return { results: report }; } catch (error: any) { console.error(`[API Error] Failed to generate report for ${account}:`, error.message || error); if (error.name === 'IgNotFoundError') { throw new McpError(ErrorCode.InvalidParams, `Account ${account} not found.`); } throw new McpError(ErrorCode.InternalError, `Failed to generate engagement report: ${error.message}`); } } private async getMediaIdFromUrl(url: string): Promise<string | null> { try { // Extract shortcode first const shortcode = extractPostIdFromUrl(url); if (!shortcode) return null; // Getting the numeric media PK (required by many feed functions) from URL/shortcode is unreliable. // Option 1: Use a library method if exists (e.g., getIdFromUrl - hypothetical) // Option 2: Use media.info(pk) - but we don't have pk! // Option 3: Use media.getByUrl(url) - might exist in some versions // Option 4: Return the shortcode and hope feed functions accept it (sometimes works) // Option 5: Oembed (public, might give ID) let mediaId: string | null = null; try { // Try using getByUrl if it exists in the installed library version // @ts-ignore // Ignore potential TS error if method doesn't exist on type const mediaInfo = await this.ig.media.getByUrl(url); if (mediaInfo && mediaInfo.pk) { console.log(`[Helper] Found media PK ${mediaInfo.pk} using getByUrl for ${url}`); mediaId = mediaInfo.pk; // pk is the numeric ID } else { console.warn(`[Helper Warn] ig.media.getByUrl did not return expected info for ${url}.`); } } catch(lookupError: any) { console.warn(`[Helper Warn] Failed to get media PK using getByUrl for ${url}: ${lookupError.message}.`); // If getByUrl fails or doesn't exist, fall back to using the shortcode directly. // Note: Some feeds (like mediaComments) require the numeric PK and will fail with the shortcode. mediaId = shortcode; console.log(`[Helper] Falling back to using shortcode ${shortcode} as media ID for ${url}`); } if (!mediaId) { console.error(`[Helper Error] Could not resolve media ID for shortcode: ${shortcode}`); return null; } return mediaId; } catch (error: any) { console.error(`[Helper Error] Failed to get media ID from URL ${url}:`, error.message); if (error.name === 'IgNotFoundError') { return null; } return null; } } async run() { console.error('[Setup] Starting Instagram Engagement MCP server...'); const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('[Setup] Instagram Engagement MCP server running on stdio'); } } const server = new InstagramEngagementServer(); server.run().catch(console.error);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Bob-lance/instagram-engagement-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server