# Directory Structure ``` ├── .gitignore ├── code │ ├── account_management.py │ ├── post_tweet.py │ └── retrieve_tweets.py ├── LICENSE ├── package.json ├── README.md └── src ├── formatter.ts ├── index.ts ├── twitter-api.ts └── types.ts ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Environment variables 5 | .env 6 | 7 | # Build output 8 | dist/ 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea/ 17 | .vscode/ 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | # OS generated files 25 | .DS_Store 26 | Thumbbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # <img src="https://www.x.com/favicon.ico" alt="X Logo" width="32" height="32"> X (Twitter) MCP Server 2 | 3 | This MCP server allows Clients to interact with X (formerly Twitter), enabling comprehensive platform operations including posting tweets, searching content, managing accounts, and organizing lists. 4 | 5 | ## Quick Start 6 | 7 | 1. Create an X Developer account and get your API keys from [X Developer Portal](https://developer.twitter.com/en/portal/dashboard) 8 | 2. Set all required API keys in the environment variables 9 | 3. Clone this repository: `git clone https://github.com/Dishant27/twitter-mcp.git` 10 | 4. Install dependencies: `npm install` 11 | 5. Run the server: 12 | - With environment variables: 13 | ```bash 14 | TWITTER_API_KEY=your_api_key \ 15 | TWITTER_API_SECRET=your_api_secret \ 16 | TWITTER_ACCESS_TOKEN=your_access_token \ 17 | TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret \ 18 | npm start 19 | ``` 20 | - Using a `.env` file: 21 | ```bash 22 | # Create a .env file with your X API keys 23 | echo "TWITTER_API_KEY=your_api_key 24 | TWITTER_API_SECRET=your_api_secret 25 | TWITTER_ACCESS_TOKEN=your_access_token 26 | TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret" > .env 27 | 28 | # Start the server 29 | npm start 30 | ``` 31 | 32 | 6. Use with a MCP client, such as Claude. 33 | 34 | ## Claude Configuration 35 | 36 | To use this server with Claude, you'll need to set up the MCP configuration. Here's an example of how the configuration structure should look: 37 | 38 | ```json 39 | { 40 | "name": "x", 41 | "display_name": "X", 42 | "description": "X MCP allows Claude to interact with X (formerly Twitter)", 43 | "path": "path/to/twitter-mcp/dist/index.js", 44 | "startup": { 45 | "env": { 46 | "TWITTER_API_KEY": "your_api_key", 47 | "TWITTER_API_SECRET": "your_api_secret", 48 | "TWITTER_ACCESS_TOKEN": "your_access_token", 49 | "TWITTER_ACCESS_TOKEN_SECRET": "your_access_token_secret" 50 | } 51 | }, 52 | "transport": "stdio" 53 | } 54 | ``` 55 | 56 | Save this configuration in your Claude MCP config directory, typically located at: 57 | - Windows: `%APPDATA%\AnthropicClaude\mcp-servers` 58 | - macOS: `~/Library/Application Support/AnthropicClaude/mcp-servers` 59 | - Linux: `~/.config/AnthropicClaude/mcp-servers` 60 | 61 | ## Features 62 | 63 | ### Post Operations 64 | - Post content (up to 280 characters) 65 | - Search for posts by query with customizable result count 66 | 67 | ### Account Management 68 | - Get profile information for any user or the authenticated account 69 | - Update profile details (name, bio, location, website URL) 70 | - Follow and unfollow users 71 | - List followers for any user or the authenticated account 72 | - List accounts that a user is following 73 | 74 | ### List Management 75 | - Create new lists (public or private) 76 | - Get information about specific lists 77 | - Retrieve all lists owned by the authenticated user 78 | 79 | ## Available MCP Tools 80 | 81 | | Tool Name | Description | 82 | |-----------|-------------| 83 | | `post_tweet` | Post new content to X | 84 | | `search_tweets` | Search for content on X | 85 | | `get_profile` | Get profile information for a user or the authenticated account | 86 | | `update_profile` | Update the authenticated user's profile | 87 | | `follow_user` | Follow a user | 88 | | `unfollow_user` | Unfollow a user | 89 | | `list_followers` | List followers of a user or the authenticated account | 90 | | `list_following` | List accounts that a user or the authenticated account is following | 91 | | `create_list` | Create a new list | 92 | | `get_list_info` | Get information about a list | 93 | | `get_user_lists` | Get all lists owned by the authenticated user | 94 | 95 | ## Requirements 96 | 97 | - Node.js 18.x or higher 98 | - X Developer account with API keys 99 | - API v1 and v2 access 100 | 101 | ## Environment Variables 102 | 103 | | Variable | Description | 104 | |----------|-------------| 105 | | `TWITTER_API_KEY` | Your API key | 106 | | `TWITTER_API_SECRET` | Your API secret | 107 | | `TWITTER_ACCESS_TOKEN` | Your access token | 108 | | `TWITTER_ACCESS_TOKEN_SECRET` | Your access token secret | 109 | 110 | ## Repository Structure 111 | 112 | ``` 113 | twitter-mcp/ 114 | ├── .github/ 115 | │ └── workflows/ 116 | │ ├── publish.yml 117 | │ └── release.yml 118 | ├── code/ 119 | │ ├── account_management.py # Sample Python code for account management 120 | │ ├── post_tweet.py # Sample Python code for posting content 121 | │ └── retrieve_tweets.py # Sample Python code for retrieving content 122 | ├── src/ 123 | │ ├── index.ts # Main entry point 124 | │ ├── twitter-api.ts # X API client 125 | │ ├── formatter.ts # Response formatter 126 | │ └── types.ts # Type definitions 127 | ├── .env.example 128 | ├── .gitignore 129 | ├── Dockerfile 130 | ├── LICENSE 131 | ├── package.json 132 | ├── README.md 133 | └── tsconfig.json 134 | ``` 135 | 136 | ## License 137 | 138 | MIT ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "twitter-mcp", 3 | "version": "1.0.0", 4 | "description": "Model Context Protocol server for Twitter integration", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "twitter-mcp": "dist/index.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "start": "node dist/index.js", 12 | "dev": "ts-node src/index.ts", 13 | "prepublishOnly": "npm run build" 14 | }, 15 | "files": [ 16 | "dist", 17 | "README.md", 18 | "LICENSE" 19 | ], 20 | "keywords": [ 21 | "mcp", 22 | "twitter", 23 | "api", 24 | "model", 25 | "context", 26 | "protocol" 27 | ], 28 | "author": "Dishant27", 29 | "license": "MIT", 30 | "dependencies": { 31 | "@modelcontextprotocol/sdk": "1.2.0", 32 | "dotenv": "^16.4.5", 33 | "twitter-api-v2": "^1.16.0" 34 | }, 35 | "devDependencies": { 36 | "ts-node": "^10.9.2", 37 | "typescript": "^5.3.3" 38 | } 39 | } ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import z from 'zod'; 2 | 3 | // Base configuration 4 | export const ConfigSchema = z.object({ 5 | apiKey: z.string(), 6 | apiSecretKey: z.string(), 7 | accessToken: z.string(), 8 | accessTokenSecret: z.string(), 9 | }); 10 | 11 | export type Config = z.infer<typeof ConfigSchema>; 12 | 13 | // Twitter API error 14 | export class TwitterError extends Error { 15 | constructor( 16 | message: string, 17 | public readonly code: number = 0, 18 | public readonly data?: any 19 | ) { 20 | super(message); 21 | this.name = 'TwitterError'; 22 | } 23 | 24 | static isRateLimit(error: TwitterError): boolean { 25 | return error.code === 88 || error.message.includes('rate limit'); 26 | } 27 | } 28 | 29 | // Schemas for tool inputs 30 | export const PostTweetSchema = z.object({ 31 | text: z.string().max(280), 32 | }); 33 | 34 | export const SearchTweetsSchema = z.object({ 35 | query: z.string(), 36 | count: z.number().min(10).max(100), 37 | }); 38 | 39 | // Account Management Schemas 40 | export const GetProfileSchema = z.object({ 41 | username: z.string().optional(), 42 | }); 43 | 44 | export const UpdateProfileSchema = z.object({ 45 | name: z.string().max(50).optional(), 46 | description: z.string().max(160).optional(), 47 | location: z.string().max(30).optional(), 48 | url: z.string().url().max(100).optional(), 49 | }).refine( 50 | data => Object.keys(data).length > 0, 51 | { message: "At least one profile field must be provided" } 52 | ); 53 | 54 | export const FollowUserSchema = z.object({ 55 | username: z.string(), 56 | }); 57 | 58 | export const UnfollowUserSchema = z.object({ 59 | username: z.string(), 60 | }); 61 | 62 | export const ListFollowersSchema = z.object({ 63 | username: z.string().optional(), 64 | count: z.number().min(1).max(200).default(20), 65 | }); 66 | 67 | export const ListFollowingSchema = z.object({ 68 | username: z.string().optional(), 69 | count: z.number().min(1).max(200).default(20), 70 | }); 71 | 72 | export const CreateListSchema = z.object({ 73 | name: z.string().max(25), 74 | description: z.string().max(100).optional(), 75 | private: z.boolean().default(false), 76 | }); 77 | 78 | export const ListInfoSchema = z.object({ 79 | listId: z.string(), 80 | }); 81 | 82 | // Types for Twitter responses 83 | export interface TwitterUser { 84 | id: string; 85 | name: string; 86 | username: string; 87 | description?: string; 88 | profileImageUrl?: string; 89 | verified: boolean; 90 | followersCount: number; 91 | followingCount: number; 92 | createdAt: string; 93 | } 94 | 95 | export interface Tweet { 96 | id: string; 97 | text: string; 98 | authorId: string; 99 | createdAt: string; 100 | publicMetrics: { 101 | retweetCount: number; 102 | replyCount: number; 103 | likeCount: number; 104 | quoteCount: number; 105 | }; 106 | } 107 | 108 | export interface TwitterList { 109 | id: string; 110 | name: string; 111 | description: string; 112 | memberCount: number; 113 | followerCount: number; 114 | private: boolean; 115 | ownerId: string; 116 | } ``` -------------------------------------------------------------------------------- /code/retrieve_tweets.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Sample code to retrieve tweets using Twitter API 4 | """ 5 | 6 | import tweepy 7 | import json 8 | 9 | def setup_twitter_client(): 10 | """ 11 | Setup Twitter API client with authentication 12 | """ 13 | # Replace these placeholder values with your actual Twitter API credentials 14 | consumer_key = "YOUR_CONSUMER_KEY" 15 | consumer_secret = "YOUR_CONSUMER_SECRET" 16 | access_token = "YOUR_ACCESS_TOKEN" 17 | access_token_secret = "YOUR_ACCESS_TOKEN_SECRET" 18 | 19 | # Authenticate to Twitter 20 | auth = tweepy.OAuth1UserHandler( 21 | consumer_key, consumer_secret, access_token, access_token_secret 22 | ) 23 | 24 | # Create API object 25 | api = tweepy.API(auth) 26 | 27 | return api 28 | 29 | def get_user_timeline(api, username, count=10): 30 | """ 31 | Retrieve recent tweets from a user's timeline 32 | 33 | Args: 34 | api: Authenticated tweepy API object 35 | username: Twitter username to retrieve tweets from 36 | count: Number of tweets to retrieve (default: 10) 37 | 38 | Returns: 39 | List of tweets 40 | """ 41 | try: 42 | tweets = api.user_timeline(screen_name=username, count=count, tweet_mode="extended") 43 | return tweets 44 | except Exception as e: 45 | print(f"Error retrieving tweets for {username}: {e}") 46 | return [] 47 | 48 | def search_tweets(api, query, count=10): 49 | """ 50 | Search for tweets matching a query 51 | 52 | Args: 53 | api: Authenticated tweepy API object 54 | query: Search query string 55 | count: Number of tweets to retrieve (default: 10) 56 | 57 | Returns: 58 | List of tweets matching the query 59 | """ 60 | try: 61 | tweets = api.search_tweets(q=query, count=count, tweet_mode="extended") 62 | return tweets 63 | except Exception as e: 64 | print(f"Error searching for tweets with query '{query}': {e}") 65 | return [] 66 | 67 | def display_tweets(tweets): 68 | """ 69 | Pretty print tweet information 70 | """ 71 | for tweet in tweets: 72 | print(f"\n{'=' * 50}") 73 | print(f"User: @{tweet.user.screen_name}") 74 | print(f"Tweet ID: {tweet.id}") 75 | print(f"Created at: {tweet.created_at}") 76 | print(f"Content: {tweet.full_text}") 77 | print(f"Retweets: {tweet.retweet_count}, Likes: {tweet.favorite_count}") 78 | 79 | def main(): 80 | # Setup Twitter client 81 | api = setup_twitter_client() 82 | 83 | # Example 1: Get tweets from a user's timeline 84 | username = "twitter" 85 | print(f"\nRetrieving recent tweets from @{username}...") 86 | user_tweets = get_user_timeline(api, username, count=5) 87 | display_tweets(user_tweets) 88 | 89 | # Example 2: Search for tweets based on a query 90 | search_query = "#Python" 91 | print(f"\nSearching for tweets with query '{search_query}'...") 92 | search_results = search_tweets(api, search_query, count=5) 93 | display_tweets(search_results) 94 | 95 | if __name__ == "__main__": 96 | main() 97 | ``` -------------------------------------------------------------------------------- /code/post_tweet.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Sample code to post tweets using Twitter API 4 | """ 5 | 6 | import tweepy 7 | import os 8 | from datetime import datetime 9 | 10 | def setup_twitter_client(): 11 | """ 12 | Setup Twitter API client with authentication 13 | """ 14 | # Replace these placeholder values with your actual Twitter API credentials 15 | consumer_key = "YOUR_CONSUMER_KEY" 16 | consumer_secret = "YOUR_CONSUMER_SECRET" 17 | access_token = "YOUR_ACCESS_TOKEN" 18 | access_token_secret = "YOUR_ACCESS_TOKEN_SECRET" 19 | 20 | # Authenticate to Twitter 21 | auth = tweepy.OAuth1UserHandler( 22 | consumer_key, consumer_secret, access_token, access_token_secret 23 | ) 24 | 25 | # Create API object 26 | api = tweepy.API(auth) 27 | 28 | return api 29 | 30 | def post_tweet(api, text): 31 | """ 32 | Post a simple text tweet 33 | 34 | Args: 35 | api: Authenticated tweepy API object 36 | text: Tweet content (max 280 characters) 37 | 38 | Returns: 39 | Posted tweet object if successful, None otherwise 40 | """ 41 | try: 42 | if len(text) > 280: 43 | print(f"Tweet is too long ({len(text)} characters). Maximum is 280 characters.") 44 | return None 45 | 46 | tweet = api.update_status(text) 47 | print(f"Tweet posted successfully! Tweet ID: {tweet.id}") 48 | return tweet 49 | except Exception as e: 50 | print(f"Error posting tweet: {e}") 51 | return None 52 | 53 | def post_tweet_with_media(api, text, media_path): 54 | """ 55 | Post a tweet with attached media (image, gif, or video) 56 | 57 | Args: 58 | api: Authenticated tweepy API object 59 | text: Tweet content 60 | media_path: Path to media file to upload 61 | 62 | Returns: 63 | Posted tweet object if successful, None otherwise 64 | """ 65 | try: 66 | if not os.path.exists(media_path): 67 | print(f"Media file not found: {media_path}") 68 | return None 69 | 70 | # Upload media to Twitter 71 | media = api.media_upload(media_path) 72 | 73 | # Post tweet with media 74 | tweet = api.update_status(text, media_ids=[media.media_id]) 75 | print(f"Tweet with media posted successfully! Tweet ID: {tweet.id}") 76 | return tweet 77 | except Exception as e: 78 | print(f"Error posting tweet with media: {e}") 79 | return None 80 | 81 | def reply_to_tweet(api, tweet_id, reply_text): 82 | """ 83 | Reply to an existing tweet 84 | 85 | Args: 86 | api: Authenticated tweepy API object 87 | tweet_id: ID of the tweet to reply to 88 | reply_text: Content of the reply 89 | 90 | Returns: 91 | Posted reply tweet object if successful, None otherwise 92 | """ 93 | try: 94 | reply = api.update_status( 95 | status=reply_text, 96 | in_reply_to_status_id=tweet_id, 97 | auto_populate_reply_metadata=True 98 | ) 99 | print(f"Reply posted successfully! Reply ID: {reply.id}") 100 | return reply 101 | except Exception as e: 102 | print(f"Error posting reply: {e}") 103 | return None 104 | 105 | def main(): 106 | # Setup Twitter client 107 | api = setup_twitter_client() 108 | 109 | # Example 1: Post a simple text tweet 110 | tweet_text = f"Testing the Twitter API with Python! #Python #TwitterAPI (posted at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')})" 111 | tweet = post_tweet(api, tweet_text) 112 | 113 | if tweet: 114 | # Example 2: Reply to our own tweet 115 | reply_text = "This is a reply to my previous tweet using Python!" 116 | reply_to_tweet(api, tweet.id, reply_text) 117 | 118 | # Example 3: Post a tweet with media (uncomment to test) 119 | # media_path = "path/to/your/image.jpg" # Replace with an actual path 120 | # post_tweet_with_media(api, "Check out this image! #Python", media_path) 121 | 122 | if __name__ == "__main__": 123 | main() 124 | ``` -------------------------------------------------------------------------------- /src/formatter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tweet, TwitterUser, TwitterList } from './types'; 2 | 3 | export class ResponseFormatter { 4 | /** 5 | * Format search response 6 | */ 7 | static formatSearchResponse( 8 | query: string, 9 | tweets: Tweet[], 10 | users: Record<string, TwitterUser> 11 | ): string { 12 | let response = `Search results for "${query}":\n\n`; 13 | 14 | if (tweets.length === 0) { 15 | return response + 'No tweets found matching your query.'; 16 | } 17 | 18 | tweets.forEach((tweet, index) => { 19 | const user = users[tweet.authorId]; 20 | response += `${index + 1}. @${user?.username || 'Unknown'} `; 21 | if (user?.verified) response += '✓ '; 22 | response += `(${user?.name || 'Unknown'}):\n`; 23 | response += `${tweet.text}\n`; 24 | response += `🔁 ${tweet.publicMetrics.retweetCount} | ❤️ ${tweet.publicMetrics.likeCount} | 💬 ${tweet.publicMetrics.replyCount}\n`; 25 | response += `🔗 https://twitter.com/${user?.username}/status/${tweet.id}\n\n`; 26 | }); 27 | 28 | return response; 29 | } 30 | 31 | /** 32 | * Format user profile 33 | */ 34 | static formatUserProfile(user: TwitterUser): string { 35 | let response = `Profile for @${user.username} `; 36 | if (user.verified) response += '✓'; 37 | response += `\n\n`; 38 | 39 | response += `Name: ${user.name}\n`; 40 | if (user.description) response += `Bio: ${user.description}\n`; 41 | response += `Followers: ${user.followersCount.toLocaleString()} | Following: ${user.followingCount.toLocaleString()}\n`; 42 | response += `Account created: ${new Date(user.createdAt).toLocaleDateString()}\n`; 43 | response += `🔗 https://twitter.com/${user.username}\n`; 44 | 45 | return response; 46 | } 47 | 48 | /** 49 | * Format users list (followers or following) 50 | */ 51 | static formatUsersList(users: TwitterUser[], listType: 'followers' | 'following'): string { 52 | const title = listType === 'followers' ? 'Followers' : 'Following'; 53 | let response = `${title} (${users.length}):\n\n`; 54 | 55 | if (users.length === 0) { 56 | return response + `No ${listType.toLowerCase()} found.`; 57 | } 58 | 59 | users.forEach((user, index) => { 60 | response += `${index + 1}. @${user.username} `; 61 | if (user.verified) response += '✓ '; 62 | response += `(${user.name})\n`; 63 | if (user.description) { 64 | // Truncate description if too long 65 | const desc = user.description.length > 50 66 | ? user.description.substring(0, 47) + '...' 67 | : user.description; 68 | response += ` ${desc}\n`; 69 | } 70 | response += ` Followers: ${user.followersCount.toLocaleString()}\n\n`; 71 | }); 72 | 73 | return response; 74 | } 75 | 76 | /** 77 | * Format Twitter lists 78 | */ 79 | static formatLists(lists: TwitterList[]): string { 80 | let response = `Twitter Lists (${lists.length}):\n\n`; 81 | 82 | if (lists.length === 0) { 83 | return response + 'No lists found.'; 84 | } 85 | 86 | lists.forEach((list, index) => { 87 | response += `${index + 1}. ${list.name} ${list.private ? '🔒' : ''}\n`; 88 | if (list.description) response += ` ${list.description}\n`; 89 | response += ` Members: ${list.memberCount.toLocaleString()} | Followers: ${list.followerCount.toLocaleString()}\n\n`; 90 | }); 91 | 92 | return response; 93 | } 94 | 95 | /** 96 | * Format Twitter list info 97 | */ 98 | static formatListInfo(list: TwitterList): string { 99 | let response = `List: ${list.name} ${list.private ? '🔒' : ''}\n\n`; 100 | 101 | if (list.description) response += `Description: ${list.description}\n`; 102 | response += `Privacy: ${list.private ? 'Private' : 'Public'}\n`; 103 | response += `Members: ${list.memberCount.toLocaleString()}\n`; 104 | response += `Followers: ${list.followerCount.toLocaleString()}\n`; 105 | response += `🔗 https://twitter.com/i/lists/${list.id}\n`; 106 | 107 | return response; 108 | } 109 | 110 | /** 111 | * Format response for MCP 112 | */ 113 | static toMcpResponse(text: string): string { 114 | return text; 115 | } 116 | } ``` -------------------------------------------------------------------------------- /src/twitter-api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Twitter from 'twitter-api-v2'; 2 | import { Config, TwitterError, TwitterUser, Tweet, TwitterList } from './types'; 3 | 4 | export class TwitterClient { 5 | private client: Twitter; 6 | 7 | constructor(config: Config) { 8 | this.client = new Twitter({ 9 | appKey: config.apiKey, 10 | appSecret: config.apiSecretKey, 11 | accessToken: config.accessToken, 12 | accessSecret: config.accessTokenSecret, 13 | }); 14 | } 15 | 16 | /** 17 | * Post a new tweet 18 | */ 19 | async postTweet(text: string): Promise<{ id: string }> { 20 | try { 21 | const { data } = await this.client.v2.tweet(text); 22 | return { id: data.id }; 23 | } catch (error) { 24 | this.handleTwitterError(error); 25 | throw error; // TypeScript needs this 26 | } 27 | } 28 | 29 | /** 30 | * Search for tweets 31 | */ 32 | async searchTweets(query: string, count: number): Promise<{ tweets: Tweet[]; users: Record<string, TwitterUser> }> { 33 | try { 34 | const result = await this.client.v2.search(query, { 35 | max_results: count, 36 | 'tweet.fields': 'created_at,public_metrics,author_id', 37 | 'user.fields': 'profile_image_url,description,created_at,verified,public_metrics', 38 | expansions: 'author_id', 39 | }); 40 | 41 | const tweets: Tweet[] = result.data.data.map(tweet => ({ 42 | id: tweet.id, 43 | text: tweet.text, 44 | authorId: tweet.author_id, 45 | createdAt: tweet.created_at, 46 | publicMetrics: { 47 | retweetCount: tweet.public_metrics?.retweet_count || 0, 48 | replyCount: tweet.public_metrics?.reply_count || 0, 49 | likeCount: tweet.public_metrics?.like_count || 0, 50 | quoteCount: tweet.public_metrics?.quote_count || 0, 51 | }, 52 | })); 53 | 54 | const users: Record<string, TwitterUser> = {}; 55 | result.includes.users?.forEach(user => { 56 | users[user.id] = { 57 | id: user.id, 58 | name: user.name, 59 | username: user.username, 60 | description: user.description, 61 | profileImageUrl: user.profile_image_url, 62 | verified: user.verified || false, 63 | followersCount: user.public_metrics?.followers_count || 0, 64 | followingCount: user.public_metrics?.following_count || 0, 65 | createdAt: user.created_at || '', 66 | }; 67 | }); 68 | 69 | return { tweets, users }; 70 | } catch (error) { 71 | this.handleTwitterError(error); 72 | throw error; // TypeScript needs this 73 | } 74 | } 75 | 76 | /** 77 | * Get user profile information 78 | */ 79 | async getUserProfile(username?: string): Promise<TwitterUser> { 80 | try { 81 | const fields = 'profile_image_url,description,created_at,verified,public_metrics'; 82 | 83 | // Get current user profile if no username is provided 84 | const user = username 85 | ? await this.client.v2.userByUsername(username, { 'user.fields': fields }) 86 | : await this.client.v2.me({ 'user.fields': fields }); 87 | 88 | const userData = user.data; 89 | 90 | return { 91 | id: userData.id, 92 | name: userData.name, 93 | username: userData.username, 94 | description: userData.description, 95 | profileImageUrl: userData.profile_image_url, 96 | verified: userData.verified || false, 97 | followersCount: userData.public_metrics?.followers_count || 0, 98 | followingCount: userData.public_metrics?.following_count || 0, 99 | createdAt: userData.created_at || '', 100 | }; 101 | } catch (error) { 102 | this.handleTwitterError(error); 103 | throw error; 104 | } 105 | } 106 | 107 | /** 108 | * Update user profile 109 | */ 110 | async updateProfile(profileData: { 111 | name?: string; 112 | description?: string; 113 | location?: string; 114 | url?: string; 115 | }): Promise<TwitterUser> { 116 | try { 117 | const result = await this.client.v1.updateAccountProfile(profileData); 118 | 119 | return { 120 | id: result.id_str, 121 | name: result.name, 122 | username: result.screen_name, 123 | description: result.description, 124 | profileImageUrl: result.profile_image_url_https, 125 | verified: result.verified, 126 | followersCount: result.followers_count, 127 | followingCount: result.friends_count, 128 | createdAt: result.created_at, 129 | }; 130 | } catch (error) { 131 | this.handleTwitterError(error); 132 | throw error; 133 | } 134 | } 135 | 136 | /** 137 | * Follow a user 138 | */ 139 | async followUser(username: string): Promise<TwitterUser> { 140 | try { 141 | const result = await this.client.v2.follow( 142 | await this.getUserIdByUsername(username) 143 | ); 144 | 145 | if (!result.data.following) { 146 | throw new TwitterError('Failed to follow user'); 147 | } 148 | 149 | return this.getUserProfile(username); 150 | } catch (error) { 151 | this.handleTwitterError(error); 152 | throw error; 153 | } 154 | } 155 | 156 | /** 157 | * Unfollow a user 158 | */ 159 | async unfollowUser(username: string): Promise<TwitterUser> { 160 | try { 161 | const result = await this.client.v2.unfollow( 162 | await this.getUserIdByUsername(username) 163 | ); 164 | 165 | if (!result.data.following) { 166 | return this.getUserProfile(username); 167 | } else { 168 | throw new TwitterError('Failed to unfollow user'); 169 | } 170 | } catch (error) { 171 | this.handleTwitterError(error); 172 | throw error; 173 | } 174 | } 175 | 176 | /** 177 | * Get followers of a user 178 | */ 179 | async getFollowers(username?: string, count: number = 20): Promise<TwitterUser[]> { 180 | try { 181 | const userId = username 182 | ? await this.getUserIdByUsername(username) 183 | : (await this.client.v2.me()).data.id; 184 | 185 | const result = await this.client.v2.followers(userId, { 186 | max_results: count, 187 | 'user.fields': 'profile_image_url,description,created_at,verified,public_metrics', 188 | }); 189 | 190 | return result.data.map(user => ({ 191 | id: user.id, 192 | name: user.name, 193 | username: user.username, 194 | description: user.description, 195 | profileImageUrl: user.profile_image_url, 196 | verified: user.verified || false, 197 | followersCount: user.public_metrics?.followers_count || 0, 198 | followingCount: user.public_metrics?.following_count || 0, 199 | createdAt: user.created_at || '', 200 | })); 201 | } catch (error) { 202 | this.handleTwitterError(error); 203 | throw error; 204 | } 205 | } 206 | 207 | /** 208 | * Get users that a user is following 209 | */ 210 | async getFollowing(username?: string, count: number = 20): Promise<TwitterUser[]> { 211 | try { 212 | const userId = username 213 | ? await this.getUserIdByUsername(username) 214 | : (await this.client.v2.me()).data.id; 215 | 216 | const result = await this.client.v2.following(userId, { 217 | max_results: count, 218 | 'user.fields': 'profile_image_url,description,created_at,verified,public_metrics', 219 | }); 220 | 221 | return result.data.map(user => ({ 222 | id: user.id, 223 | name: user.name, 224 | username: user.username, 225 | description: user.description, 226 | profileImageUrl: user.profile_image_url, 227 | verified: user.verified || false, 228 | followersCount: user.public_metrics?.followers_count || 0, 229 | followingCount: user.public_metrics?.following_count || 0, 230 | createdAt: user.created_at || '', 231 | })); 232 | } catch (error) { 233 | this.handleTwitterError(error); 234 | throw error; 235 | } 236 | } 237 | 238 | /** 239 | * Create a Twitter list 240 | */ 241 | async createList(name: string, description?: string, isPrivate: boolean = false): Promise<TwitterList> { 242 | try { 243 | const result = await this.client.v2.createList({ 244 | name, 245 | description, 246 | private: isPrivate, 247 | }); 248 | 249 | return { 250 | id: result.data.id, 251 | name: result.data.name, 252 | description: result.data.description || '', 253 | memberCount: 0, 254 | followerCount: 0, 255 | private: result.data.private || false, 256 | ownerId: await this.getCurrentUserId(), 257 | }; 258 | } catch (error) { 259 | this.handleTwitterError(error); 260 | throw error; 261 | } 262 | } 263 | 264 | /** 265 | * Get list information 266 | */ 267 | async getListInfo(listId: string): Promise<TwitterList> { 268 | try { 269 | const result = await this.client.v2.list(listId, { 270 | 'list.fields': 'follower_count,member_count,owner_id,private', 271 | }); 272 | 273 | return { 274 | id: result.data.id, 275 | name: result.data.name, 276 | description: result.data.description || '', 277 | memberCount: result.data.member_count || 0, 278 | followerCount: result.data.follower_count || 0, 279 | private: result.data.private || false, 280 | ownerId: result.data.owner_id || '', 281 | }; 282 | } catch (error) { 283 | this.handleTwitterError(error); 284 | throw error; 285 | } 286 | } 287 | 288 | /** 289 | * Get user lists 290 | */ 291 | async getUserLists(): Promise<TwitterList[]> { 292 | try { 293 | const userId = await this.getCurrentUserId(); 294 | const result = await this.client.v2.listsOwned(userId, { 295 | 'list.fields': 'follower_count,member_count,owner_id,private', 296 | }); 297 | 298 | return result.data.map(list => ({ 299 | id: list.id, 300 | name: list.name, 301 | description: list.description || '', 302 | memberCount: list.member_count || 0, 303 | followerCount: list.follower_count || 0, 304 | private: list.private || false, 305 | ownerId: list.owner_id || userId, 306 | })); 307 | } catch (error) { 308 | this.handleTwitterError(error); 309 | throw error; 310 | } 311 | } 312 | 313 | /** 314 | * Helper: Get user ID by username 315 | */ 316 | private async getUserIdByUsername(username: string): Promise<string> { 317 | try { 318 | const result = await this.client.v2.userByUsername(username); 319 | return result.data.id; 320 | } catch (error) { 321 | this.handleTwitterError(error); 322 | throw error; 323 | } 324 | } 325 | 326 | /** 327 | * Helper: Get current user ID 328 | */ 329 | private async getCurrentUserId(): Promise<string> { 330 | try { 331 | const result = await this.client.v2.me(); 332 | return result.data.id; 333 | } catch (error) { 334 | this.handleTwitterError(error); 335 | throw error; 336 | } 337 | } 338 | 339 | /** 340 | * Error handler 341 | */ 342 | private handleTwitterError(error: any): never { 343 | console.error('Twitter API error:', error); 344 | 345 | // Handle rate limiting 346 | if (error.code === 88 || (error.errors && error.errors[0]?.code === 88)) { 347 | throw new TwitterError('Twitter rate limit exceeded', 88, error); 348 | } 349 | 350 | // Handle auth errors 351 | if ([32, 89, 135, 215, 226].includes(error.code) || 352 | (error.errors && [32, 89, 135, 215, 226].includes(error.errors[0]?.code))) { 353 | throw new TwitterError('Twitter authentication error', error.code || 0, error); 354 | } 355 | 356 | // For all other errors 357 | const message = error.message || 358 | (error.errors && error.errors[0]?.message) || 359 | 'Unknown Twitter API error'; 360 | 361 | const code = error.code || 362 | (error.errors && error.errors[0]?.code) || 363 | 0; 364 | 365 | throw new TwitterError(message, code, error); 366 | } 367 | } ``` -------------------------------------------------------------------------------- /code/account_management.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Sample code for Twitter account management operations 4 | """ 5 | 6 | import tweepy 7 | import os 8 | from PIL import Image 9 | import io 10 | 11 | def setup_twitter_client(): 12 | """ 13 | Setup Twitter API client with authentication 14 | """ 15 | # Replace these placeholder values with your actual Twitter API credentials 16 | consumer_key = "YOUR_CONSUMER_KEY" 17 | consumer_secret = "YOUR_CONSUMER_SECRET" 18 | access_token = "YOUR_ACCESS_TOKEN" 19 | access_token_secret = "YOUR_ACCESS_TOKEN_SECRET" 20 | 21 | # Authenticate to Twitter 22 | auth = tweepy.OAuth1UserHandler( 23 | consumer_key, consumer_secret, access_token, access_token_secret 24 | ) 25 | 26 | # Create API object 27 | api = tweepy.API(auth) 28 | 29 | return api 30 | 31 | def get_account_info(api): 32 | """ 33 | Get information about the authenticated user's account 34 | 35 | Args: 36 | api: Authenticated tweepy API object 37 | 38 | Returns: 39 | User object containing account information 40 | """ 41 | try: 42 | me = api.verify_credentials() 43 | print(f"Account information for @{me.screen_name}:") 44 | print(f" - Display name: {me.name}") 45 | print(f" - Bio: {me.description}") 46 | print(f" - Location: {me.location}") 47 | print(f" - Following: {me.friends_count}, Followers: {me.followers_count}") 48 | print(f" - Tweets: {me.statuses_count}") 49 | print(f" - Account created: {me.created_at}") 50 | return me 51 | except Exception as e: 52 | print(f"Error getting account information: {e}") 53 | return None 54 | 55 | def update_profile(api, **kwargs): 56 | """ 57 | Update the authenticated user's profile information 58 | 59 | Args: 60 | api: Authenticated tweepy API object 61 | kwargs: Optional fields to update (name, description, location, url) 62 | 63 | Returns: 64 | Updated user object if successful, None otherwise 65 | """ 66 | try: 67 | # Only pass parameters that are provided 68 | update_params = {} 69 | 70 | if 'name' in kwargs: 71 | update_params['name'] = kwargs['name'] 72 | 73 | if 'description' in kwargs: 74 | update_params['description'] = kwargs['description'] 75 | 76 | if 'location' in kwargs: 77 | update_params['location'] = kwargs['location'] 78 | 79 | if 'url' in kwargs: 80 | update_params['url'] = kwargs['url'] 81 | 82 | # Update profile 83 | updated_user = api.update_profile(**update_params) 84 | 85 | print(f"Profile updated successfully for @{updated_user.screen_name}") 86 | return updated_user 87 | except Exception as e: 88 | print(f"Error updating profile: {e}") 89 | return None 90 | 91 | def update_profile_image(api, image_path): 92 | """ 93 | Update the authenticated user's profile image 94 | 95 | Args: 96 | api: Authenticated tweepy API object 97 | image_path: Path to the new profile image file 98 | 99 | Returns: 100 | Updated user object if successful, None otherwise 101 | """ 102 | try: 103 | if not os.path.exists(image_path): 104 | print(f"Image file not found: {image_path}") 105 | return None 106 | 107 | # Check image size and format 108 | with Image.open(image_path) as img: 109 | width, height = img.size 110 | print(f"Image dimensions: {width}x{height}") 111 | 112 | # Twitter recommends 400x400 pixels for profile images 113 | if width < 400 or height < 400: 114 | print("Warning: Twitter recommends profile images of at least 400x400 pixels") 115 | 116 | # Update profile image 117 | updated_user = api.update_profile_image(filename=image_path) 118 | 119 | print(f"Profile image updated successfully for @{updated_user.screen_name}") 120 | return updated_user 121 | except Exception as e: 122 | print(f"Error updating profile image: {e}") 123 | return None 124 | 125 | def update_profile_banner(api, banner_path): 126 | """ 127 | Update the authenticated user's profile banner 128 | 129 | Args: 130 | api: Authenticated tweepy API object 131 | banner_path: Path to the new banner image file 132 | 133 | Returns: 134 | True if successful, False otherwise 135 | """ 136 | try: 137 | if not os.path.exists(banner_path): 138 | print(f"Banner file not found: {banner_path}") 139 | return False 140 | 141 | # Check image size and format 142 | with Image.open(banner_path) as img: 143 | width, height = img.size 144 | print(f"Banner dimensions: {width}x{height}") 145 | 146 | # Twitter recommends 1500x500 pixels for banners 147 | if width < 1500 or height < 500: 148 | print("Warning: Twitter recommends banner images of 1500x500 pixels") 149 | 150 | # Update profile banner 151 | api.update_profile_banner(filename=banner_path) 152 | 153 | print("Profile banner updated successfully") 154 | return True 155 | except Exception as e: 156 | print(f"Error updating profile banner: {e}") 157 | return False 158 | 159 | def get_followers(api, count=20): 160 | """ 161 | Get a list of users following the authenticated user 162 | 163 | Args: 164 | api: Authenticated tweepy API object 165 | count: Number of followers to retrieve (default: 20) 166 | 167 | Returns: 168 | List of follower user objects 169 | """ 170 | try: 171 | followers = api.get_followers(count=count) 172 | print(f"Retrieved {len(followers)} followers:") 173 | 174 | for i, follower in enumerate(followers, 1): 175 | print(f" {i}. @{follower.screen_name} - {follower.name}") 176 | 177 | return followers 178 | except Exception as e: 179 | print(f"Error retrieving followers: {e}") 180 | return [] 181 | 182 | def get_following(api, count=20): 183 | """ 184 | Get a list of users that the authenticated user is following 185 | 186 | Args: 187 | api: Authenticated tweepy API object 188 | count: Number of following users to retrieve (default: 20) 189 | 190 | Returns: 191 | List of following user objects 192 | """ 193 | try: 194 | following = api.get_friends(count=count) 195 | print(f"Retrieved {len(following)} accounts you are following:") 196 | 197 | for i, friend in enumerate(following, 1): 198 | print(f" {i}. @{friend.screen_name} - {friend.name}") 199 | 200 | return following 201 | except Exception as e: 202 | print(f"Error retrieving following accounts: {e}") 203 | return [] 204 | 205 | def follow_user(api, username): 206 | """ 207 | Follow a specified user 208 | 209 | Args: 210 | api: Authenticated tweepy API object 211 | username: Screen name of the user to follow 212 | 213 | Returns: 214 | Followed user object if successful, None otherwise 215 | """ 216 | try: 217 | user = api.create_friendship(screen_name=username) 218 | print(f"Successfully followed @{user.screen_name}") 219 | return user 220 | except Exception as e: 221 | print(f"Error following user @{username}: {e}") 222 | return None 223 | 224 | def unfollow_user(api, username): 225 | """ 226 | Unfollow a specified user 227 | 228 | Args: 229 | api: Authenticated tweepy API object 230 | username: Screen name of the user to unfollow 231 | 232 | Returns: 233 | Unfollowed user object if successful, None otherwise 234 | """ 235 | try: 236 | user = api.destroy_friendship(screen_name=username) 237 | print(f"Successfully unfollowed @{user.screen_name}") 238 | return user 239 | except Exception as e: 240 | print(f"Error unfollowing user @{username}: {e}") 241 | return None 242 | 243 | def create_list(api, name, description, private=False): 244 | """ 245 | Create a new Twitter list 246 | 247 | Args: 248 | api: Authenticated tweepy API object 249 | name: Name of the list 250 | description: Description of the list 251 | private: Whether the list should be private (default: False) 252 | 253 | Returns: 254 | Created list object if successful, None otherwise 255 | """ 256 | try: 257 | new_list = api.create_list(name=name, description=description, mode='private' if private else 'public') 258 | print(f"List '{new_list.name}' created successfully") 259 | return new_list 260 | except Exception as e: 261 | print(f"Error creating list: {e}") 262 | return None 263 | 264 | def get_lists(api): 265 | """ 266 | Get all lists owned by the authenticated user 267 | 268 | Args: 269 | api: Authenticated tweepy API object 270 | 271 | Returns: 272 | List of owned lists 273 | """ 274 | try: 275 | owned_lists = api.get_lists() 276 | print(f"Retrieved {len(owned_lists)} lists:") 277 | 278 | for i, lst in enumerate(owned_lists, 1): 279 | print(f" {i}. {lst.name} - {lst.description} ({lst.member_count} members)") 280 | 281 | return owned_lists 282 | except Exception as e: 283 | print(f"Error retrieving lists: {e}") 284 | return [] 285 | 286 | def main(): 287 | # Setup Twitter client 288 | api = setup_twitter_client() 289 | 290 | # Example 1: Get account information 291 | print("\n=== Account Information ===") 292 | account_info = get_account_info(api) 293 | 294 | # Example 2: Update profile information 295 | print("\n=== Update Profile Information ===") 296 | print("Note: Commented out to prevent actual updates") 297 | # update_profile( 298 | # api, 299 | # name="Updated Name", 300 | # description="This is an updated bio using Python Tweepy!", 301 | # location="San Francisco, CA", 302 | # url="https://example.com" 303 | # ) 304 | 305 | # Example 3: Update profile image 306 | print("\n=== Update Profile Image ===") 307 | print("Note: Commented out to prevent actual updates") 308 | # profile_image_path = "path/to/profile/image.jpg" # Replace with actual path 309 | # update_profile_image(api, profile_image_path) 310 | 311 | # Example 4: Update profile banner 312 | print("\n=== Update Profile Banner ===") 313 | print("Note: Commented out to prevent actual updates") 314 | # banner_image_path = "path/to/banner/image.jpg" # Replace with actual path 315 | # update_profile_banner(api, banner_image_path) 316 | 317 | # Example 5: Get followers 318 | print("\n=== Get Followers ===") 319 | followers = get_followers(api, count=5) # Limit to 5 for example 320 | 321 | # Example 6: Get accounts you're following 322 | print("\n=== Get Following ===") 323 | following = get_following(api, count=5) # Limit to 5 for example 324 | 325 | # Example 7: Follow a user 326 | print("\n=== Follow User ===") 327 | print("Note: Commented out to prevent actual follow") 328 | # follow_user(api, "twitter") 329 | 330 | # Example 8: Unfollow a user 331 | print("\n=== Unfollow User ===") 332 | print("Note: Commented out to prevent actual unfollow") 333 | # unfollow_user(api, "twitter") 334 | 335 | # Example 9: Create a Twitter list 336 | print("\n=== Create Twitter List ===") 337 | print("Note: Commented out to prevent actual list creation") 338 | # create_list(api, "Python Developers", "A list of Python developers and organizations", private=False) 339 | 340 | # Example 10: Get all owned lists 341 | print("\n=== Get Owned Lists ===") 342 | owned_lists = get_lists(api) 343 | 344 | if __name__ == "__main__": 345 | main() 346 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | ListToolsRequestSchema, 6 | CallToolRequestSchema, 7 | Tool, 8 | ErrorCode, 9 | McpError, 10 | TextContent 11 | } from '@modelcontextprotocol/sdk/types.js'; 12 | import { TwitterClient } from './twitter-api.js'; 13 | import { ResponseFormatter } from './formatter.js'; 14 | import { 15 | Config, ConfigSchema, 16 | PostTweetSchema, SearchTweetsSchema, 17 | GetProfileSchema, UpdateProfileSchema, 18 | FollowUserSchema, UnfollowUserSchema, 19 | ListFollowersSchema, ListFollowingSchema, 20 | CreateListSchema, ListInfoSchema, 21 | TwitterError 22 | } from './types.js'; 23 | import dotenv from 'dotenv'; 24 | 25 | export class TwitterServer { 26 | private server: Server; 27 | private client: TwitterClient; 28 | 29 | constructor(config: Config) { 30 | // Validate config 31 | const result = ConfigSchema.safeParse(config); 32 | if (!result.success) { 33 | throw new Error(`Invalid configuration: ${result.error.message}`); 34 | } 35 | 36 | this.client = new TwitterClient(config); 37 | this.server = new Server({ 38 | name: 'twitter-mcp', 39 | version: '1.0.0' 40 | }, { 41 | capabilities: { 42 | tools: {} 43 | } 44 | }); 45 | 46 | this.setupHandlers(); 47 | } 48 | 49 | private setupHandlers(): void { 50 | // Error handler 51 | this.server.onerror = (error) => { 52 | console.error('[MCP Error]:', error); 53 | }; 54 | 55 | // Graceful shutdown 56 | process.on('SIGINT', async () => { 57 | console.error('Shutting down server...'); 58 | await this.server.close(); 59 | process.exit(0); 60 | }); 61 | 62 | // Register tool handlers 63 | this.setupToolHandlers(); 64 | } 65 | 66 | private setupToolHandlers(): void { 67 | // List available tools 68 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 69 | tools: [ 70 | // Tweet operations 71 | { 72 | name: 'post_tweet', 73 | description: 'Post a new tweet to Twitter', 74 | inputSchema: { 75 | type: 'object', 76 | properties: { 77 | text: { 78 | type: 'string', 79 | description: 'The content of your tweet', 80 | maxLength: 280 81 | } 82 | }, 83 | required: ['text'] 84 | } 85 | } as Tool, 86 | { 87 | name: 'search_tweets', 88 | description: 'Search for tweets on Twitter', 89 | inputSchema: { 90 | type: 'object', 91 | properties: { 92 | query: { 93 | type: 'string', 94 | description: 'Search query' 95 | }, 96 | count: { 97 | type: 'number', 98 | description: 'Number of tweets to return (10-100)', 99 | minimum: 10, 100 | maximum: 100 101 | } 102 | }, 103 | required: ['query', 'count'] 104 | } 105 | } as Tool, 106 | 107 | // Account management operations 108 | { 109 | name: 'get_profile', 110 | description: 'Get Twitter profile information for a user or the authenticated account', 111 | inputSchema: { 112 | type: 'object', 113 | properties: { 114 | username: { 115 | type: 'string', 116 | description: 'Twitter username (if not provided, returns authenticated user profile)' 117 | } 118 | }, 119 | required: [] 120 | } 121 | } as Tool, 122 | { 123 | name: 'update_profile', 124 | description: 'Update the authenticated user\'s Twitter profile', 125 | inputSchema: { 126 | type: 'object', 127 | properties: { 128 | name: { 129 | type: 'string', 130 | description: 'Display name (max 50 chars)' 131 | }, 132 | description: { 133 | type: 'string', 134 | description: 'Bio (max 160 chars)' 135 | }, 136 | location: { 137 | type: 'string', 138 | description: 'Location (max 30 chars)' 139 | }, 140 | url: { 141 | type: 'string', 142 | description: 'Website URL (max 100 chars)' 143 | } 144 | }, 145 | required: [] 146 | } 147 | } as Tool, 148 | { 149 | name: 'follow_user', 150 | description: 'Follow a Twitter user', 151 | inputSchema: { 152 | type: 'object', 153 | properties: { 154 | username: { 155 | type: 'string', 156 | description: 'Twitter username to follow' 157 | } 158 | }, 159 | required: ['username'] 160 | } 161 | } as Tool, 162 | { 163 | name: 'unfollow_user', 164 | description: 'Unfollow a Twitter user', 165 | inputSchema: { 166 | type: 'object', 167 | properties: { 168 | username: { 169 | type: 'string', 170 | description: 'Twitter username to unfollow' 171 | } 172 | }, 173 | required: ['username'] 174 | } 175 | } as Tool, 176 | { 177 | name: 'list_followers', 178 | description: 'List followers of a Twitter user or the authenticated account', 179 | inputSchema: { 180 | type: 'object', 181 | properties: { 182 | username: { 183 | type: 'string', 184 | description: 'Twitter username (if not provided, returns authenticated user\'s followers)' 185 | }, 186 | count: { 187 | type: 'number', 188 | description: 'Number of followers to return (1-200)', 189 | minimum: 1, 190 | maximum: 200, 191 | default: 20 192 | } 193 | }, 194 | required: [] 195 | } 196 | } as Tool, 197 | { 198 | name: 'list_following', 199 | description: 'List accounts that a Twitter user or the authenticated account is following', 200 | inputSchema: { 201 | type: 'object', 202 | properties: { 203 | username: { 204 | type: 'string', 205 | description: 'Twitter username (if not provided, returns authenticated user\'s following)' 206 | }, 207 | count: { 208 | type: 'number', 209 | description: 'Number of accounts to return (1-200)', 210 | minimum: 1, 211 | maximum: 200, 212 | default: 20 213 | } 214 | }, 215 | required: [] 216 | } 217 | } as Tool, 218 | { 219 | name: 'create_list', 220 | description: 'Create a new Twitter list', 221 | inputSchema: { 222 | type: 'object', 223 | properties: { 224 | name: { 225 | type: 'string', 226 | description: 'List name (max 25 chars)' 227 | }, 228 | description: { 229 | type: 'string', 230 | description: 'List description (max 100 chars)' 231 | }, 232 | private: { 233 | type: 'boolean', 234 | description: 'Whether the list should be private (default: false)' 235 | } 236 | }, 237 | required: ['name'] 238 | } 239 | } as Tool, 240 | { 241 | name: 'get_list_info', 242 | description: 'Get information about a Twitter list', 243 | inputSchema: { 244 | type: 'object', 245 | properties: { 246 | listId: { 247 | type: 'string', 248 | description: 'Twitter list ID' 249 | } 250 | }, 251 | required: ['listId'] 252 | } 253 | } as Tool, 254 | { 255 | name: 'get_user_lists', 256 | description: 'Get all lists owned by the authenticated user', 257 | inputSchema: { 258 | type: 'object', 259 | properties: {}, 260 | required: [] 261 | } 262 | } as Tool 263 | ] 264 | })); 265 | 266 | // Handle tool execution 267 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 268 | const { name, arguments: args } = request.params; 269 | console.error(`Tool called: ${name}`, args); 270 | 271 | try { 272 | switch (name) { 273 | // Tweet operations 274 | case 'post_tweet': 275 | return await this.handlePostTweet(args); 276 | case 'search_tweets': 277 | return await this.handleSearchTweets(args); 278 | 279 | // Account management operations 280 | case 'get_profile': 281 | return await this.handleGetProfile(args); 282 | case 'update_profile': 283 | return await this.handleUpdateProfile(args); 284 | case 'follow_user': 285 | return await this.handleFollowUser(args); 286 | case 'unfollow_user': 287 | return await this.handleUnfollowUser(args); 288 | case 'list_followers': 289 | return await this.handleListFollowers(args); 290 | case 'list_following': 291 | return await this.handleListFollowing(args); 292 | case 'create_list': 293 | return await this.handleCreateList(args); 294 | case 'get_list_info': 295 | return await this.handleGetListInfo(args); 296 | case 'get_user_lists': 297 | return await this.handleGetUserLists(args); 298 | default: 299 | throw new McpError( 300 | ErrorCode.MethodNotFound, 301 | `Unknown tool: ${name}` 302 | ); 303 | } 304 | } catch (error) { 305 | return this.handleError(error); 306 | } 307 | }); 308 | } 309 | 310 | // Tweet operations handlers 311 | private async handlePostTweet(args: unknown) { 312 | const result = PostTweetSchema.safeParse(args); 313 | if (!result.success) { 314 | throw new McpError( 315 | ErrorCode.InvalidParams, 316 | `Invalid parameters: ${result.error.message}` 317 | ); 318 | } 319 | 320 | const tweet = await this.client.postTweet(result.data.text); 321 | return { 322 | content: [{ 323 | type: 'text', 324 | text: `Tweet posted successfully!\nURL: https://twitter.com/status/${tweet.id}` 325 | }] as TextContent[] 326 | }; 327 | } 328 | 329 | private async handleSearchTweets(args: unknown) { 330 | const result = SearchTweetsSchema.safeParse(args); 331 | if (!result.success) { 332 | throw new McpError( 333 | ErrorCode.InvalidParams, 334 | `Invalid parameters: ${result.error.message}` 335 | ); 336 | } 337 | 338 | const { tweets, users } = await this.client.searchTweets( 339 | result.data.query, 340 | result.data.count 341 | ); 342 | 343 | const formattedResponse = ResponseFormatter.formatSearchResponse( 344 | result.data.query, 345 | tweets, 346 | users 347 | ); 348 | 349 | return { 350 | content: [{ 351 | type: 'text', 352 | text: ResponseFormatter.toMcpResponse(formattedResponse) 353 | }] as TextContent[] 354 | }; 355 | } 356 | 357 | // Account management operations handlers 358 | private async handleGetProfile(args: unknown) { 359 | const result = GetProfileSchema.safeParse(args); 360 | if (!result.success) { 361 | throw new McpError( 362 | ErrorCode.InvalidParams, 363 | `Invalid parameters: ${result.error.message}` 364 | ); 365 | } 366 | 367 | const profile = await this.client.getUserProfile(result.data.username); 368 | const formattedResponse = ResponseFormatter.formatUserProfile(profile); 369 | 370 | return { 371 | content: [{ 372 | type: 'text', 373 | text: ResponseFormatter.toMcpResponse(formattedResponse) 374 | }] as TextContent[] 375 | }; 376 | } 377 | 378 | private async handleUpdateProfile(args: unknown) { 379 | const result = UpdateProfileSchema.safeParse(args); 380 | if (!result.success) { 381 | throw new McpError( 382 | ErrorCode.InvalidParams, 383 | `Invalid parameters: ${result.error.message}` 384 | ); 385 | } 386 | 387 | const updatedProfile = await this.client.updateProfile({ 388 | name: result.data.name, 389 | description: result.data.description, 390 | location: result.data.location, 391 | url: result.data.url 392 | }); 393 | 394 | const formattedResponse = `Profile updated successfully!\n\n${ResponseFormatter.formatUserProfile(updatedProfile)}`; 395 | 396 | return { 397 | content: [{ 398 | type: 'text', 399 | text: ResponseFormatter.toMcpResponse(formattedResponse) 400 | }] as TextContent[] 401 | }; 402 | } 403 | 404 | private async handleFollowUser(args: unknown) { 405 | const result = FollowUserSchema.safeParse(args); 406 | if (!result.success) { 407 | throw new McpError( 408 | ErrorCode.InvalidParams, 409 | `Invalid parameters: ${result.error.message}` 410 | ); 411 | } 412 | 413 | const user = await this.client.followUser(result.data.username); 414 | const formattedResponse = `Successfully followed @${user.username}!\n\n${ResponseFormatter.formatUserProfile(user)}`; 415 | 416 | return { 417 | content: [{ 418 | type: 'text', 419 | text: ResponseFormatter.toMcpResponse(formattedResponse) 420 | }] as TextContent[] 421 | }; 422 | } 423 | 424 | private async handleUnfollowUser(args: unknown) { 425 | const result = UnfollowUserSchema.safeParse(args); 426 | if (!result.success) { 427 | throw new McpError( 428 | ErrorCode.InvalidParams, 429 | `Invalid parameters: ${result.error.message}` 430 | ); 431 | } 432 | 433 | const user = await this.client.unfollowUser(result.data.username); 434 | const formattedResponse = `Successfully unfollowed @${user.username}!\n\n${ResponseFormatter.formatUserProfile(user)}`; 435 | 436 | return { 437 | content: [{ 438 | type: 'text', 439 | text: ResponseFormatter.toMcpResponse(formattedResponse) 440 | }] as TextContent[] 441 | }; 442 | } 443 | 444 | private async handleListFollowers(args: unknown) { 445 | const result = ListFollowersSchema.safeParse(args); 446 | if (!result.success) { 447 | throw new McpError( 448 | ErrorCode.InvalidParams, 449 | `Invalid parameters: ${result.error.message}` 450 | ); 451 | } 452 | 453 | const followers = await this.client.getFollowers( 454 | result.data.username, 455 | result.data.count 456 | ); 457 | 458 | const formattedResponse = ResponseFormatter.formatUsersList(followers, 'followers'); 459 | 460 | return { 461 | content: [{ 462 | type: 'text', 463 | text: ResponseFormatter.toMcpResponse(formattedResponse) 464 | }] as TextContent[] 465 | }; 466 | } 467 | 468 | private async handleListFollowing(args: unknown) { 469 | const result = ListFollowingSchema.safeParse(args); 470 | if (!result.success) { 471 | throw new McpError( 472 | ErrorCode.InvalidParams, 473 | `Invalid parameters: ${result.error.message}` 474 | ); 475 | } 476 | 477 | const following = await this.client.getFollowing( 478 | result.data.username, 479 | result.data.count 480 | ); 481 | 482 | const formattedResponse = ResponseFormatter.formatUsersList(following, 'following'); 483 | 484 | return { 485 | content: [{ 486 | type: 'text', 487 | text: ResponseFormatter.toMcpResponse(formattedResponse) 488 | }] as TextContent[] 489 | }; 490 | } 491 | 492 | private async handleCreateList(args: unknown) { 493 | const result = CreateListSchema.safeParse(args); 494 | if (!result.success) { 495 | throw new McpError( 496 | ErrorCode.InvalidParams, 497 | `Invalid parameters: ${result.error.message}` 498 | ); 499 | } 500 | 501 | const list = await this.client.createList( 502 | result.data.name, 503 | result.data.description, 504 | result.data.private 505 | ); 506 | 507 | const formattedResponse = `List "${list.name}" created successfully!\n\n${ResponseFormatter.formatListInfo(list)}`; 508 | 509 | return { 510 | content: [{ 511 | type: 'text', 512 | text: ResponseFormatter.toMcpResponse(formattedResponse) 513 | }] as TextContent[] 514 | }; 515 | } 516 | 517 | private async handleGetListInfo(args: unknown) { 518 | const result = ListInfoSchema.safeParse(args); 519 | if (!result.success) { 520 | throw new McpError( 521 | ErrorCode.InvalidParams, 522 | `Invalid parameters: ${result.error.message}` 523 | ); 524 | } 525 | 526 | const list = await this.client.getListInfo(result.data.listId); 527 | const formattedResponse = ResponseFormatter.formatListInfo(list); 528 | 529 | return { 530 | content: [{ 531 | type: 'text', 532 | text: ResponseFormatter.toMcpResponse(formattedResponse) 533 | }] as TextContent[] 534 | }; 535 | } 536 | 537 | private async handleGetUserLists(args: unknown) { 538 | // No parameters needed for this endpoint 539 | const lists = await this.client.getUserLists(); 540 | const formattedResponse = ResponseFormatter.formatLists(lists); 541 | 542 | return { 543 | content: [{ 544 | type: 'text', 545 | text: ResponseFormatter.toMcpResponse(formattedResponse) 546 | }] as TextContent[] 547 | }; 548 | } 549 | 550 | private handleError(error: unknown) { 551 | if (error instanceof McpError) { 552 | throw error; 553 | } 554 | 555 | if (error instanceof TwitterError) { 556 | if (TwitterError.isRateLimit(error)) { 557 | return { 558 | content: [{ 559 | type: 'text', 560 | text: 'Rate limit exceeded. Please wait a moment before trying again.', 561 | isError: true 562 | }] as TextContent[] 563 | }; 564 | } 565 | 566 | return { 567 | content: [{ 568 | type: 'text', 569 | text: `Twitter API error: ${(error as TwitterError).message}`, 570 | isError: true 571 | }] as TextContent[] 572 | }; 573 | } 574 | 575 | console.error('Unexpected error:', error); 576 | throw new McpError( 577 | ErrorCode.InternalError, 578 | 'An unexpected error occurred' 579 | ); 580 | } 581 | 582 | async start(): Promise<void> { 583 | const transport = new StdioServerTransport(); 584 | await this.server.connect(transport); 585 | console.error('Twitter MCP server running on stdio'); 586 | } 587 | } 588 | 589 | // Start the server 590 | dotenv.config(); 591 | 592 | const config = { 593 | apiKey: process.env.TWITTER_API_KEY!, 594 | apiSecretKey: process.env.TWITTER_API_SECRET!, 595 | accessToken: process.env.TWITTER_ACCESS_TOKEN!, 596 | accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET! 597 | }; 598 | 599 | const server = new TwitterServer(config); 600 | server.start().catch(error => { 601 | console.error('Failed to start server:', error); 602 | process.exit(1); 603 | }); ```