Beyond MCP Server
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}`);
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: user.verified_addresses?.eth_addresses?.length > 0 || user.verifications?.length > 0,
metadata: {
verifications: user.verifications || []
}
};
} catch (error) {
console.error('Error fetching Farcaster user profile:', error);
throw new Error(`Failed to fetch profile for user ${userId}`);
}
}
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"];
}
}
}