#
tokens: 17774/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | });
```