Skip to main content
Glama

Beyond MCP Server

farcasterProvider.ts48.1 kB
import { ContentProvider, SocialContent, UserProfile, Thread, SearchOptions, ContentOptions, ThreadOptions, TrendingOptions } from '../interfaces/provider'; import config from '../../config'; // Import the Neynar SDK without type checking // @ts-ignore import * as neynar from '@neynar/nodejs-sdk'; // @ts-ignore import { FeedType, FilterType } from '@neynar/nodejs-sdk/build/api'; export class FarcasterProvider implements ContentProvider { private client: any; public name = 'farcaster'; public platform = 'farcaster'; constructor() { console.error('Initializing FarcasterProvider'); console.error('Neynar API Key:', config.providers.farcaster.neynarApiKey ? 'Set (not showing full key)' : 'Not set'); // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('No Neynar API key provided. Farcaster provider will not function correctly.'); console.error('Please set NEYNAR_API_KEY in your .env file or environment variables.'); // Still initialize the client with an empty key, but it won't work for API calls } try { // Initialize the Neynar client without strict typing const neynarConfig = new neynar.Configuration({ apiKey: config.providers.farcaster.neynarApiKey }); console.error('Neynar configuration created successfully'); this.client = new neynar.NeynarAPIClient(neynarConfig); console.error('Neynar client initialized successfully'); } catch (error) { console.error('Error initializing Neynar client:', error); throw error; } } async isAvailable(): Promise<boolean> { console.error('Checking if Farcaster provider is available'); // If no API key is provided, return false immediately if (!config.providers.farcaster.neynarApiKey) { console.error('Farcaster provider is not available: No API key provided'); return false; } // Try up to 3 times to connect to the API for (let attempt = 1; attempt <= 3; attempt++) { try { // Make a simple API call to check if Neynar is available console.error(`Making test API call to Neynar (attempt ${attempt} of 3)`); // Use a simple endpoint that's less likely to fail const response = await this.client.fetchBulkUsers({ fids: [1] }); // If we get here, the API is available console.error('Neynar API call successful: Response received'); return true; } catch (error) { // Check if this is a 502 Bad Gateway or other server error const errorString = String(error); if (errorString.includes('502 Bad Gateway') || errorString.includes('503 Service Unavailable') || errorString.includes('504 Gateway Timeout')) { console.error(`Neynar API server error on attempt ${attempt}: ${errorString}`); // If this isn't our last attempt, wait before retrying if (attempt < 3) { const waitTime = attempt * 1000; // Increase wait time with each attempt console.error(`Waiting ${waitTime}ms before retry...`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } else { // For other errors, log and fail immediately console.error('Farcaster provider unavailable:', error); return false; } } } // If we've tried 3 times and still failed, return false console.error('Farcaster provider unavailable after 3 attempts'); // For development purposes, return true to allow testing even when API is down if (process.env.NODE_ENV === 'development') { console.error('Running in development mode - marking provider as available despite API issues'); return true; } return false; } async searchContent(query: string, options: SearchOptions = {}): Promise<SocialContent[]> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot search Farcaster content: No API key provided'); return [{ id: 'error', text: 'Cannot search Farcaster: No API key provided. Please set NEYNAR_API_KEY in your .env file.', authorId: 'system', authorName: 'System', authorUsername: 'system', createdAt: new Date().toISOString(), platform: this.platform, metadata: { error: 'missing_api_key' } }]; } try { console.error(`Searching Farcaster for: "${query}"`); // Handle special query formats if (query.startsWith('from:')) { const username = query.substring(5).trim(); console.error(`Detected 'from:' query for username: ${username}`); try { // First, find the user's FID const userResponse = await this.client.searchUser({ q: username, limit: 1 }); if (!userResponse || !userResponse.result || !userResponse.result.users || !Array.isArray(userResponse.result.users) || userResponse.result.users.length === 0) { console.error(`User not found for query: ${query}`); return []; } const user = userResponse.result.users[0]; if (!user || !user.fid) { console.error(`User has no FID for query: ${query}`); return []; } const fid = user.fid; console.error(`Found FID ${fid} for username ${username}, fetching casts`); // Fetch casts for this user const response = await this.client.fetchCastsForUser({ fid: fid, limit: options.limit || 20 }); if (!response || !response.casts || !Array.isArray(response.casts)) { console.error('No casts found for user'); return []; } console.error(`Found ${response.casts.length} casts for user ${username}`); // Map the response to our format return this.mapCastsToSocialContent(response.casts); } catch (error) { console.error(`Error processing 'from:' query: ${error}`); return []; } } else { // Regular search query console.error(`Making search API call with parameters: ${JSON.stringify({ q: query, limit: options.limit || 20 })}`); try { // Use the searchCasts method with the correct parameters const response = await this.client.searchCasts({ q: query, limit: options.limit || 20 }); console.error(`Search response received: ${response ? 'Yes' : 'No'}`); if (!response || !response.result || !response.result.casts || !Array.isArray(response.result.casts)) { console.error('No casts found in response'); return []; } console.error(`Found ${response.result.casts.length} casts`); // Map Neynar response to our standardized SocialContent format return this.mapCastsToSocialContent(response.result.casts); } catch (error) { console.error(`Error during search API call: ${error}`); // If the search fails, return an empty array return []; } } } catch (error) { console.error('Error in searchContent:', error); return []; } } // Helper method to map casts to SocialContent format private mapCastsToSocialContent(casts: any[]): SocialContent[] { return casts.map((cast: any) => { try { return { id: cast.hash || '', text: cast.text || '', authorId: cast.author?.fid ? String(cast.author.fid) : 'unknown', authorName: cast.author?.display_name || cast.author?.username || 'Unknown Author', authorUsername: cast.author?.username || 'unknown', createdAt: cast.timestamp ? new Date(cast.timestamp).toISOString() : new Date().toISOString(), platform: this.platform, replyToId: cast.parent_hash || undefined, threadId: cast.thread_hash || cast.hash || '', likes: cast.reactions?.likes_count || 0, reposts: cast.reactions?.recasts_count || 0, replies: cast.replies?.count || 0, url: cast.author?.username ? `https://warpcast.com/${cast.author.username}/${cast.hash ? cast.hash.substring(0, 10) : ''}` : `https://warpcast.com/~/cast/${cast.hash || ''}`, metadata: { embeds: cast.embeds || [], mentions: cast.mentioned_profiles || [] } }; } catch (castError) { console.error(`Error processing cast: ${castError}`); // Return a minimal valid cast object return { id: cast.hash || 'unknown', text: cast.text || '', authorId: 'unknown', authorName: 'Unknown Author', authorUsername: 'unknown', createdAt: new Date().toISOString(), platform: this.platform, url: '', metadata: {} }; } }); } async getUserProfile(userId: string): Promise<UserProfile> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot get Farcaster user profile: No API key provided'); return { id: 'error', displayName: 'Error', username: 'error', bio: 'Cannot get Farcaster user profile: No API key provided. Please set NEYNAR_API_KEY in your .env file.', platform: this.platform, metadata: { error: 'missing_api_key' } }; } try { console.error(`Fetching user profile for: ${userId}`); let response; // Check if userId is a numeric FID or a username if (/^\d+$/.test(userId)) { // If userId is numeric, parse it as FID const fid = parseInt(userId, 10); console.error(`Treating ${userId} as numeric FID: ${fid}`); // Fetch user by FID response = await this.client.fetchBulkUsers({ fids: [fid] }); } else { // If userId is not numeric, try to fetch by username console.error(`Treating ${userId} as username`); // Use searchUser for username lookups response = await this.client.searchUser({ q: userId, limit: 1 }); } console.error(`Response received: ${response ? 'Yes' : 'No'}`); let user; if (response.users && Array.isArray(response.users) && response.users.length > 0) { // For fetchBulkUsers response user = response.users[0]; } else if (response.result && response.result.users && Array.isArray(response.result.users) && response.result.users.length > 0) { // For searchUser response user = response.result.users[0]; } if (!user) { console.error(`No user found for: ${userId}`); throw new Error(`User with ID ${userId} not found`); } console.error(`User found: ${user.username || user.fname}`); // Extract wallet addresses and verifications const verifiedAddresses = user.verified_addresses || {}; const ethAddresses = verifiedAddresses.eth_addresses || []; const solAddresses = verifiedAddresses.sol_addresses || []; const verifications = user.verifications || []; return { id: String(user.fid), displayName: user.display_name || user.displayName || user.username || user.fname, username: user.username || user.fname, bio: user.profile?.bio?.text || undefined, profileImageUrl: user.pfp_url || user.pfp?.url || undefined, followerCount: user.follower_count || user.followerCount || 0, followingCount: user.following_count || user.followingCount || 0, platform: this.platform, verified: ethAddresses.length > 0 || verifications.length > 0, metadata: { verifications: verifications, verifiedEthAddresses: ethAddresses, verifiedSolAddresses: solAddresses, primaryEthAddress: ethAddresses[0] || undefined, primarySolAddress: solAddresses[0] || undefined, activeStatus: user.active_status || undefined, viewerContext: user.viewer_context || undefined, profileUrl: user.profile_url || undefined, custodyAddress: user.custody_address || undefined, recoveryAddress: user.recovery_address || undefined, hasEmail: user.has_email || false, powerBadge: user.power_badge || undefined } }; } catch (error) { console.error('Error fetching Farcaster user profile:', error); throw new Error(`Failed to fetch profile for user ${userId}`); } } async getUserProfileByWalletAddress(walletAddress: string): Promise<UserProfile> { if (!this.client) { console.error('Neynar client not initialized'); throw new Error('Neynar client not initialized'); } try { console.log(`Fetching user profile for wallet address: ${walletAddress}`); // Use the correct endpoint to fetch users by ETH address const response = await this.client.fetchBulkUsersByEthOrSolAddress({ addresses: [walletAddress] }); // The response contains a map of address to array of users const users = response[walletAddress.toLowerCase()]; if (!users || users.length === 0) { console.log(`No user found for wallet address: ${walletAddress}`); throw new Error(`User with wallet address ${walletAddress} not found`); } // Get the most complete profile (one with username and display name) const user = users.find((u: { username: string }) => u.username && !u.username.startsWith('!')) || users[0]; return { id: user.fid.toString(), displayName: user.display_name || '', username: user.username || '', bio: user.profile?.bio?.text || '', profileImageUrl: user.pfp_url || '', followerCount: user.follower_count || 0, followingCount: user.following_count || 0, platform: this.platform, metadata: { verifications: user.verifications || [], verifiedEthAddresses: user.verified_addresses?.eth_addresses || [], verifiedSolAddresses: user.verified_addresses?.sol_addresses || [] } }; } catch (error) { console.error('Error fetching Farcaster user profile by wallet:', error); throw new Error(`Failed to fetch profile for wallet address ${walletAddress}`); } } async getUserContent(userId: string, options: ContentOptions = {}): Promise<SocialContent[]> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot get Farcaster user content: No API key provided'); return [{ id: 'error', text: 'Cannot get Farcaster user content: No API key provided. Please set NEYNAR_API_KEY in your .env file.', authorId: 'system', authorName: 'System', authorUsername: 'system', createdAt: new Date().toISOString(), platform: this.platform, metadata: { error: 'missing_api_key' } }]; } try { console.error(`Fetching content for user: ${userId}`); let fid: number; // Check if userId is numeric (FID) or a username if (/^\d+$/.test(userId)) { fid = parseInt(userId, 10); console.error(`Using FID: ${fid}`); } else { // Treat as username, look up the FID console.error(`Treating ${userId} as username, looking up user`); try { // Use searchUser instead of lookupUserByUsername which is causing issues const userResponse = await this.client.searchUser({ q: userId, limit: 1 }); if (!userResponse || !userResponse.result || !userResponse.result.users || !Array.isArray(userResponse.result.users) || userResponse.result.users.length === 0) { throw new Error(`User not found: ${userId}`); } const user = userResponse.result.users[0]; if (!user || !user.fid) { throw new Error(`User not found: ${userId}`); } fid = user.fid; console.error(`Found FID ${fid} for username ${userId}`); } catch (error) { console.error(`Error looking up user by username: ${error}`); throw new Error(`Failed to find user: ${userId}`); } } // Fetch user's casts using the correct method console.error(`Fetching casts for FID: ${fid}`); const response = await this.client.fetchCastsForUser({ fid: fid, limit: options.limit || 20, cursor: options.cursor }); console.error(`Response received: ${response ? 'Yes' : 'No'}`); if (!response || !response.casts || !Array.isArray(response.casts)) { console.error('No casts found in response'); return []; } console.error(`Found ${response.casts.length} casts`); // Map Neynar response to our standardized SocialContent format with safe property access return response.casts.map((cast: any) => { try { return { id: cast.hash || '', text: cast.text || '', authorId: cast.author?.fid ? String(cast.author.fid) : 'unknown', authorName: cast.author?.display_name || cast.author?.username || 'Unknown Author', authorUsername: cast.author?.username || 'unknown', createdAt: cast.timestamp ? new Date(cast.timestamp).toISOString() : new Date().toISOString(), platform: this.platform, replyToId: cast.parent_hash || undefined, threadId: cast.thread_hash || cast.hash || '', likes: cast.reactions?.likes_count || 0, reposts: cast.reactions?.recasts_count || 0, replies: cast.replies?.count || 0, url: cast.author?.username ? `https://warpcast.com/${cast.author.username}/${cast.hash ? cast.hash.substring(0, 10) : ''}` : `https://warpcast.com/~/cast/${cast.hash || ''}`, metadata: { embeds: cast.embeds || [], mentions: cast.mentioned_profiles || [] } }; } catch (castError) { console.error(`Error processing cast: ${castError}`); // Return a minimal valid cast object return { id: cast.hash || 'unknown', text: cast.text || '', authorId: 'unknown', authorName: 'Unknown Author', authorUsername: 'unknown', createdAt: new Date().toISOString(), platform: this.platform, url: '', metadata: {} }; } }); } catch (error) { console.error('Error fetching Farcaster user content:', error); return []; } } async getThread(threadId: string, options: ThreadOptions = {}): Promise<Thread> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot get Farcaster thread: No API key provided'); const errorContent: SocialContent = { id: 'error', text: 'Cannot get Farcaster thread: No API key provided. Please set NEYNAR_API_KEY in your .env file.', authorId: 'system', authorName: 'System', authorUsername: 'system', createdAt: new Date().toISOString(), platform: this.platform, metadata: { error: 'missing_api_key' } }; return { id: 'error', content: errorContent, replies: [], platform: this.platform, metadata: { error: 'missing_api_key' } }; } try { console.error(`Fetching thread: ${threadId}`); // Try multiple approaches to fetch the cast let cast = null; let castResponse = null; // Approach 1: Try with the provided identifier directly try { const isUrl = threadId.startsWith('http'); let identifier = threadId; // If it's a hash, make sure it's properly formatted with 0x prefix if (!isUrl && !threadId.startsWith('0x')) { identifier = `0x${threadId}`; } console.error(`Trying with identifier: ${identifier}`); castResponse = await this.client.lookupCastByHashOrWarpcastUrl({ identifier: identifier, type: isUrl ? 'url' : 'hash' }); if (castResponse && castResponse.cast) { cast = castResponse.cast; console.error(`Found cast with hash: ${cast.hash}`); } } catch (error: any) { console.error(`First approach failed: ${error.message}`); } // Approach 2: Try with a constructed Warpcast URL if (!cast) { try { // Try with a constructed URL using 'rish' as the username const constructedUrl = `https://warpcast.com/rish/${threadId.replace(/^0x/, '')}`; console.error(`Trying with URL: ${constructedUrl}`); castResponse = await this.client.lookupCastByHashOrWarpcastUrl({ identifier: constructedUrl, type: 'url' }); if (castResponse && castResponse.cast) { cast = castResponse.cast; console.error(`Found cast with hash: ${cast.hash}`); } } catch (error: any) { console.error(`Second approach failed: ${error.message}`); } } // If we couldn't find the cast, return an error thread if (!cast) { console.error(`Failed to find cast with ID: ${threadId}`); return { id: threadId, content: { id: threadId, text: `Thread not found: ${threadId}`, authorId: 'unknown', authorName: 'Unknown Author', authorUsername: 'unknown', createdAt: new Date().toISOString(), platform: this.platform, url: '', metadata: {} }, replies: [], platform: this.platform }; } // Create the main content object with safe property access const mainContent: SocialContent = { id: cast.hash || '', text: cast.text || '', authorId: cast.author?.fid ? String(cast.author.fid) : 'unknown', authorName: cast.author?.display_name || cast.author?.username || 'Unknown Author', authorUsername: cast.author?.username || 'unknown', createdAt: cast.timestamp ? new Date(cast.timestamp).toISOString() : new Date().toISOString(), platform: this.platform, replyToId: cast.parent_hash || undefined, threadId: cast.thread_hash || cast.hash || '', likes: cast.reactions?.likes_count || 0, reposts: cast.reactions?.recasts_count || 0, replies: cast.replies?.count || 0, url: cast.author?.username ? `https://warpcast.com/${cast.author.username}/${cast.hash ? cast.hash.substring(0, 10) : ''}` : `https://warpcast.com/~/cast/${cast.hash || ''}`, metadata: { embeds: cast.embeds || [], mentions: cast.mentioned_profiles || [] } }; // Step 2: Get the conversation (replies) using lookupCastConversation let repliesResponse; try { console.error(`Fetching conversation for hash: ${cast.hash}`); repliesResponse = await this.client.lookupCastConversation({ identifier: cast.hash, type: 'hash', limit: options.limit || 20 }); } catch (error: any) { console.error(`Error fetching replies: ${error.message}`); repliesResponse = null; } // Process replies with safe property access const replies: SocialContent[] = []; // Check if we have direct_replies in the conversation.cast if (repliesResponse?.conversation?.cast?.direct_replies && Array.isArray(repliesResponse.conversation.cast.direct_replies)) { const directReplies = repliesResponse.conversation.cast.direct_replies; console.error(`Processing ${directReplies.length} replies`); for (const reply of directReplies) { try { const replyContent: SocialContent = { id: reply.hash || '', text: reply.text || '', authorId: reply.author?.fid ? String(reply.author.fid) : 'unknown', authorName: reply.author?.display_name || reply.author?.username || 'Unknown Author', authorUsername: reply.author?.username || 'unknown', createdAt: reply.timestamp ? new Date(reply.timestamp).toISOString() : new Date().toISOString(), platform: this.platform, replyToId: reply.parent_hash || undefined, threadId: reply.thread_hash || reply.hash || '', likes: reply.reactions?.likes_count || 0, reposts: reply.reactions?.recasts_count || 0, replies: reply.replies?.count || 0, url: reply.author?.username ? `https://warpcast.com/${reply.author.username}/${reply.hash ? reply.hash.substring(0, 10) : ''}` : `https://warpcast.com/~/cast/${reply.hash || ''}`, metadata: { embeds: reply.embeds || [], mentions: reply.mentioned_profiles || [] } }; replies.push(replyContent); // If this reply has direct_replies, process them as well if (reply.direct_replies && Array.isArray(reply.direct_replies)) { for (const nestedReply of reply.direct_replies) { try { const nestedReplyContent: SocialContent = { id: nestedReply.hash || '', text: nestedReply.text || '', authorId: nestedReply.author?.fid ? String(nestedReply.author.fid) : 'unknown', authorName: nestedReply.author?.display_name || nestedReply.author?.username || 'Unknown Author', authorUsername: nestedReply.author?.username || 'unknown', createdAt: nestedReply.timestamp ? new Date(nestedReply.timestamp).toISOString() : new Date().toISOString(), platform: this.platform, replyToId: nestedReply.parent_hash || undefined, threadId: nestedReply.thread_hash || nestedReply.hash || '', likes: nestedReply.reactions?.likes_count || 0, reposts: nestedReply.reactions?.recasts_count || 0, replies: nestedReply.replies?.count || 0, url: nestedReply.author?.username ? `https://warpcast.com/${nestedReply.author.username}/${nestedReply.hash ? nestedReply.hash.substring(0, 10) : ''}` : `https://warpcast.com/~/cast/${nestedReply.hash || ''}`, metadata: { embeds: nestedReply.embeds || [], mentions: nestedReply.mentioned_profiles || [] } }; replies.push(nestedReplyContent); } catch (nestedReplyError) { console.error(`Error processing nested reply: ${nestedReplyError}`); } } } } catch (replyError) { console.error(`Error processing reply: ${replyError}`); } } } else { console.error('No replies found in the conversation response'); } console.error(`Total replies processed: ${replies.length}`); // Create and return the thread object const thread: Thread = { id: threadId, content: mainContent, replies: replies, platform: this.platform }; return thread; } catch (error: any) { console.error(`Error in getThread: ${error.message}`); // Return a minimal valid thread object instead of throwing return { id: threadId, content: { id: threadId, text: `Error fetching farcaster thread '${threadId}': ${error.message}`, authorId: 'unknown', authorName: 'Unknown Author', authorUsername: 'unknown', createdAt: new Date().toISOString(), platform: this.platform, url: '', metadata: {} }, replies: [], platform: this.platform }; } } async getTrendingTopics(options: TrendingOptions = {}): Promise<string[]> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot get Farcaster trending topics: No API key provided'); return ['Error: No API key provided. Please set NEYNAR_API_KEY in your .env file.']; } try { console.error('Fetching trending topics from Farcaster'); // Fetch trending feed using the correct API method const response = await this.client.fetchFeed({ feedType: FeedType.Filter, filterType: FilterType.GlobalTrending, limit: 25 // Get a good sample of trending casts }); console.error(`Response received: ${response ? 'Yes' : 'No'}`); if (!response || !response.casts || !Array.isArray(response.casts)) { console.error('No trending casts found'); return ["farcaster", "web3", "crypto", "ai"]; // Fallback to default topics } console.error(`Found ${response.casts.length} trending casts`); // Extract hashtags from trending casts const hashtags = new Set<string>(); response.casts.forEach((cast: any) => { // Extract hashtags from cast text const tags = cast.text.match(/#[\w-]+/g) || []; tags.forEach((tag: string) => hashtags.add(tag.substring(1))); // Remove the # symbol }); // If no hashtags found, return default ones if (hashtags.size === 0) { console.error('No hashtags found in trending casts, using defaults'); return ["farcaster", "web3", "crypto", "ai"]; } // Convert to array and limit by options const trendingTopics = Array.from(hashtags); const limitedTopics = trendingTopics.slice(0, options.limit || 10); console.error(`Extracted ${limitedTopics.length} trending topics`); return limitedTopics; } catch (error) { console.error('Error fetching trending topics:', error); // Fallback to default topics return ["farcaster", "web3", "crypto", "ai"]; } } async getTrendingFeed(options: TrendingOptions = {}): Promise<SocialContent[]> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot get Farcaster trending feed: No API key provided'); return [{ id: 'error', text: 'Cannot get Farcaster trending feed: No API key provided. Please set NEYNAR_API_KEY in your .env file.', authorId: 'system', authorName: 'System', authorUsername: 'system', createdAt: new Date().toISOString(), platform: this.platform, metadata: { error: 'missing_api_key' } }]; } try { console.error('Fetching trending feed from Farcaster'); // Default to neynar provider if none specified const provider = options.provider || 'neynar'; console.error(`Using provider: ${provider}`); // Prepare the request parameters const params: any = { limit: options.limit || 20, time_window: options.timeWindow || '24h' }; // Add provider-specific parameters if (provider === 'mbd' && options.providerMetadata) { // For MBD provider, we can add custom filters const providerMetadata = { filters: { ...options.providerMetadata, // Ensure we have valid timestamps if provided start_timestamp: options.providerMetadata.startTimestamp, end_timestamp: options.providerMetadata.endTimestamp } }; params.provider_metadata = encodeURIComponent(JSON.stringify(providerMetadata)); } // Make the API call based on the provider let response; if (provider === 'neynar') { response = await this.client.fetchFeed({ feedType: FeedType.Filter, filterType: FilterType.GlobalTrending, limit: params.limit }); } else { // For openrank and mbd providers, use the trending endpoint const url = `https://api.neynar.com/v2/farcaster/feed/trending?${new URLSearchParams(params)}`; const fetchResponse = await fetch(url, { method: 'GET', headers: { 'accept': 'application/json', 'api_key': config.providers.farcaster.neynarApiKey } }); response = await fetchResponse.json(); } console.error(`Response received: ${response ? 'Yes' : 'No'}`); // Handle different response formats based on provider let casts: any[] = []; if (provider === 'neynar') { casts = response.casts || []; } else { // For openrank and mbd providers, the response format is different casts = response.result?.casts || []; } if (!Array.isArray(casts) || casts.length === 0) { console.error('No trending casts found'); return []; } console.error(`Found ${casts.length} trending casts`); // Map the casts to our standardized SocialContent format and apply limit const mappedCasts = this.mapCastsToSocialContent(casts); return options.limit ? mappedCasts.slice(0, options.limit) : mappedCasts; } catch (error) { console.error('Error fetching trending feed:', error); return []; } } async getUserBalance(userId: string | number): Promise<any> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot get Farcaster user balance: No API key provided'); throw new Error('Cannot get Farcaster user balance: No API key provided. Please set NEYNAR_API_KEY in your .env file.'); } try { let fid: number; // Check if userId is a numeric FID or a username if (typeof userId === 'number' || /^\d+$/.test(String(userId))) { // If userId is numeric, parse it as FID fid = typeof userId === 'number' ? userId : parseInt(String(userId), 10); console.error(`Treating ${userId} as numeric FID: ${fid}`); } else { // If userId is not numeric, try to fetch by username console.error(`Treating ${userId} as username, looking up user`); try { // Use searchUser for username lookups const userResponse = await this.client.searchUser({ q: String(userId), limit: 1 }); if (!userResponse || !userResponse.result || !userResponse.result.users || !Array.isArray(userResponse.result.users) || userResponse.result.users.length === 0) { throw new Error(`User not found: ${userId}`); } const user = userResponse.result.users[0]; if (!user || !user.fid) { throw new Error(`User not found: ${userId}`); } fid = user.fid; console.error(`Found FID ${fid} for username ${userId}`); } catch (error) { console.error(`Error looking up user by username: ${error}`); throw new Error(`Failed to find user: ${userId}`); } } console.error(`Fetching user balance for FID: ${fid}`); // Make the API call to fetch user balance const response = await this.client.fetchUserBalance({ fid: fid, networks: ['base'] // Currently, only 'base' is supported }); console.error(`Response received: ${response ? 'Yes' : 'No'}`); if (!response || !response.user_balance) { console.error('No balance data found in response'); throw new Error('No balance data found for user'); } // Return the user_balance object directly return response.user_balance; } catch (error) { console.error('Error fetching user balance:', error); throw error; } } async searchChannels(query: string, options: { limit?: number; cursor?: string; includeChannels?: boolean; } = {}): Promise<{ channels: Array<{ id: string; name: string; description?: string; followerCount: number; parentUrl?: string; imageUrl?: string; leadFid?: number; createdAt: string; updatedAt: string; focusAreas?: string[]; communityStats?: { followers: number; created: string; lastUpdated: string; }; significance?: string; }>; nextCursor?: string; }> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot search Farcaster channels: No API key provided'); throw new Error('Cannot search Farcaster channels: No API key provided. Please set NEYNAR_API_KEY in your .env file.'); } try { console.error(`Searching Farcaster channels for: "${query}"`); // Prepare the request parameters const params: any = { q: query, limit: options.limit || 20 }; // Add cursor if provided if (options.cursor) { params.cursor = options.cursor; } // Add include_channels parameter if specified if (options.includeChannels !== undefined) { params.include_channels = options.includeChannels; } // Make the API call to search channels const response = await this.client.searchChannels(params); console.error(`Response received: ${response ? 'Yes' : 'No'}`); if (!response || !response.channels || !Array.isArray(response.channels)) { console.error('No channels found in response'); return { channels: [], nextCursor: undefined }; } console.error(`Found ${response.channels.length} channels`); // Map the response to our standardized format with enhanced information const channels = response.channels.map((channel: any) => { // Extract focus areas from description const focusAreas = channel.description ? this.extractFocusAreas(channel.description) : []; // Format dates const createdAt = new Date(channel.created_at * 1000).toISOString(); const updatedAt = channel.updated_at || createdAt; // Determine channel significance const significance = this.determineChannelSignificance(channel); return { id: channel.id || '', name: channel.name || '', description: channel.description, followerCount: channel.follower_count || 0, parentUrl: channel.parent_url, imageUrl: channel.image_url, leadFid: channel.lead_fid, createdAt, updatedAt, focusAreas, communityStats: { followers: channel.follower_count || 0, created: createdAt, lastUpdated: updatedAt }, significance }; }); // Log detailed information about each channel channels.forEach((channel: { name: string; description?: string; focusAreas?: string[]; followerCount: number; createdAt: string; updatedAt: string; significance?: string; }) => { console.error(` Channel Details for "${query}": - Name: ${channel.name} - Description: ${channel.description || 'N/A'} - Focus Areas: ${channel.focusAreas?.join(', ') || 'N/A'} - Followers: ${channel.followerCount} - Created: ${channel.createdAt} - Last Updated: ${channel.updatedAt} - Significance: ${channel.significance || 'N/A'} `); }); return { channels, nextCursor: response.next?.cursor }; } catch (error) { console.error('Error searching channels:', error); throw error; } } async searchBulkChannels(queries: string[], options: { limit?: number; cursor?: string; includeChannels?: boolean; } = {}): Promise<{ [query: string]: { channels: Array<{ id: string; name: string; description?: string; followerCount: number; parentUrl?: string; imageUrl?: string; leadFid?: number; createdAt: string; updatedAt: string; focusAreas?: string[]; communityStats?: { followers: number; created: string; lastUpdated: string; }; significance?: string; }>; nextCursor?: string; }; }> { // Check if API key is provided if (!config.providers.farcaster.neynarApiKey) { console.error('Cannot search Farcaster channels: No API key provided'); throw new Error('Cannot search Farcaster channels: No API key provided. Please set NEYNAR_API_KEY in your .env file.'); } try { console.error(`Searching Farcaster channels for ${queries.length} queries`); // Prepare the request parameters const params: any = { limit: options.limit || 20 }; // Add cursor if provided if (options.cursor) { params.cursor = options.cursor; } // Add include_channels parameter if specified if (options.includeChannels !== undefined) { params.include_channels = options.includeChannels; } // Create a map to store results for each query const results: { [query: string]: { channels: Array<{ id: string; name: string; description?: string; followerCount: number; parentUrl?: string; imageUrl?: string; leadFid?: number; createdAt: string; updatedAt: string; focusAreas?: string[]; communityStats?: { followers: number; created: string; lastUpdated: string; }; significance?: string; }>; nextCursor?: string; }; } = {}; // Process each query in parallel await Promise.all( queries.map(async (query) => { try { console.error(`Processing query: "${query}"`); // Add the query to the parameters params.q = query; // Make the API call to search channels const response = await this.client.searchChannels(params); console.error(`Response received for query "${query}": ${response ? 'Yes' : 'No'}`); if (!response || !response.channels || !Array.isArray(response.channels)) { console.error(`No channels found for query: "${query}"`); results[query] = { channels: [], nextCursor: undefined }; return; } console.error(`Found ${response.channels.length} channels for query "${query}"`); // Map the response to our standardized format with enhanced information const channels = response.channels.map((channel: any) => { // Extract focus areas from description const focusAreas = channel.description ? this.extractFocusAreas(channel.description) : []; // Format dates const createdAt = new Date(channel.created_at * 1000).toISOString(); const updatedAt = channel.updated_at || createdAt; // Determine channel significance const significance = this.determineChannelSignificance(channel); return { id: channel.id || '', name: channel.name || '', description: channel.description, followerCount: channel.follower_count || 0, parentUrl: channel.parent_url, imageUrl: channel.image_url, leadFid: channel.lead_fid, createdAt, updatedAt, focusAreas, communityStats: { followers: channel.follower_count || 0, created: createdAt, lastUpdated: updatedAt }, significance }; }); results[query] = { channels, nextCursor: response.next?.cursor }; // Log detailed information about each channel channels.forEach((channel: { name: string; description?: string; focusAreas?: string[]; followerCount: number; createdAt: string; updatedAt: string; significance?: string; }) => { console.error(` Channel Details for "${query}": - Name: ${channel.name} - Description: ${channel.description || 'N/A'} - Focus Areas: ${channel.focusAreas?.join(', ') || 'N/A'} - Followers: ${channel.followerCount} - Created: ${channel.createdAt} - Last Updated: ${channel.updatedAt} - Significance: ${channel.significance || 'N/A'} `); }); } catch (error) { console.error(`Error processing query "${query}":`, error); results[query] = { channels: [], nextCursor: undefined }; } }) ); return results; } catch (error) { console.error('Error in bulk channel search:', error); throw error; } } private extractFocusAreas(description: string): string[] { // Extract key topics from description const topics = description.toLowerCase() .split(/[.,;]|\band\b|\bor\b/) .map(topic => topic.trim()) .filter(topic => topic.length > 0); // Remove common words and keep only relevant topics const commonWords = ['the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'with']; return topics .filter(topic => !commonWords.includes(topic)) .map(topic => topic.charAt(0).toUpperCase() + topic.slice(1)); } private determineChannelSignificance(channel: { name: string; description?: string; follower_count?: number; image_url?: string; }): string { const followerCount = channel.follower_count || 0; const hasDescription = !!channel.description; const hasImage = !!channel.image_url; const isOfficialChannel = channel.name.toLowerCase().includes('official'); if (isOfficialChannel) { return 'Official channel for platform/project updates and announcements'; } else if (followerCount > 10000) { return 'Major community hub with significant following'; } else if (followerCount > 1000) { return 'Growing community with active engagement'; } else if (hasDescription && hasImage) { return 'Well-maintained channel with regular updates'; } else { return 'Emerging channel in development'; } } }

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/Beyond-Network-AI/beyond-mcp-server'

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