Skip to main content
Glama
simple-thread.js11.4 kB
import readlineSync from "readline-sync"; import { McpClient } from "./mcp-client.js"; import { threadTweets } from "./tweets.js"; import fs from "fs"; async function main() { let client = null; try { console.log("Starting MCP client..."); // Create a new client with a different port to avoid conflicts client = new McpClient({ port: 3007, // Use a different port than the default debug: process.env.DEBUG === "true", maxPortAttempts: 5, portIncrement: 1, startServer: true, // Always start a new server }); await client.start(); console.log("MCP client started successfully!"); // Display thread options console.log("\n=== Twitter Thread Creator ===\n"); console.log("Options:"); console.log("[1] Use pre-defined thread"); console.log("[2] Create custom thread"); const threadOption = readlineSync.question("\nSelect option (1 or 2): "); let tweets = []; if (threadOption === "1") { // Use pre-defined thread console.log("\nPre-defined thread:"); threadTweets.forEach((tweet, index) => { console.log(`\n[${index + 1}] ${tweet}`); console.log(`Length: ${tweet.length} characters`); }); if (readlineSync.keyInYNStrict("\nUse this thread?")) { tweets = [...threadTweets]; } else { console.log("Thread cancelled. Exiting..."); await client.stop(); return; } } else if (threadOption === "2") { // Create custom thread console.log("\nCreate your custom thread:"); console.log( "Enter each tweet in the thread. Enter an empty line when done." ); let tweetNumber = 1; let continueAdding = true; while (continueAdding) { const tweet = readlineSync.question( `\nTweet #${tweetNumber} (max 280 chars, empty to finish): ` ); if (!tweet.trim()) { continueAdding = false; continue; } if (tweet.length > 280) { console.log( `Tweet is too long (${tweet.length} characters). Maximum is 280 characters.` ); console.log("Please try again with a shorter tweet."); continue; } tweets.push(tweet); tweetNumber++; } if (tweets.length === 0) { console.log("No tweets entered. Exiting..."); await client.stop(); return; } console.log(`\nCreated thread with ${tweets.length} tweets.`); } else { console.log("Invalid option. Exiting..."); await client.stop(); return; } // Confirm before sending console.log("\nReady to send thread with the following tweets:"); tweets.forEach((tweet, index) => { console.log(`\n[${index + 1}] ${tweet}`); }); if (!readlineSync.keyInYNStrict("\nSend this thread?")) { console.log("Thread cancelled. Exiting..."); await client.stop(); return; } console.log("\nSending thread..."); // Send the first tweet let previousTweetId = null; for (let i = 0; i < tweets.length; i++) { const tweet = tweets[i]; console.log(`\nSending tweet ${i + 1} of ${tweets.length}...`); try { // Create a direct JSON-RPC request const requestId = Math.floor(Math.random() * 10000); const request = { jsonrpc: "2.0", id: requestId.toString(), method: "tools/call", params: { name: "send_tweet", arguments: { text: tweet, }, }, }; // Add replyToTweetId if this is not the first tweet if (previousTweetId) { request.params.arguments.replyToTweetId = previousTweetId; } if (process.env.DEBUG === "true") { console.log( "DEBUG: Request payload:", JSON.stringify(request, null, 2) ); } // Register a response handler const tweetPromise = new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error("Timeout waiting for response from Twitter")); }, 30000); // 30 second timeout client.responseHandlers.set(requestId.toString(), (response) => { clearTimeout(timeoutId); if (process.env.DEBUG === "true") { console.log( "DEBUG: Raw response:", JSON.stringify(response, null, 2) ); } // Check for error in the response content if ( response.result && response.result.content && response.result.content.length > 0 && response.result.content[0].isError ) { const errorMessage = response.result.content[0].text || "Unknown error in response"; reject(new Error(errorMessage)); return; } // Check for standard error format if (response.error) { reject(new Error(response.error.message || "Unknown error")); return; } // If we get here, the tweet was sent successfully if ( response.result && response.result.content && response.result.content.length > 0 ) { try { // Try to parse the tweet data from the response const contentText = response.result.content[0].text; if (contentText) { try { const tweetData = JSON.parse(contentText); if (tweetData && tweetData.tweet && tweetData.tweet.id) { resolve(tweetData.tweet); return; } else if (tweetData && tweetData.success) { // Some versions might return a success flag instead of tweet data resolve({ id: "unknown", success: true }); return; } } catch (parseError) { console.log( "Warning: Failed to parse tweet data:", parseError.message ); // Try to extract tweet ID using regex if JSON parsing fails const idMatch = contentText.match(/tweet ID: (\d+)/i) || contentText.match(/id['"]?\s*:\s*['"]?(\d+)/i); if (idMatch && idMatch[1]) { resolve({ id: idMatch[1], text: tweet }); return; } } } } catch (error) { console.log( "Warning: Exception while processing response:", error.message ); } // If we couldn't extract the tweet data but got a successful response, // assume the tweet was sent successfully but we can't continue the thread if ( response.result.status === "success" || (response.result.content && response.result.content[0].text && !response.result.content[0].isError) ) { resolve({ id: "unknown", success: true }); return; } reject(new Error("Failed to extract tweet ID from response")); } else { reject(new Error("Invalid response from MCP server")); } }); // Send the request client.sendRequest(request); }); const result = await tweetPromise; console.log(`✅ Tweet ${i + 1} sent successfully!`); if (result.id && result.id !== "unknown") { console.log("Tweet ID:", result.id); console.log( "Tweet URL:", `https://twitter.com/user/status/${result.id}` ); // Save the tweet ID for the next tweet in the thread previousTweetId = result.id; // Save the last tweet ID for future replies if (i === tweets.length - 1) { fs.writeFileSync("./last-tweet-id.txt", result.id); console.log("Last tweet ID saved for future replies."); } } else { console.log("Note: Tweet ID not returned in the response."); console.log( "The tweet was likely sent successfully, but we couldn't extract the ID." ); console.log( "This means we can't properly thread the remaining tweets." ); if (i < tweets.length - 1) { if ( !readlineSync.keyInYNStrict( "Continue sending tweets as individual tweets (not threaded)?" ) ) { console.log("Thread cancelled. Exiting..."); break; } previousTweetId = null; // Reset so remaining tweets aren't threaded } } // Add a delay between tweets to avoid rate limiting if (i < tweets.length - 1) { const delaySeconds = 2; console.log( `Waiting ${delaySeconds} seconds before sending next tweet...` ); await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000) ); } } catch (error) { console.error( `\n❌ ERROR: Failed to send tweet ${i + 1}: ${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:", tweet.length, "characters"); } // Check for authentication errors if ( error.message.includes("authentication") || error.message.includes("auth") || error.message.includes("login") || error.message.includes("credentials") ) { console.log("\nThere seems to be an authentication issue."); console.log( "Please check your Twitter credentials in the .env file." ); } // Check for rate limiting if ( error.message.includes("rate limit") || error.message.includes("too many requests") ) { console.log("\nYou've hit Twitter's rate limits."); console.log("Please wait a while before trying again."); } if (error.details) { console.error( "Error details:", JSON.stringify(error.details, null, 2) ); } if ( !readlineSync.keyInYNStrict("Continue with the rest of the thread?") ) { console.log("Thread cancelled. Exiting..."); break; } } } console.log("\nThread complete!"); // Clean up console.log("\nCleaning up..."); await client.stop(); console.log("Done!"); } 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