# 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 | });
```