searchTwitter
Search Twitter with natural language queries to find relevant tweets, filtered by latest or top results, with customizable limits for precise output.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| query | Yes | ||
| section | No | latest |
Implementation Reference
- tools/twitter.js:73-101 (registration)Registers the "searchTwitter" MCP tool, providing input schema validation and an inline handler function that orchestrates the search and formatting.server.tool("searchTwitter", { query: z.string().min(1, "Search query is required"), section: z.enum(["latest", "top"]).optional().default("latest"), limit: z.number().int().positive().optional().default(20) }, async ({ query, section, limit }) => { try { // Use the search function with the query directly const tweets = await performTwitterSearch(query, section, limit); // Format the response return { content: [{ type: "text", text: formatTwitterResults(query, tweets, section) }] }; } catch (error) { console.error('Error searching Twitter:', error); return { content: [{ type: "text", text: `Error searching Twitter: ${error.message}` }] }; } } );
- tools/twitter.js:74-78 (schema)Zod schema for tool inputs: required query string, optional section (latest/top), optional positive integer limit (default 20).{ query: z.string().min(1, "Search query is required"), section: z.enum(["latest", "top"]).optional().default("latest"), limit: z.number().int().positive().optional().default(20) },
- tools/twitter.js:79-100 (handler)Inline handler function executed when the tool is called; invokes the performTwitterSearch helper, formats output, and returns MCP-formatted content or error.async ({ query, section, limit }) => { try { // Use the search function with the query directly const tweets = await performTwitterSearch(query, section, limit); // Format the response return { content: [{ type: "text", text: formatTwitterResults(query, tweets, section) }] }; } catch (error) { console.error('Error searching Twitter:', error); return { content: [{ type: "text", text: `Error searching Twitter: ${error.message}` }] }; } }
- tools/twitter.js:14-70 (helper)Core search logic: performs paginated API calls to RapidAPI Twitter search endpoint, handles continuation tokens, aggregates results up to the limit.const performTwitterSearch = async (query, section, limit) => { // Check for API key if (!RAPIDAPI_KEY) { throw new Error("RAPIDAPI_KEY environment variable is not set"); } // Log the query for debugging console.error(`TWITTER SEARCH: ${query}`); // Track all fetched tweets let allTweets = []; let continuationToken = null; // RapidAPI has a max of 20 per request, so we'll need to paginate const maxPerRequest = 20; const requestsNeeded = Math.ceil(limit / maxPerRequest); // Make requests until we hit the limit or run out of results for (let i = 0; i < requestsNeeded; i++) { // Build the query parameters const params = new URLSearchParams({ query: query, section: section, limit: Math.min(maxPerRequest, limit - allTweets.length).toString() }); // Add continuation token if available if (continuationToken) { params.append('continuation_token', continuationToken); } // Make the API request const response = await axios({ method: 'GET', url: `https://twitter154.p.rapidapi.com/search/search?${params.toString()}`, headers: { 'x-rapidapi-key': RAPIDAPI_KEY, 'x-rapidapi-host': RAPIDAPI_HOST } }); // Extract tweets and continuation token const newTweets = response.data.results || []; allTweets = [...allTweets, ...newTweets]; // Store continuation token for next request continuationToken = response.data.continuation_token; // Stop if we don't have a continuation token or reached our limit if (!continuationToken || allTweets.length >= limit) { break; } } // Return only up to the requested limit return allTweets.slice(0, limit); };
- tools/twitter.js:111-213 (helper)Comprehensive formatter that converts raw tweet data into a detailed Markdown report including user details, content, metrics, timestamps, media links, and direct tweet URLs.function formatTwitterResults(query, tweets, section) { if (!tweets || tweets.length === 0) { return `No tweets found for query: ${query}`; } let output = []; tweets.forEach((tweet, index) => { // Create clear separation between tweets output.push(`## [${index + 1}] Tweet by @${tweet.user.username}`); // User information let userInfo = `**User:** ${tweet.user.name} (@${tweet.user.username})`; if (tweet.user.is_blue_verified) { userInfo += ` [Verified]`; } else if (tweet.user.is_verified) { userInfo += ` [Verified]`; } output.push(userInfo); // User stats if available if (tweet.user) { const userStats = []; if (tweet.user.follower_count !== undefined) userStats.push(`${tweet.user.follower_count} followers`); if (tweet.user.following_count !== undefined) userStats.push(`${tweet.user.following_count} following`); if (userStats.length > 0) { output.push(`**Account stats:** ${userStats.join(', ')}`); } } // Tweet content output.push(`\n**Content:** ${tweet.text}\n`); // Engagement metrics let metrics = []; metrics.push(`${tweet.favorite_count} likes`); metrics.push(`${tweet.retweet_count} retweets`); metrics.push(`${tweet.reply_count} replies`); // Add quotes and bookmarks if available if (tweet.quote_count) metrics.push(`${tweet.quote_count} quotes`); if (tweet.bookmark_count) metrics.push(`${tweet.bookmark_count} bookmarks`); // Add view count if available if (tweet.views) metrics.push(`${tweet.views} views`); output.push(`**Engagement:** ${metrics.join(' | ')}`); // Creation date - fix the Invalid Date issue try { // Try parsing the date properly const date = new Date(tweet.created_at || tweet.creation_date); const formattedDate = !isNaN(date.getTime()) ? `${date.toLocaleDateString('en-GB')} ${date.toLocaleTimeString('en-GB')}` : 'Date unavailable'; output.push(`**Posted:** ${formattedDate}`); } catch (e) { output.push(`**Posted:** Date format error`); } // Add reply info if it's a reply if (tweet.in_reply_to_status_id_str) { output.push(`**Reply to:** https://twitter.com/status/${tweet.in_reply_to_status_id_str}`); } // Add media if available if (tweet.media && tweet.media.length > 0) { output.push(`**Media:** ${tweet.media.length} attachment(s)`); tweet.media.forEach(media => { output.push(` - ${media.media_url_https || media.media_url}`); }); } // Add video info if available if ((tweet.extended_entities && tweet.extended_entities.media && tweet.extended_entities.media.some(m => m.type === "video" || m.type === "animated_gif")) || tweet.video_url) { output.push(`**Video:** ${tweet.video_url || "Multiple formats available"}`); } // Add tweet URL - construct a proper URL const tweetId = tweet.id_str || tweet.tweet_id; if (tweetId && tweet.user && tweet.user.username) { output.push(`**URL:** https://twitter.com/${tweet.user.username}/status/${tweetId}`); } else { output.push(`**URL:** Unable to construct URL (missing id or username)`); } // Add separator between tweets output.push('\n---\n'); }); return output.join('\n'); }