Skip to main content
Glama
send-tweet.js12.5 kB
import readlineSync from "readline-sync"; import { McpClient } from "./mcp-client.js"; import { singleTweets } from "./tweets.js"; import fs from "fs"; // Function to fetch information about a tweet by ID async function fetchTweetInfo(client, tweetId) { // Skip tweet info fetching if we're in debug mode to avoid EPIPE errors if (process.env.DEBUG === "true") { console.log( "DEBUG mode: Skipping tweet info fetch to avoid potential EPIPE errors" ); return null; } return new Promise((resolve, _reject) => { // Set a timeout to avoid hanging if there's no response const timeoutId = setTimeout(() => { console.log( "Timeout while fetching tweet info. Continuing without tweet details." ); resolve(null); }, 3000); try { // Check if the client is still connected before sending the request if ( (client.options.startServer && (!client.mcpProcess || client.mcpProcess.killed || client.mcpProcess.exitCode !== null || !client.mcpProcess.stdin || !client.mcpProcess.stdin.writable)) || (!client.options.startServer && (!client.socket || !client.socket.writable)) ) { console.log( "Warning: MCP connection is not available. Cannot fetch tweet info." ); clearTimeout(timeoutId); resolve(null); return; } // Create the request in JSON-RPC 2.0 format const requestId = Math.floor(Math.random() * 10000); const request = { jsonrpc: "2.0", id: requestId.toString(), method: "tools/call", params: { name: "get_tweet_by_id", arguments: { id: tweetId, }, }, }; // Register response handler client.responseHandlers.set(requestId.toString(), (response) => { clearTimeout(timeoutId); if ( response.result && response.result.content && response.result.content.length > 0 ) { try { // Parse the nested JSON string const tweetData = JSON.parse(response.result.content[0].text); if (tweetData && tweetData.tweet) { resolve(tweetData.tweet); } else { console.log("Invalid tweet data format"); resolve(null); } } catch (parseError) { console.log(`Error parsing tweet data: ${parseError.message}`); resolve(null); } } else if (response.error) { console.log( `Error from server: ${response.error.message || "Unknown error"}` ); resolve(null); } else { console.log("Invalid response from MCP server"); resolve(null); } }); // Send the request with error handling try { client.sendRequest(request); } catch (sendError) { console.log( `Error sending request to fetch tweet info: ${sendError.message}` ); clearTimeout(timeoutId); resolve(null); } } catch (error) { console.log(`Error in fetchTweetInfo: ${error.message}`); clearTimeout(timeoutId); resolve(null); } }); } async function main() { let client = null; try { console.log("Starting MCP client..."); client = new McpClient({ port: process.env.PORT ? parseInt(process.env.PORT) : 3001, debug: process.env.DEBUG === "true", maxPortAttempts: 5, portIncrement: 1, startServer: process.env.START_SERVER !== "false", // Start server by default }); await client.start(); console.log("MCP client started successfully!"); // Display available tweets console.log("\nAvailable tweets:"); singleTweets.forEach((tweet, index) => { // Calculate character count const charCount = tweet.length; const countDisplay = charCount <= 280 ? `${charCount}/280 characters` : `⚠️ ${charCount}/280 characters (exceeds limit)`; console.log(`\n[${index + 1}] ${tweet}\n`); console.log(`Length: ${countDisplay}`); }); console.log(`\n[c] Custom tweet\n`); // Get user selection const selection = readlineSync.question( "\nEnter the number of the tweet to send, 'c' for custom tweet, or 'q' to quit: " ); if (selection.toLowerCase() === "q") { console.log("Exiting..."); await client.stop(); return; } let selectedTweet; if (selection.toLowerCase() === "c") { // Get custom tweet text selectedTweet = readlineSync.question("\nEnter your tweet text: "); if (!selectedTweet.trim()) { console.log("Empty tweet. Exiting..."); await client.stop(); return; } // Add character limit validation if (selectedTweet.length > 280) { console.log("\nThe tweet exceeds Twitter's 280 character limit."); console.log("Current length:", selectedTweet.length, "characters"); console.log("Please edit your tweet to be shorter."); await client.stop(); return; } } else { const tweetIndex = parseInt(selection) - 1; if ( isNaN(tweetIndex) || tweetIndex < 0 || tweetIndex >= singleTweets.length ) { console.log("Invalid selection. Exiting..."); await client.stop(); return; } selectedTweet = singleTweets[tweetIndex]; // Add character limit validation for predefined tweets too if (selectedTweet.length > 280) { console.log( "\nThe selected tweet exceeds Twitter's 280 character limit." ); console.log("Current length:", selectedTweet.length, "characters"); console.log("Please select a different tweet or enter a custom tweet."); await client.stop(); return; } } console.log(`\nSending tweet: "${selectedTweet}"\n`); // Check if there's a saved tweet ID for replying let savedTweetId = null; let shouldUseSavedTweetId = false; // In debug mode, skip reading the saved tweet ID to avoid potential EPIPE errors if (process.env.DEBUG !== "true") { try { if (fs.existsSync("./last-tweet-id.txt")) { try { savedTweetId = fs .readFileSync("./last-tweet-id.txt", "utf8") .trim(); if (savedTweetId) { console.log(`Found saved tweet ID: ${savedTweetId}`); // Only try to fetch tweet info if we have a valid ID if (savedTweetId.match(/^\d+$/)) { try { console.log("Fetching information about the saved tweet..."); const tweetInfo = await fetchTweetInfo(client, savedTweetId); if (tweetInfo) { console.log( `Tweet by: ${tweetInfo.author.name} (@${tweetInfo.author.username})` ); console.log( `Tweet text: "${tweetInfo.text.substring(0, 50)}${ tweetInfo.text.length > 50 ? "..." : "" }"` ); shouldUseSavedTweetId = true; } else { console.log( "Could not fetch information about the saved tweet." ); savedTweetId = null; } } catch (tweetInfoError) { console.log( "Could not fetch additional information about the saved tweet." ); console.log(`Error: ${tweetInfoError.message}`); // We'll still allow using the saved ID even if we can't fetch info about it shouldUseSavedTweetId = true; } } else { console.log("Saved tweet ID is not valid. Ignoring it."); savedTweetId = null; } } else { console.log("Saved tweet ID file is empty."); savedTweetId = null; } } catch (readError) { console.log(`Error reading saved tweet ID: ${readError.message}`); savedTweetId = null; } } } catch (error) { console.log("No saved tweet ID found or error reading the file."); console.log(`Error: ${error.message}`); savedTweetId = null; } } else { console.log( "DEBUG mode: Skipping saved tweet ID handling to avoid potential EPIPE errors" ); } // Ask if this is a reply let isReply = false; if (shouldUseSavedTweetId && savedTweetId) { isReply = readlineSync.keyInYNStrict( `Would you like to reply to the saved tweet (ID: ${savedTweetId})?` ); } if (!isReply) { isReply = readlineSync.keyInYNStrict("Is this a reply to another tweet?"); } let replyToTweetId = null; if (isReply) { if ( savedTweetId && readlineSync.keyInYNStrict("Use the saved tweet ID?") ) { replyToTweetId = savedTweetId; } else { replyToTweetId = readlineSync.question( "Enter the tweet ID to reply to: " ); if (!replyToTweetId.trim()) { console.log("No tweet ID provided. Sending as a regular tweet..."); replyToTweetId = null; } } // When replying, Twitter adds some metadata that counts toward the character limit // Typically this is the "@username" of the person you're replying to // Let's estimate this as 15 characters to be safe const estimatedReplyMetadataLength = 15; if (selectedTweet.length + estimatedReplyMetadataLength > 280) { console.log( "\nWARNING: Your reply may exceed Twitter's character limit when the username is added." ); console.log( "Current tweet length:", selectedTweet.length, "characters" ); console.log( "Estimated total with reply metadata:", selectedTweet.length + estimatedReplyMetadataLength, "characters" ); console.log("Maximum allowed:", 280, "characters"); if (!readlineSync.keyInYNStrict("Do you want to continue anyway?")) { console.log("Aborting tweet. Please edit your tweet to be shorter."); await client.stop(); return; } } } // Send the tweet try { const result = await client.sendTweet(selectedTweet, replyToTweetId); console.log("Tweet sent successfully!"); if (result && result.id) { console.log("Tweet ID:", result.id); console.log( "Tweet URL:", `https://twitter.com/user/status/${result.id}` ); // Ask if user wants to save this tweet ID for future replies if ( readlineSync.keyInYNStrict( "Would you like to save this tweet ID for future replies?" ) ) { try { fs.writeFileSync("./last-tweet-id.txt", result.id); console.log(`Tweet ID saved to ./last-tweet-id.txt`); } catch (fsError) { console.error("Error saving tweet ID to file:", fsError.message); } } } else { console.log("Note: Tweet ID not returned in the response."); console.log("The tweet may not have been sent successfully."); console.log("Please check your Twitter account to confirm."); } } catch (error) { console.error("\nERROR: Failed to send tweet:", error.message); // Check for character limit error if (error.message.includes("cannot exceed 280 characters")) { console.log("\nThe tweet exceeds Twitter's 280 character limit."); console.log("Current length:", selectedTweet.length, "characters"); console.log("Please edit your tweet to be shorter."); } if (error.details) { console.error("Error details:", JSON.stringify(error.details, null, 2)); } } // Clean up await client.stop(); } catch (error) { console.error("Error:", error); if (client) { try { await client.stop(); } catch (stopError) { console.error("Error stopping client:", stopError); } } process.exit(1); } } main();

Latest Blog Posts

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/ryanmac/agent-twitter-client-mcp'

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