├── .env.example ├── .gitignore ├── Dockerfile ├── Readme.md ├── image.png ├── requirements.txt ├── results.md ├── smithery.yaml └── src └── tweet_service.py /.env.example: -------------------------------------------------------------------------------- 1 | USERNAME = 2 | EMAIL = 3 | PASSWORD = -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore 2 | __pycache__ 3 | /venv 4 | /cookies.json 5 | .env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a Python image 2 | FROM python:3.10-slim 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy requirements first for better cache utilization 8 | COPY requirements.txt ./ 9 | 10 | # Install the project's dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Copy the rest of the project files 14 | COPY . /app 15 | 16 | # Set environment variables for Twitter authentication 17 | # These should be provided at runtime for security purposes 18 | ENV TWITTER_USERNAME "@example" 19 | ENV TWITTER_EMAIL "me@example.com" 20 | ENV TWITTER_PASSWORD "secret" 21 | 22 | # Set the entrypoint command to run the MCP server 23 | ENTRYPOINT ["twitter-mcp"] -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Twitter MCP Server 2 | 3 | Welcome to the Twitter MCP (Multi-Channel Platform) Server! This application helps you manage your Twitter account programmatically with a variety of powerful features. 4 | 5 | ## Features 6 | - **Get Timeline:** Retrieve your Twitter home timeline. 7 | - **Get Any User's Tweets:** Fetch tweets from any public Twitter user. 8 | - **Hashtag Search:** Search for tweets containing any hashtag (e.g., `#AI`). 9 | - **Get Replies & Summaries:** Retrieve replies to tweets and get summarized insights. 10 | - **User Direct Messages:** Send and receive Twitter DMs. 11 | - **Create Post:** Programmatically create new tweets. 12 | - **Delete Post:** Delete your tweets through the API. 13 | - **And much more...** 14 | 15 | 16 | [![Twitter MCP Server](https://github.com/user-attachments/assets/1a05ab8f-75b3-4fec-b91d-402f65d41544)](https://smithery.ai/server/@LuniaKunal/mcp-twitter) 17 | 18 | ![Twitter MCP Server](image.png) 19 | 20 | ## Getting Started 21 | 22 | ### Prerequisites 23 | - Python 3.11+ 24 | - [uvicorn](https://www.uvicorn.org/) (for running the server) 25 | - Twitter API credentials (set in `.env` file) 26 | 27 | ### Installation 28 | 1. Clone this repository. 29 | 2. Install dependencies: 30 | ```bash 31 | pip install -r requirements.txt 32 | ``` 33 | 3. Set up your `.env` file with your Twitter API credentials (see `.env.example`). 34 | 35 | ### Running the Application 36 | 37 | To start the server, run: 38 | ```bash 39 | uv run --with twikit --with mcp Path\\src\\tweet_service.py 40 | ``` 41 | 42 | ``` 43 | { 44 | "mcpServers": { 45 | "twitter-mcp": { 46 | "command": "uv", 47 | "args": [ 48 | "--directory", 49 | "Path\\src\\", 50 | "run", 51 | "--with", 52 | "twikit", 53 | "--with", 54 | "mcp", 55 | "tweet_service.py" 56 | ], 57 | "env": { 58 | "COOKIES_PATH": "Path\\cookies.json", 59 | "ENV_FILE": ".env" 60 | } 61 | } 62 | 63 | } 64 | } 65 | ``` 66 | 67 | --- 68 | 69 | Feel free to contribute or suggest new features! 70 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuniaKunal/mcp-twitter/9685b025a0d8755c2d8e53c624d0a6bd63989a89/image.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp[cli] 2 | twikit 3 | mcp 4 | python-dotenv -------------------------------------------------------------------------------- /results.md: -------------------------------------------------------------------------------- 1 | I'll gather the replies for each of these tweets and then provide a summary. Let me start collecting the replies for each tweet. 2 | 3 | It looks like there are no replies for the first tweet. Let me check the next one. 4 | 5 | Let me try another tweet: 6 | 7 | Let me try a different approach. I notice the tweets I'm checking don't seem to return any replies through this method. Let me try with one of their popular tweets: 8 | 9 | I'm not getting replies through this method. Let me try another approach by searching for mentions of @getgrass_io to see interactions and replies: 10 | 11 | Now I have more context about the interactions with getgrass_io. Let's get more replies by searching for additional tweets directed at them: 12 | 13 | Let's get one more batch of replies: 14 | 15 | Based on all the replies and interactions with getgrass_io, here's a summary of what I've gathered: 16 | 17 | ### Summary of Interactions with @getgrass_io 18 | 19 | 1. **Epoch and Airdrop Related Discussions**: 20 | - Many users are asking about "Epoch 7" and when "Season 2 airdrop" will happen 21 | - Some users express frustration about the length of epochs, with comments like "the never ending Epoch" and references to "7 months" of waiting 22 | - There are frequent questions about airdrop timelines and eligibility 23 | - Some users report issues with points decreasing unexpectedly between epochs 24 | 25 | 2. **Technical Issues**: 26 | - Multiple users report connection problems with the Grass node/app 27 | - Questions about wallet connections, particularly with Phantom wallet 28 | - Users asking if they can change wallets or reconnect after issues 29 | - Support requests for technical problems that haven't been resolved 30 | 31 | 3. **Project Sentiment**: 32 | - Mixed reactions - some highly supportive comments praising Grass as "the best DePin" project 33 | - Some users express strong negative sentiment, accusing the project of "farming us dry" 34 | - References to the Grass Foundation's commitment to decentralization 35 | - Some promotional content sharing invite codes and encouraging others to join 36 | 37 | 4. **Business Developments**: 38 | - References to Grass being listed on cryptocurrency exchanges like Bitkub and AscendEX 39 | - Mentions of marketing campaigns offering rewards for social media promotion 40 | - Discussion about the project's data scraping achievements (57M GB in Q1, daily rates over 1M GB) 41 | 42 | 5. **Community Engagement**: 43 | - Users sharing memes and inside jokes like "touch grass" 44 | - Requests for clarification on project roadmap and future plans 45 | - Some users acting as community advocates, defending the project's consistency 46 | 47 | The interactions show a community that's actively engaged with the project but with mixed sentiments - from strong supporters to frustrated users waiting for rewards. There appears to be significant interest in airdrops and token economics, alongside some technical challenges users are experiencing with the platform. -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | configSchema: 2 | type: object 3 | required: 4 | - twitterUsername 5 | - twitterEmail 6 | - twitterPassword 7 | properties: 8 | twitterUsername: 9 | type: string 10 | description: Your Twitter username. 11 | twitterEmail: 12 | type: string 13 | description: Your Twitter email. 14 | twitterPassword: 15 | type: string 16 | description: Your Twitter password. 17 | 18 | startCommand: 19 | type: stdio 20 | commandFunction: | 21 | (config) => ({ 22 | "command": "python", 23 | "args": ["/app/src/tweet_service.py"], 24 | "env": { 25 | "TWITTER_USERNAME": config.twitterUsername, 26 | "TWITTER_EMAIL": config.twitterEmail, 27 | "TWITTER_PASSWORD": config.twitterPassword 28 | } 29 | }) -------------------------------------------------------------------------------- /src/tweet_service.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | import json 3 | from twikit import Client 4 | from typing import List, Dict, Optional 5 | from pathlib import Path 6 | import twikit 7 | import logging 8 | import asyncio 9 | import os 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | USERNAME = os.getenv('TWITTER_USERNAME') 15 | EMAIL = os.getenv('TWITTER_EMAIL') 16 | PASSWORD = os.getenv('TWITTER_PASSWORD') 17 | COOKIES_PATH = Path.home() / 'E:\\Agents\\mcp-twitter\\cookies.json' 18 | mcp = FastMCP("twitter-mcp") 19 | 20 | async def get_twitter_client() -> twikit.Client: 21 | """Initialize and return an authenticated Twitter client.""" 22 | client = twikit.Client('en-US') 23 | 24 | if COOKIES_PATH.exists(): 25 | client.load_cookies(COOKIES_PATH) 26 | else: 27 | try: 28 | await client.login( 29 | auth_info_1=USERNAME, 30 | auth_info_2=EMAIL, 31 | password=PASSWORD 32 | ) 33 | except Exception as e: 34 | logger.error(f"Failed to login: {e}") 35 | raise 36 | COOKIES_PATH.parent.mkdir(parents=True, exist_ok=True) 37 | client.save_cookies(COOKIES_PATH) 38 | 39 | return client 40 | 41 | @mcp.tool() 42 | async def get_tweets(query: str, sort_by: str = 'Latest', count: int = 20) -> List[dict]: 43 | """Search twitter with a query. Sort by 'Top' or 'Latest'""" 44 | try: 45 | client = await get_twitter_client() 46 | tweets = await client.search_tweet(query, sort_by, count=count) 47 | 48 | tweet_data = [get_tweet_data(tweet) for tweet in tweets] 49 | return tweet_data 50 | except Exception as e: 51 | logger.error(f"Error during tweet retrieval: {e}") 52 | return [] 53 | 54 | @mcp.tool() 55 | async def get_user_tweets(username: str, tweet_type: str = 'Tweets', count: int = 10) -> str: 56 | """Get tweets from a specific user's timeline.""" 57 | try: 58 | client = await get_twitter_client() 59 | username = username.lstrip('@') 60 | user = await client.get_user_by_screen_name(username) 61 | if not user: 62 | return f"Could not find user {username}" 63 | 64 | tweets = await client.get_user_tweets( 65 | user_id=user.id, 66 | tweet_type=tweet_type, 67 | count=count 68 | ) 69 | 70 | tweet_data = [get_tweet_data(tweet) for tweet in tweets] 71 | return tweet_data 72 | except Exception as e: 73 | logger.error(f"Failed to get user tweets: {e}") 74 | return f"Failed to get user tweets: {e}" 75 | 76 | @mcp.tool() 77 | async def get_replies_for_tweet(tweet_id: str, count: int = 30) -> List[Dict]: 78 | """ 79 | Get Tweets replies of a specific tweet using tweet_id. 80 | """ 81 | 82 | try: 83 | # Fetch replies using pagination 84 | replies = await get_replies(tweet_id, count) 85 | 86 | if replies: 87 | reply_texts = [reply.text for reply in replies] 88 | 89 | return [get_tweet_data(reply) for reply in replies] 90 | return [] 91 | except Exception as e: 92 | logger.error(f"Error fetching replies for tweet {tweet_id}: {e}") 93 | return [] 94 | 95 | async def get_replies(tweet_id: str, count: int) -> list: 96 | """ 97 | Fetch up to `count` replies for a given tweet_id using pagination. 98 | """ 99 | all_replies = [] 100 | cursor = "" 101 | 102 | try: 103 | client = await get_twitter_client() 104 | 105 | while len(all_replies) < count: 106 | try: 107 | result = await client._get_more_replies(tweet_id, cursor) 108 | if len(result) == 1: 109 | break 110 | except TooManyRequests as e: 111 | # rate_limit_reset = datetime.fromtimestamp(e.rate_limit_reset) 112 | print(f"[DEBUG] Rate limit exceeded. Resetting at {datetime.now()}") 113 | # wait_time = (rate_limit_reset - datetime.now()).total_seconds() 114 | time.sleep(15) 115 | continue 116 | 117 | if not hasattr(result, '_Result__results') or not result._Result__results: 118 | break 119 | 120 | all_replies.extend(result._Result__results) 121 | 122 | if not hasattr(result, 'next_cursor'): 123 | break 124 | 125 | cursor = result.next_cursor 126 | 127 | return all_replies 128 | 129 | except Exception as e: 130 | logger.error(f"Error in get_replies: {e}") 131 | return [] 132 | 133 | # New write tools 134 | @mcp.tool() 135 | async def post_tweet( 136 | text: str, 137 | media_paths: Optional[List[str]] = None, 138 | reply_to: Optional[str] = None, 139 | tags: Optional[List[str]] = None 140 | ) -> str: 141 | """Post a tweet with optional media, reply, and tags.""" 142 | try: 143 | 144 | client = await get_twitter_client() 145 | 146 | # Handle tags by converting to mentions 147 | if tags: 148 | mentions = ' '.join(f"@{tag.lstrip('@')}" for tag in tags) 149 | text = f"""{text} 150 | {mentions}""" 151 | 152 | # Upload media if provided 153 | media_ids = [] 154 | if media_paths: 155 | for path in media_paths: 156 | media_id = await client.upload_media(path, wait_for_completion=True) 157 | media_ids.append(media_id) 158 | 159 | # Create the tweet 160 | tweet = await client.create_tweet( 161 | text=text, 162 | media_ids=media_ids if media_ids else None, 163 | reply_to=reply_to 164 | ) 165 | return f"Successfully posted tweet: {tweet.id}" 166 | except Exception as e: 167 | logger.error(f"Failed to post tweet: {e}") 168 | return f"Failed to post tweet: {e}" 169 | 170 | @mcp.tool() 171 | async def delete_tweet(tweet_id: str) -> str: 172 | """Delete a tweet by its ID.""" 173 | try: 174 | client = await get_twitter_client() 175 | await client.delete_tweet(tweet_id) 176 | return f"Successfully deleted tweet {tweet_id}" 177 | except Exception as e: 178 | logger.error(f"Failed to delete tweet: {e}") 179 | return f"Failed to delete tweet: {e}" 180 | 181 | 182 | @mcp.tool() 183 | async def get_timeline(count: int = 20) -> str: 184 | """Get tweets from your home timeline (For You).""" 185 | try: 186 | client = await get_twitter_client() 187 | tweets = await client.get_timeline(count=count) 188 | return convert_tweets_to_markdown(tweets) 189 | except Exception as e: 190 | logger.error(f"Failed to get timeline: {e}") 191 | return f"Failed to get timeline: {e}" 192 | 193 | @mcp.tool() 194 | async def get_latest_timeline(count: int = 20) -> str: 195 | """Get tweets from your home timeline (Following).""" 196 | try: 197 | client = await get_twitter_client() 198 | tweets = await client.get_latest_timeline(count=count) 199 | return convert_tweets_to_markdown(tweets) 200 | except Exception as e: 201 | logger.error(f"Failed to get latest timeline: {e}") 202 | return f"Failed to get latest timeline: {e}" 203 | 204 | def get_tweet_data(tweet) -> Dict: 205 | """Enhanced tweet data formatting with additional metrics""" 206 | try: 207 | legacy_data = tweet._legacy if hasattr(tweet, '_legacy') else {} 208 | core_data = tweet._data.get('core', {}) if hasattr(tweet, '_data') else {} 209 | user_data = core_data.get('user_results', {}).get('result', {}).get('legacy', {}) 210 | 211 | text = legacy_data.get('full_text', '') 212 | 213 | 214 | return { 215 | 'id': getattr(tweet, 'id', ''), 216 | 'text': text, 217 | 'username': user_data.get('screen_name', 'unknown'), 218 | 'created_at': legacy_data.get('created_at', ''), 219 | 'likes': legacy_data.get('favorite_count', 0), 220 | 'replies': legacy_data.get('reply_count', 0), 221 | 'retweets': legacy_data.get('retweet_count', 0), 222 | 'quote_count': legacy_data.get('quote_count', 0), 223 | } 224 | except Exception as e: 225 | logger.error(f"Error processing tweet data: {e}") 226 | return {'error': str(e)} 227 | 228 | 229 | 230 | if __name__ == "__main__": 231 | # Initialize and run the server 232 | mcp.run() --------------------------------------------------------------------------------