Email Checker MCP Server

This file is a merged representation of the entire codebase, combining all repository files into a single document. Generated by Repomix on: 2025-01-17T15:46:21.699Z ================================================================ File Summary ================================================================ Purpose: -------- This file contains a packed representation of the entire repository's contents. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes. File Format: ------------ The content is organized as follows: 1. This summary section 2. Repository information 3. Directory structure 4. Multiple file entries, each consisting of: a. A separator line (================) b. The file path (File: path/to/file) c. Another separator line d. The full contents of the file e. A blank line Usage Guidelines: ----------------- - This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version. - When processing this file, use the file path to distinguish between different files in the repository. - Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. Notes: ------ - Some files may have been excluded based on .gitignore rules and Repomix's configuration. - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files. Additional Info: ---------------- ================================================================ Directory Structure ================================================================ src/ client/ twitter.ts handlers/ engagement.handlers.ts index.ts list.handlers.ts search.handlers.ts tweet.handlers.ts user.handlers.ts types/ handlers.ts utils/ response.ts index.ts tools.ts twitterClient.ts types.ts .env.example .gitignore LICENSE package.json README.md tsconfig.json ================================================================ Files ================================================================ ================ File: src/client/twitter.ts ================ import { TwitterApi, ApiResponseError, ApiRequestError, TwitterApiReadOnly, ListV2, UserV2, UserOwnedListsV2Paginator, UserListMembershipsV2Paginator, UserListMembersV2Paginator, ListTimelineV2Result, UserV2TimelineResult } from 'twitter-api-v2'; export interface TwitterCredentials { appKey: string; appSecret: string; accessToken: string; accessSecret: string; } export interface PaginationOptions { maxResults?: number; pageLimit?: number; } export interface PaginatedResponse<T> { data: T[]; meta: { result_count: number; total_retrieved: number; next_token?: string; }; } const RATE_LIMIT_WINDOW = 15 * 60 * 1000; // 15 minutes in milliseconds const MAX_RETRIES = 3; const DEFAULT_PAGE_LIMIT = 10; const MAX_RESULTS_PER_PAGE = 100; export class TwitterClient extends TwitterApi { private rateLimitRetryCount: Map<string, number>; private rateLimitResetTime: Map<string, number>; constructor(credentials: TwitterCredentials) { super(credentials); this.rateLimitRetryCount = new Map(); this.rateLimitResetTime = new Map(); } private async handleRateLimit(endpoint: string, error: ApiResponseError | ApiRequestError): Promise<void> { if (error instanceof ApiResponseError && error.rateLimitError && error.rateLimit) { const { remaining, reset } = error.rateLimit; if (remaining === 0) { const retryCount = this.rateLimitRetryCount.get(endpoint) || 0; if (retryCount >= MAX_RETRIES) { throw new Error(`Rate limit exceeded for ${endpoint}. Max retries reached.`); } const resetTime = reset * 1000; // Convert to milliseconds this.rateLimitResetTime.set(endpoint, resetTime); this.rateLimitRetryCount.set(endpoint, retryCount + 1); const waitTime = resetTime - Date.now(); if (waitTime > 0) { console.log(`Rate limit hit for ${endpoint}. Waiting ${Math.ceil(waitTime / 1000)} seconds...`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } } } private async withRateLimitRetry<T>(endpoint: string, operation: () => Promise<T>): Promise<T> { try { const result = await operation(); // Reset retry count on successful operation this.rateLimitRetryCount.delete(endpoint); this.rateLimitResetTime.delete(endpoint); return result; } catch (error) { if (error instanceof ApiResponseError || error instanceof ApiRequestError) { await this.handleRateLimit(endpoint, error); // Retry the operation return this.withRateLimitRetry(endpoint, operation); } throw error; } } private async *paginateResults<T, P extends { data: { data: T[] }; meta: { next_token?: string }; next(): Promise<P> }>( endpoint: string, initialFetch: () => Promise<P>, options: PaginationOptions = {} ): AsyncGenerator<T[], void, unknown> { const { maxResults, pageLimit = DEFAULT_PAGE_LIMIT } = options; let currentPage = 0; let totalResults = 0; try { let paginator = await this.withRateLimitRetry(endpoint, initialFetch); while ( paginator.data?.data?.length > 0 && currentPage < pageLimit && (!maxResults || totalResults < maxResults) ) { const results = paginator.data.data; totalResults += results.length; yield results; if (!paginator.meta.next_token) { break; } currentPage++; paginator = await this.withRateLimitRetry(endpoint, () => paginator.next()); } } catch (error) { console.error(`Error during pagination for ${endpoint}:`, error); throw error; } } async getUserByUsername(username: string) { return this.withRateLimitRetry('getUserByUsername', () => this.v2.userByUsername(username) ); } async getOwnedLists(userId: string, options: any): Promise<PaginatedResponse<ListV2>> { const paginationOptions: PaginationOptions = { maxResults: options.max_results, pageLimit: options.pageLimit }; const allLists: ListV2[] = []; const iterator = this.paginateResults<ListV2, UserOwnedListsV2Paginator>( 'getOwnedLists', () => this.v2.listsOwned(userId, { ...options, max_results: Math.min(options.max_results || MAX_RESULTS_PER_PAGE, MAX_RESULTS_PER_PAGE) }), paginationOptions ); for await (const lists of iterator) { allLists.push(...lists); if (paginationOptions.maxResults && allLists.length >= paginationOptions.maxResults) { allLists.length = paginationOptions.maxResults; break; } } return { data: allLists, meta: { result_count: allLists.length, total_retrieved: allLists.length } }; } async getListMemberships(userId: string, options: any): Promise<PaginatedResponse<ListV2>> { const paginationOptions: PaginationOptions = { maxResults: options.max_results, pageLimit: options.pageLimit }; const allMemberships: ListV2[] = []; const iterator = this.paginateResults<ListV2, UserListMembershipsV2Paginator>( 'getListMemberships', () => this.v2.listMemberships(userId, { ...options, max_results: Math.min(options.max_results || MAX_RESULTS_PER_PAGE, MAX_RESULTS_PER_PAGE) }), paginationOptions ); for await (const memberships of iterator) { allMemberships.push(...memberships); if (paginationOptions.maxResults && allMemberships.length >= paginationOptions.maxResults) { allMemberships.length = paginationOptions.maxResults; break; } } return { data: allMemberships, meta: { result_count: allMemberships.length, total_retrieved: allMemberships.length } }; } async getListMembers(listId: string, options: any): Promise<PaginatedResponse<UserV2>> { const paginationOptions: PaginationOptions = { maxResults: options.max_results, pageLimit: options.pageLimit }; const allMembers: UserV2[] = []; const iterator = this.paginateResults<UserV2, UserListMembersV2Paginator>( 'getListMembers', () => this.v2.listMembers(listId, { ...options, max_results: Math.min(options.max_results || MAX_RESULTS_PER_PAGE, MAX_RESULTS_PER_PAGE) }), paginationOptions ); for await (const members of iterator) { allMembers.push(...members); if (paginationOptions.maxResults && allMembers.length >= paginationOptions.maxResults) { allMembers.length = paginationOptions.maxResults; break; } } return { data: allMembers, meta: { result_count: allMembers.length, total_retrieved: allMembers.length } }; } async createList(name: string, description: string = '', isPrivate: boolean = false) { return this.withRateLimitRetry('createList', () => this.v2.createList({ name, description, private: isPrivate }) ); } async addListMember(listId: string, userId: string) { // Add a small delay before the operation to help prevent rate limits await new Promise(resolve => setTimeout(resolve, 1000)); return this.withRateLimitRetry('addListMember', () => this.v2.addListMember(listId, userId) ); } async removeListMember(listId: string, userId: string) { return this.withRateLimitRetry('removeListMember', () => this.v2.removeListMember(listId, userId) ); } async getUserById(userId: string) { return this.withRateLimitRetry('getUserById', () => this.v2.user(userId, { 'user.fields': ['username', 'name', 'verified'] }) ); } async getList(listId: string) { return this.withRateLimitRetry('getList', () => this.v2.list(listId, { 'list.fields': ['created_at', 'follower_count', 'member_count', 'private', 'description'] }) ); } } ================ File: src/handlers/engagement.handlers.ts ================ import { TwitterClient } from '../twitterClient.js'; import { UserV2 } from 'twitter-api-v2'; import { HandlerResponse, TwitterHandler } from '../types/handlers.js'; import { createResponse } from '../utils/response.js'; interface TweetEngagementArgs { tweetId: string; } interface GetRetweetsArgs extends TweetEngagementArgs { maxResults?: number; userFields?: string[]; } interface GetLikedTweetsArgs { userId: string; maxResults?: number; tweetFields?: string[]; } export const handleLikeTweet: TwitterHandler<TweetEngagementArgs> = async ( client: TwitterClient, { tweetId }: TweetEngagementArgs ): Promise<HandlerResponse> => { try { const { data: { id: userId } } = await client.v2.me(); await client.v2.like(userId, tweetId); return createResponse(`Successfully liked tweet: ${tweetId}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to like tweet: ${error.message}`); } throw error; } }; export const handleUnlikeTweet: TwitterHandler<TweetEngagementArgs> = async ( client: TwitterClient, { tweetId }: TweetEngagementArgs ): Promise<HandlerResponse> => { try { const userId = await client.v2.me().then(response => response.data.id); await client.v2.unlike(userId, tweetId); return createResponse(`Successfully unliked tweet: ${tweetId}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to unlike tweet: ${error.message}`); } throw error; } }; export const handleRetweet: TwitterHandler<TweetEngagementArgs> = async ( client: TwitterClient, { tweetId }: TweetEngagementArgs ): Promise<HandlerResponse> => { try { const userId = await client.v2.me().then(response => response.data.id); await client.v2.retweet(userId, tweetId); return createResponse(`Successfully retweeted tweet: ${tweetId}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to retweet: ${error.message}`); } throw error; } }; export const handleUndoRetweet: TwitterHandler<TweetEngagementArgs> = async ( client: TwitterClient, { tweetId }: TweetEngagementArgs ): Promise<HandlerResponse> => { try { const userId = await client.v2.me().then(response => response.data.id); await client.v2.unretweet(userId, tweetId); return createResponse(`Successfully undid retweet: ${tweetId}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to undo retweet: ${error.message}`); } throw error; } }; export const handleGetRetweets: TwitterHandler<GetRetweetsArgs> = async ( client: TwitterClient, { tweetId, maxResults = 100, userFields }: GetRetweetsArgs ): Promise<HandlerResponse> => { try { const retweets = await client.v2.tweetRetweetedBy(tweetId, { max_results: maxResults, 'user.fields': userFields?.join(',') || 'description,profile_image_url,public_metrics,verified' }); if (!retweets.data || retweets.data.length === 0) { return createResponse(`No retweets found for tweet: ${tweetId}`); } return createResponse(`Users who retweeted: ${JSON.stringify(retweets.data, null, 2)}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get retweets: ${error.message}`); } throw error; } }; export const handleGetLikedTweets: TwitterHandler<GetLikedTweetsArgs> = async ( client: TwitterClient, { userId, maxResults = 100, tweetFields }: GetLikedTweetsArgs ): Promise<HandlerResponse> => { try { const likedTweets = await client.v2.userLikedTweets(userId, { max_results: maxResults, 'tweet.fields': tweetFields?.join(',') || 'created_at,public_metrics,author_id' }); const tweets = Array.isArray(likedTweets.data) ? likedTweets.data : []; if (tweets.length === 0) { return createResponse(`No liked tweets found for user: ${userId}`); } return createResponse(`Liked tweets: ${JSON.stringify(tweets, null, 2)}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get liked tweets: ${error.message}`); } throw error; } }; ================ File: src/handlers/index.ts ================ // This will be the main export file for all handlers export * from './tweet.handlers.js'; export * from './user.handlers.js'; export * from './list.handlers.js'; export * from './engagement.handlers.js'; ================ File: src/handlers/list.handlers.ts ================ import { TwitterClient } from '../client/twitter.js'; import { HandlerResponse } from '../types/handlers.js'; import { createResponse } from '../utils/response.js'; import { ListV2, UserV2, ApiResponseError } from 'twitter-api-v2'; export interface GetUserListsArgs { username: string; maxResults?: number; pageLimit?: number; } export interface CreateListArgs { name: string; description?: string; isPrivate?: boolean; } export interface AddUserToListArgs { listId: string; userId: string; } export interface RemoveUserFromListArgs { listId: string; userId: string; } export interface GetListMembersArgs { listId: string; maxResults?: number; pageLimit?: number; userFields?: string[]; } export async function handleGetUserLists( client: TwitterClient, args: GetUserListsArgs ): Promise<HandlerResponse> { try { const user = await client.getUserByUsername(args.username); if (!user.data) { throw new Error('User not found'); } const options = { 'list.fields': ['created_at', 'follower_count', 'member_count', 'private', 'description'], expansions: ['owner_id'], 'user.fields': ['username', 'name', 'verified'], max_results: args.maxResults, pageLimit: args.pageLimit }; const [ownedLists, memberLists] = await Promise.all([ client.getOwnedLists(user.data.id, options), client.getListMemberships(user.data.id, options) ]); const ownedListsCount = ownedLists.meta.result_count || 0; const memberListsCount = memberLists.meta.result_count || 0; let responseText = `Found ${ownedListsCount} owned lists and ${memberListsCount} list memberships.\n\n`; if (ownedLists.data && ownedLists.data.length > 0) { responseText += 'Owned Lists:\n'; ownedLists.data.forEach((list) => { responseText += formatListInfo(list); }); responseText += '\n'; } if (memberLists.data && memberLists.data.length > 0) { responseText += 'Member of Lists:\n'; memberLists.data.forEach((list) => { responseText += formatListInfo(list); }); } const totalRetrieved = (ownedLists.meta.total_retrieved || 0) + (memberLists.meta.total_retrieved || 0); const totalRequested = args.maxResults ? args.maxResults * 2 : undefined; if (totalRequested && totalRetrieved >= totalRequested) { responseText += '\nNote: Maximum requested results reached. There might be more lists available.'; } return createResponse(responseText); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get user lists: ${error.message}`); } throw new Error('Failed to get user lists: Unknown error occurred'); } } function formatListInfo(list: ListV2): string { const name = list.name.length > 50 ? `${list.name.substring(0, 47)}...` : list.name; const description = list.description ? list.description.length > 100 ? `${list.description.substring(0, 97)}...` : list.description : ''; return `- ${name} (${list.member_count} members${list.private ? ', private' : ''})${ description ? `: ${description}` : '' }\n`; } export async function handleCreateList( client: TwitterClient, args: CreateListArgs ): Promise<HandlerResponse> { try { const list = await client.createList(args.name, args.description, args.isPrivate); if (!list.data) { throw new Error('Failed to create list'); } return createResponse(`Successfully created list: ${list.data.name}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to create list: ${error.message}`); } throw new Error('Failed to create list: Unknown error occurred'); } } export async function handleAddUserToList( client: TwitterClient, args: AddUserToListArgs ): Promise<HandlerResponse> { try { // First, verify the user exists and get their username for the response message const user = await client.getUserById(args.userId); if (!user.data) { throw new Error(`User with ID ${args.userId} not found`); } // Then verify the list exists and get its name for the response message const list = await client.getList(args.listId); if (!list.data) { throw new Error(`List with ID ${args.listId} not found`); } // Now try to add the user to the list await client.addListMember(args.listId, args.userId); return createResponse( `Successfully added user @${user.data.username} to list "${list.data.name}"` ); } catch (error) { if (error instanceof ApiResponseError) { // Handle specific Twitter API errors if (error.rateLimitError && error.rateLimit) { const resetMinutes = Math.ceil(error.rateLimit.reset / 60); throw new Error(`Rate limit exceeded. Please try again in ${resetMinutes} minutes.`); } if (error.code === 403) { throw new Error('You do not have permission to add members to this list.'); } if (error.code === 404) { throw new Error('The specified user or list could not be found.'); } throw new Error(`Twitter API Error: ${error.message}`); } if (error instanceof Error) { throw new Error(`Failed to add user to list: ${error.message}`); } throw new Error('Failed to add user to list: Unknown error occurred'); } } export async function handleRemoveUserFromList( client: TwitterClient, args: RemoveUserFromListArgs ): Promise<HandlerResponse> { try { await client.removeListMember(args.listId, args.userId); return createResponse(`Successfully removed user from list ${args.listId}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to remove user from list: ${error.message}`); } throw new Error('Failed to remove user from list: Unknown error occurred'); } } export async function handleGetListMembers( client: TwitterClient, args: GetListMembersArgs ): Promise<HandlerResponse> { try { const options = { max_results: args.maxResults, pageLimit: args.pageLimit, 'user.fields': args.userFields }; const members = await client.getListMembers(args.listId, options); if (!members.data || members.data.length === 0) { return createResponse(`No members found for list ${args.listId}`); } const memberCount = members.meta.result_count || 0; let responseText = `Found ${memberCount} members in list ${args.listId}:\n\n`; members.data.forEach((member) => { responseText += `- ${member.name} (@${member.username})\n`; }); if (members.meta.total_retrieved === args.maxResults) { responseText += '\nNote: Maximum requested results reached. There might be more members available.'; } return createResponse(responseText); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get list members: ${error.message}`); } throw new Error('Failed to get list members: Unknown error occurred'); } } ================ File: src/handlers/search.handlers.ts ================ import { TwitterClient } from '../twitterClient.js'; import { HandlerResponse, TwitterHandler } from '../types/handlers.js'; import { createResponse } from '../utils/response.js'; import { TweetV2, TwitterApiReadOnly, UserV2, TweetSearchRecentV2Paginator } from 'twitter-api-v2'; interface SearchTweetsArgs { query: string; maxResults?: number; tweetFields?: string[]; } interface HashtagAnalyticsArgs { hashtag: string; startTime?: string; endTime?: string; } interface TweetWithAuthor extends TweetV2 { author?: UserV2; } export const handleSearchTweets: TwitterHandler<SearchTweetsArgs> = async ( client: TwitterClient, { query, maxResults = 10, tweetFields }: SearchTweetsArgs ): Promise<HandlerResponse> => { try { const searchResult = await client.v2.search(query, { max_results: maxResults, 'tweet.fields': tweetFields?.join(',') || 'created_at,public_metrics', expansions: ['author_id'], 'user.fields': ['username'] }); const tweets = Array.isArray(searchResult.data) ? searchResult.data : []; if (tweets.length === 0) { return createResponse(`No tweets found for query: ${query}`); } const formattedTweets = tweets.map((tweet: TweetV2): TweetWithAuthor => ({ ...tweet, author: searchResult.includes?.users?.find(u => u.id === tweet.author_id) })); return createResponse(`Search results: ${JSON.stringify(formattedTweets, null, 2)}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to search tweets: ${error.message}`); } throw error; } }; export const handleHashtagAnalytics: TwitterHandler<HashtagAnalyticsArgs> = async ( client: TwitterClient, { hashtag, startTime, endTime }: HashtagAnalyticsArgs ): Promise<HandlerResponse> => { try { const query = `#${hashtag.replace(/^#/, '')}`; const searchResult = await client.v2.search(query, { max_results: 100, 'tweet.fields': 'public_metrics,created_at', start_time: startTime, end_time: endTime }); const tweets = Array.isArray(searchResult.data) ? searchResult.data : []; if (tweets.length === 0) { return createResponse(`No tweets found for hashtag: ${hashtag}`); } const analytics = { totalTweets: tweets.length, totalLikes: tweets.reduce((sum: number, tweet: TweetV2) => sum + (tweet.public_metrics?.like_count || 0), 0), totalRetweets: tweets.reduce((sum: number, tweet: TweetV2) => sum + (tweet.public_metrics?.retweet_count || 0), 0), totalReplies: tweets.reduce((sum: number, tweet: TweetV2) => sum + (tweet.public_metrics?.reply_count || 0), 0) }; return createResponse(`Hashtag Analytics for ${hashtag}:\n${JSON.stringify(analytics, null, 2)}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get hashtag analytics: ${error.message}`); } throw error; } }; ================ File: src/handlers/tweet.handlers.ts ================ import { TwitterClient } from '../client/twitter.js'; import { HandlerResponse } from '../types/handlers.js'; import { createResponse } from '../utils/response.js'; export interface MediaTweetHandlerArgs { text: string; mediaPath: string; mediaType: string; altText?: string; } export async function handlePostTweet( client: TwitterClient, { text }: { text: string } ): Promise<HandlerResponse> { try { const tweet = await client.v2.tweet(text); return createResponse(`Successfully posted tweet: ${tweet.data.id}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to post tweet: ${error.message}`); } throw new Error('Failed to post tweet: Unknown error occurred'); } } export async function handlePostTweetWithMedia( client: TwitterClient, { text, mediaPath, mediaType, altText }: MediaTweetHandlerArgs ): Promise<HandlerResponse> { try { // Upload media const mediaId = await client.v1.uploadMedia(mediaPath, { type: mediaType }); // Set alt text if provided if (altText) { await client.v1.createMediaMetadata(mediaId, { alt_text: { text: altText } }); } // Post tweet with media const tweet = await client.v2.tweet(text, { media: { media_ids: [mediaId] } }); return createResponse(`Successfully posted tweet with media: ${tweet.data.id}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to post tweet with media: ${error.message}`); } throw new Error('Failed to post tweet with media: Unknown error occurred'); } } export async function handleGetTweetById( client: TwitterClient, { tweetId }: { tweetId: string } ): Promise<HandlerResponse> { try { const tweet = await client.v2.singleTweet(tweetId, { 'tweet.fields': 'created_at,public_metrics,text' }); return createResponse(`Tweet details: ${JSON.stringify(tweet.data, null, 2)}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get tweet: ${error.message}`); } throw new Error('Failed to get tweet: Unknown error occurred'); } } export async function handleReplyToTweet( client: TwitterClient, { tweetId, text }: { tweetId: string; text: string } ): Promise<HandlerResponse> { try { const tweet = await client.v2.reply(text, tweetId); return createResponse(`Successfully replied to tweet: ${tweet.data.id}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to reply to tweet: ${error.message}`); } throw new Error('Failed to reply to tweet: Unknown error occurred'); } } export async function handleDeleteTweet( client: TwitterClient, { tweetId }: { tweetId: string } ): Promise<HandlerResponse> { try { await client.v2.deleteTweet(tweetId); return createResponse(`Successfully deleted tweet: ${tweetId}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to delete tweet: ${error.message}`); } throw new Error('Failed to delete tweet: Unknown error occurred'); } } // Add other tweet-related handlers... ================ File: src/handlers/user.handlers.ts ================ import { TwitterClient } from '../twitterClient.js'; import { UserV2, TTweetv2UserField } from 'twitter-api-v2'; import { HandlerResponse, TwitterHandler, UserHandlerArgs } from '../types/handlers.js'; import { createResponse } from '../utils/response.js'; interface GetUserInfoArgs extends UserHandlerArgs { fields?: TTweetv2UserField[]; } interface GetUserTimelineArgs extends UserHandlerArgs { maxResults?: number; tweetFields?: string[]; } interface GetFollowersArgs extends UserHandlerArgs { maxResults?: number; userFields?: string[]; } interface GetFollowingArgs extends UserHandlerArgs { maxResults?: number; userFields?: string[]; } export const handleGetUserInfo: TwitterHandler<GetUserInfoArgs> = async ( client: TwitterClient, { username, fields }: GetUserInfoArgs ): Promise<HandlerResponse> => { const user = await client.v2.userByUsername( username, { 'user.fields': fields || ['description', 'public_metrics', 'profile_image_url', 'verified'] as TTweetv2UserField[] } ); if (!user.data) { throw new Error(`User not found: ${username}`); } return createResponse(`User info: ${JSON.stringify(user.data, null, 2)}`); }; export const handleGetUserTimeline: TwitterHandler<GetUserTimelineArgs> = async ( client: TwitterClient, { username, maxResults, tweetFields }: GetUserTimelineArgs ): Promise<HandlerResponse> => { const userResponse = await client.v2.userByUsername(username); if (!userResponse.data) { throw new Error(`User not found: ${username}`); } const tweets = await client.v2.userTimeline(userResponse.data.id, { max_results: maxResults, 'tweet.fields': tweetFields?.join(',') }); return createResponse(`User timeline: ${JSON.stringify(tweets.data, null, 2)}`); }; export const handleFollowUser: TwitterHandler<UserHandlerArgs> = async ( client: TwitterClient, { username }: UserHandlerArgs ): Promise<HandlerResponse> => { try { const userId = await client.v2.me().then(response => response.data.id); const targetUser = await client.v2.userByUsername(username); if (!targetUser.data) { throw new Error(`User not found: ${username}`); } await client.v2.follow(userId, targetUser.data.id); return createResponse(`Successfully followed user: ${username}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to follow user: ${error.message}`); } throw error; } }; export const handleUnfollowUser: TwitterHandler<UserHandlerArgs> = async ( client: TwitterClient, { username }: UserHandlerArgs ): Promise<HandlerResponse> => { try { const userId = await client.v2.me().then(response => response.data.id); const targetUser = await client.v2.userByUsername(username); if (!targetUser.data) { throw new Error(`User not found: ${username}`); } await client.v2.unfollow(userId, targetUser.data.id); return createResponse(`Successfully unfollowed user: ${username}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to unfollow user: ${error.message}`); } throw error; } }; export const handleGetFollowers: TwitterHandler<GetFollowersArgs> = async ( client: TwitterClient, { username, maxResults, userFields }: GetFollowersArgs ): Promise<HandlerResponse> => { try { const user = await client.v2.userByUsername(username); if (!user.data) { throw new Error(`User not found: ${username}`); } const followers = await client.v2.followers(user.data.id, { max_results: maxResults, 'user.fields': userFields?.join(',') || 'description,public_metrics' }); if (!followers.data) { return createResponse(`No followers found for user: ${username}`); } return createResponse(`Followers for ${username}: ${JSON.stringify(followers.data, null, 2)}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get followers: ${error.message}`); } throw error; } }; export const handleGetFollowing: TwitterHandler<GetFollowingArgs> = async ( client: TwitterClient, { username, maxResults, userFields }: GetFollowingArgs ): Promise<HandlerResponse> => { try { const user = await client.v2.userByUsername(username); if (!user.data) { throw new Error(`User not found: ${username}`); } const following = await client.v2.following(user.data.id, { max_results: maxResults, 'user.fields': userFields?.join(',') || 'description,profile_image_url,public_metrics,verified' }); if (!following.data || following.data.length === 0) { return createResponse(`User ${username} is not following anyone`); } return createResponse(`Users followed by ${username}: ${JSON.stringify(following.data, null, 2)}`); } catch (error) { if (error instanceof Error) { throw new Error(`Failed to get following: ${error.message}`); } throw error; } }; ================ File: src/types/handlers.ts ================ import { TwitterClient } from '../client/twitter.js'; export interface HandlerResponse { response: string; tools?: Record<string, any>; } export interface MediaTweetHandlerArgs { text: string; mediaPath: string; mediaType: string; altText?: string; } export interface TweetHandlerArgs { text: string; } export interface TweetEngagementArgs { tweetId: string; } export interface UserHandlerArgs { username: string; } export interface GetUserInfoArgs { username: string; } export interface GetUserTimelineArgs { username: string; maxResults?: number; } export interface AddUserToListArgs { listId: string; userId: string; } export interface RemoveUserFromListArgs { listId: string; userId: string; } export interface GetListMembersArgs { listId: string; maxResults?: number; userFields?: string[]; } export interface GetUserListsArgs { username: string; maxResults?: number; } export interface SearchTweetsArgs { query: string; maxResults?: number; } export interface HashtagAnalyticsArgs { hashtag: string; startTime?: string; endTime?: string; } export type TwitterHandler<T> = (client: TwitterClient, args: T) => Promise<HandlerResponse>; ================ File: src/utils/response.ts ================ import { HandlerResponse } from '../types/handlers.js'; export function createResponse(text: string, tools?: Record<string, any>): HandlerResponse { return { response: text, tools }; } ================ File: src/index.ts ================ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { TwitterClient } from './client/twitter.js'; import { TOOLS } from './tools.js'; import { config } from 'dotenv'; import { handlePostTweet, handlePostTweetWithMedia, handleGetTweetById, handleReplyToTweet, handleDeleteTweet } from './handlers/tweet.handlers.js'; import { handleLikeTweet, handleUnlikeTweet, handleRetweet, handleUndoRetweet, handleGetRetweets, handleGetLikedTweets } from './handlers/engagement.handlers.js'; import { handleGetUserInfo, handleGetUserTimeline, handleFollowUser, handleUnfollowUser, handleGetFollowers, handleGetFollowing } from './handlers/user.handlers.js'; import { handleCreateList, handleAddUserToList, handleRemoveUserFromList, handleGetListMembers, handleGetUserLists } from './handlers/list.handlers.js'; import { handleSearchTweets, handleHashtagAnalytics } from './handlers/search.handlers.js'; // Load environment variables config(); const server = new Server({ name: 'twitter-mcp-server', version: '0.0.1', }, { capabilities: { tools: TOOLS } }); // Initialize Twitter client with all required credentials const client = new TwitterClient({ appKey: process.env.X_API_KEY || '', appSecret: process.env.X_API_SECRET || '', accessToken: process.env.X_ACCESS_TOKEN || '', accessSecret: process.env.X_ACCESS_TOKEN_SECRET || '', }); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: Object.entries(TOOLS).map(([name, tool]) => ({ name, ...tool })) })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { let response; switch (request.params.name) { case 'postTweet': { const { text } = request.params.arguments as { text: string }; response = await handlePostTweet(client, { text }); break; } case 'postTweetWithMedia': { const { text, mediaPath, mediaType, altText } = request.params.arguments as { text: string; mediaPath: string; mediaType: string; altText?: string; }; response = await handlePostTweetWithMedia(client, { text, mediaPath, mediaType, altText }); break; } case 'getTweetById': { const { tweetId } = request.params.arguments as { tweetId: string }; response = await handleGetTweetById(client, { tweetId }); break; } case 'replyToTweet': { const { tweetId, text } = request.params.arguments as { tweetId: string; text: string }; response = await handleReplyToTweet(client, { tweetId, text }); break; } case 'deleteTweet': { const { tweetId } = request.params.arguments as { tweetId: string }; response = await handleDeleteTweet(client, { tweetId }); break; } case 'likeTweet': { const { tweetId } = request.params.arguments as { tweetId: string }; response = await handleLikeTweet(client, { tweetId }); break; } case 'unlikeTweet': { const { tweetId } = request.params.arguments as { tweetId: string }; response = await handleUnlikeTweet(client, { tweetId }); break; } case 'retweet': { const { tweetId } = request.params.arguments as { tweetId: string }; response = await handleRetweet(client, { tweetId }); break; } case 'undoRetweet': { const { tweetId } = request.params.arguments as { tweetId: string }; response = await handleUndoRetweet(client, { tweetId }); break; } case 'getRetweets': { const { tweetId, maxResults } = request.params.arguments as { tweetId: string; maxResults?: number }; response = await handleGetRetweets(client, { tweetId, maxResults }); break; } case 'getLikedTweets': { const { userId, maxResults } = request.params.arguments as { userId: string; maxResults?: number }; response = await handleGetLikedTweets(client, { userId, maxResults }); break; } case 'getUserInfo': { const { username } = request.params.arguments as { username: string }; response = await handleGetUserInfo(client, { username }); break; } case 'getUserTimeline': { const { username, maxResults } = request.params.arguments as { username: string; maxResults?: number }; response = await handleGetUserTimeline(client, { username, maxResults }); break; } case 'followUser': { const { username } = request.params.arguments as { username: string }; response = await handleFollowUser(client, { username }); break; } case 'unfollowUser': { const { username } = request.params.arguments as { username: string }; response = await handleUnfollowUser(client, { username }); break; } case 'getFollowers': { const { username, maxResults } = request.params.arguments as { username: string; maxResults?: number }; response = await handleGetFollowers(client, { username, maxResults }); break; } case 'getFollowing': { const { username, maxResults } = request.params.arguments as { username: string; maxResults?: number }; response = await handleGetFollowing(client, { username, maxResults }); break; } case 'createList': { const { name, description, isPrivate } = request.params.arguments as { name: string; description?: string; isPrivate?: boolean; }; response = await handleCreateList(client, { name, description, isPrivate }); break; } case 'addUserToList': { const { listId, userId } = request.params.arguments as { listId: string; userId: string }; response = await handleAddUserToList(client, { listId, userId }); break; } case 'removeUserFromList': { const { listId, userId } = request.params.arguments as { listId: string; userId: string }; response = await handleRemoveUserFromList(client, { listId, userId }); break; } case 'getListMembers': { const { listId, maxResults, userFields } = request.params.arguments as { listId: string; maxResults?: number; userFields?: string[]; }; response = await handleGetListMembers(client, { listId, maxResults, userFields }); break; } case 'getUserLists': { const { username, maxResults } = request.params.arguments as { username: string; maxResults?: number }; response = await handleGetUserLists(client, { username, maxResults }); break; } case 'searchTweets': { const { query, maxResults } = request.params.arguments as { query: string; maxResults?: number }; response = await handleSearchTweets(client, { query, maxResults }); break; } case 'getHashtagAnalytics': { const { hashtag, startTime, endTime } = request.params.arguments as { hashtag: string; startTime?: string; endTime?: string; }; response = await handleHashtagAnalytics(client, { hashtag, startTime, endTime }); break; } default: throw new Error(`Unknown tool: ${request.params.name}`); } return { content: [{ type: 'text', text: response.response }], tools: response.tools }; } catch (error) { if (error instanceof Error) { return { content: [{ type: 'text', text: `Error: ${error.message}` }] }; } return { content: [{ type: 'text', text: 'An unknown error occurred' }] }; } }); const transport = new StdioServerTransport(); server.connect(transport).catch(console.error); ================ File: src/tools.ts ================ import { z } from 'zod'; export const TOOLS = { postTweet: { description: 'Post a tweet to Twitter', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'The text of the tweet' }, }, required: ['text'], }, }, postTweetWithMedia: { description: 'Post a tweet with media attachment to Twitter', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'The text of the tweet' }, mediaPath: { type: 'string', description: 'Local file path to the media to upload' }, mediaType: { type: 'string', enum: ['image/jpeg', 'image/png', 'image/gif', 'video/mp4'], description: 'MIME type of the media file' }, altText: { type: 'string', description: 'Alternative text for the media (accessibility)' } }, required: ['text', 'mediaPath', 'mediaType'], }, }, likeTweet: { description: 'Like a tweet by its ID', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet to like' } }, required: ['tweetId'], }, }, unlikeTweet: { description: 'Unlike a previously liked tweet', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet to unlike' } }, required: ['tweetId'], }, }, getLikedTweets: { description: 'Get a list of tweets liked by a user', inputSchema: { type: 'object', properties: { userId: { type: 'string', description: 'The ID of the user whose likes to fetch' }, maxResults: { type: 'number', description: 'The maximum number of results to return (default: 100, max: 100)', minimum: 1, maximum: 100 }, tweetFields: { type: 'array', items: { type: 'string', enum: ['created_at', 'author_id', 'conversation_id', 'public_metrics', 'entities', 'context_annotations'] }, description: 'Additional tweet fields to include in the response' }, }, required: ['userId'], }, }, searchTweets: { description: 'Search for tweets using a query string', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The search query' }, maxResults: { type: 'number', description: 'Maximum number of results to return' }, tweetFields: { type: 'array', items: { type: 'string' }, description: 'Fields to include in the tweet objects' } }, required: ['query'] } }, replyToTweet: { description: 'Reply to a tweet', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet to reply to' }, text: { type: 'string', description: 'The text of the reply' } }, required: ['tweetId', 'text'] } }, getUserTimeline: { description: 'Get recent tweets from a user timeline', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'The username of the user' }, }, required: ['username'], }, }, getTweetById: { description: 'Get a tweet by its ID', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet' }, tweetFields: { type: 'array', items: { type: 'string' }, description: 'Fields to include in the tweet object' } }, required: ['tweetId'] } }, getUserInfo: { description: 'Get information about a Twitter user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'The username of the user' }, }, required: ['username'], }, }, getTweetsByIds: { description: 'Get multiple tweets by their IDs', inputSchema: { type: 'object', properties: { tweetIds: { type: 'array', items: { type: 'string' }, description: 'Array of tweet IDs to fetch', maxItems: 100 }, tweetFields: { type: 'array', items: { type: 'string', enum: ['created_at', 'author_id', 'conversation_id', 'public_metrics', 'entities', 'context_annotations'] }, description: 'Additional tweet fields to include in the response' }, }, required: ['tweetIds'], }, }, retweet: { description: 'Retweet a tweet by its ID', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet to retweet' } }, required: ['tweetId'], }, }, undoRetweet: { description: 'Undo a retweet by its ID', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet to un-retweet' } }, required: ['tweetId'], }, }, getRetweets: { description: 'Get a list of retweets of a tweet', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet to get retweets for' }, maxResults: { type: 'number', description: 'The maximum number of results to return (default: 100, max: 100)', minimum: 1, maximum: 100 }, userFields: { type: 'array', items: { type: 'string', enum: ['description', 'profile_image_url', 'public_metrics', 'verified'] }, description: 'Additional user fields to include in the response' }, }, required: ['tweetId'], }, }, followUser: { description: 'Follow a user by their username', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'The username of the user to follow' } }, required: ['username'], }, }, unfollowUser: { description: 'Unfollow a user by their username', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'The username of the user to unfollow' } }, required: ['username'], }, }, getFollowers: { description: 'Get followers of a user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'The username of the account' }, maxResults: { type: 'number', description: 'Maximum number of followers to return' }, userFields: { type: 'array', items: { type: 'string' }, description: 'Fields to include in the user objects' } }, required: ['username'] } }, getFollowing: { description: 'Get a list of users that a user is following', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'The username of the user whose following list to fetch' }, maxResults: { type: 'number', description: 'The maximum number of results to return (default: 100, max: 1000)', minimum: 1, maximum: 1000 }, userFields: { type: 'array', items: { type: 'string', enum: ['description', 'profile_image_url', 'public_metrics', 'verified', 'location', 'url'] }, description: 'Additional user fields to include in the response' }, }, required: ['username'], }, }, createList: { description: 'Create a new Twitter list', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'The name of the list' }, description: { type: 'string', description: 'A description of the list' }, private: { type: 'boolean', description: 'Whether the list should be private' } }, required: ['name'], }, }, addUserToList: { description: 'Add a user to a Twitter list', inputSchema: { type: 'object', properties: { listId: { type: 'string', description: 'The ID of the list' }, username: { type: 'string', description: 'The username of the user to add' } }, required: ['listId', 'username'], }, }, removeUserFromList: { description: 'Remove a user from a Twitter list', inputSchema: { type: 'object', properties: { listId: { type: 'string', description: 'The ID of the list' }, username: { type: 'string', description: 'The username of the user to remove' } }, required: ['listId', 'username'], }, }, getListMembers: { description: 'Get members of a Twitter list', inputSchema: { type: 'object', properties: { listId: { type: 'string', description: 'The ID of the list' }, maxResults: { type: 'number', description: 'The maximum number of results to return (default: 100, max: 100)', minimum: 1, maximum: 100 }, userFields: { type: 'array', items: { type: 'string', enum: ['description', 'profile_image_url', 'public_metrics', 'verified', 'location', 'url'] }, description: 'Additional user fields to include in the response' }, }, required: ['listId'], }, }, getUserLists: { description: 'Get lists owned by a user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'The username of the user whose lists to fetch' }, maxResults: { type: 'number', description: 'The maximum number of results to return (default: 100, max: 100)', minimum: 1, maximum: 100 }, listFields: { type: 'array', items: { type: 'string', enum: ['created_at', 'follower_count', 'member_count', 'private', 'description'] }, description: 'Additional list fields to include in the response' }, }, required: ['username'], }, }, getHashtagAnalytics: { description: 'Get analytics for a specific hashtag', inputSchema: { type: 'object', properties: { hashtag: { type: 'string', description: 'The hashtag to analyze (with or without #)' }, startTime: { type: 'string', description: 'Start time for the analysis (ISO 8601)' }, endTime: { type: 'string', description: 'End time for the analysis (ISO 8601)' } }, required: ['hashtag'] } }, deleteTweet: { description: 'Delete a tweet by its ID', inputSchema: { type: 'object', properties: { tweetId: { type: 'string', description: 'The ID of the tweet to delete' } }, required: ['tweetId'] } }, }; ================ File: src/twitterClient.ts ================ import { TwitterApi } from 'twitter-api-v2'; export type TwitterClient = TwitterApi; export function getTwitterClient() { const client = new TwitterApi({ appKey: process.env.X_API_KEY || '', appSecret: process.env.X_API_SECRET || '', accessToken: process.env.X_ACCESS_TOKEN || '', accessSecret: process.env.X_ACCESS_TOKEN_SECRET || '', }); return client; } ================ File: src/types.ts ================ export interface PostTweetArgs { text: string; } export interface PostTweetWithMediaArgs { text: string; mediaPath: string; mediaType: 'image/jpeg' | 'image/png' | 'image/gif' | 'video/mp4'; altText?: string; } export interface SearchTweetsArgs { query: string; since?: string; until?: string; tweetFields?: string[]; } export interface ReplyToTweetArgs { tweetId: string; text: string; } export interface GetUserTimelineArgs { username: string; } export interface GetTweetByIdArgs { tweetId: string; } export interface GetUserInfoArgs { username: string; } export interface GetTweetsByIdsArgs { tweetIds: string[]; tweetFields?: string[]; } export interface LikeTweetArgs { tweetId: string; } export interface UnlikeTweetArgs { tweetId: string; } export interface GetLikedTweetsArgs { userId: string; maxResults?: number; tweetFields?: string[]; } export interface RetweetArgs { tweetId: string; } export interface UndoRetweetArgs { tweetId: string; } export interface GetRetweetsArgs { tweetId: string; maxResults?: number; userFields?: string[]; } export interface FollowUserArgs { username: string; } export interface UnfollowUserArgs { username: string; } export interface GetFollowersArgs { username: string; maxResults?: number; userFields?: string[]; } export interface GetFollowingArgs { username: string; maxResults?: number; userFields?: string[]; } export interface CreateListArgs { name: string; description?: string; private?: boolean; } export interface AddUserToListArgs { listId: string; username: string; } export interface RemoveUserFromListArgs { listId: string; username: string; } export interface GetListMembersArgs { listId: string; maxResults?: number; userFields?: string[]; } export interface GetUserListsArgs { username: string; maxResults?: number; listFields?: string[]; } export interface GetHashtagAnalyticsArgs { hashtag: string; maxResults?: number; tweetFields?: string[]; } export function assertPostTweetArgs(args: unknown): asserts args is PostTweetArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('text' in args) || typeof (args as any).text !== 'string') { throw new Error('Invalid arguments: expected text string'); } } export function assertPostTweetWithMediaArgs(args: unknown): asserts args is PostTweetWithMediaArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('text' in args) || typeof (args as any).text !== 'string') { throw new Error('Invalid arguments: expected text string'); } if (!('mediaPath' in args) || typeof (args as any).mediaPath !== 'string') { throw new Error('Invalid arguments: expected mediaPath string'); } if (!('mediaType' in args) || typeof (args as any).mediaType !== 'string') { throw new Error('Invalid arguments: expected mediaType string'); } const validMediaTypes = ['image/jpeg', 'image/png', 'image/gif', 'video/mp4']; if (!validMediaTypes.includes((args as any).mediaType)) { throw new Error(`Invalid arguments: mediaType must be one of: ${validMediaTypes.join(', ')}`); } if ('altText' in args && typeof (args as any).altText !== 'string') { throw new Error('Invalid arguments: expected altText to be a string'); } } export function assertSearchTweetsArgs(args: unknown): asserts args is SearchTweetsArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('query' in args) || typeof (args as any).query !== 'string') { throw new Error('Invalid arguments: expected query string'); } if ('since' in args && typeof (args as any).since !== 'string') { throw new Error('Invalid arguments: expected since to be an ISO 8601 date string'); } if ('until' in args && typeof (args as any).until !== 'string') { throw new Error('Invalid arguments: expected until to be an ISO 8601 date string'); } if ('tweetFields' in args) { if (!Array.isArray((args as any).tweetFields)) { throw new Error('Invalid arguments: expected tweetFields to be an array'); } for (const field of (args as any).tweetFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected tweetFields to be an array of strings'); } } } } export function assertReplyToTweetArgs(args: unknown): asserts args is ReplyToTweetArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetId' in args) || typeof (args as any).tweetId !== 'string') { throw new Error('Invalid arguments: expected tweetId string'); } if (!('text' in args) || typeof (args as any).text !== 'string') { throw new Error('Invalid arguments: expected text string'); } } export function assertGetUserTimelineArgs(args: unknown): asserts args is GetUserTimelineArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } } export function assertGetTweetByIdArgs(args: unknown): asserts args is GetTweetByIdArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetId' in args) || typeof (args as any).tweetId !== 'string') { throw new Error('Invalid arguments: expected tweetId string'); } } export function assertGetUserInfoArgs(args: unknown): asserts args is GetUserInfoArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } } export function assertGetTweetsByIdsArgs(args: unknown): asserts args is GetTweetsByIdsArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetIds' in args) || !Array.isArray((args as any).tweetIds)) { throw new Error('Invalid arguments: expected tweetIds array'); } if ((args as any).tweetIds.length === 0) { throw new Error('Invalid arguments: tweetIds array cannot be empty'); } if ((args as any).tweetIds.length > 100) { throw new Error('Invalid arguments: cannot fetch more than 100 tweets at once'); } for (const id of (args as any).tweetIds) { if (typeof id !== 'string') { throw new Error('Invalid arguments: expected tweetIds to be an array of strings'); } } if ('tweetFields' in args) { if (!Array.isArray((args as any).tweetFields)) { throw new Error('Invalid arguments: expected tweetFields to be an array'); } for (const field of (args as any).tweetFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected tweetFields to be an array of strings'); } } } } export function assertLikeTweetArgs(args: unknown): asserts args is LikeTweetArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetId' in args) || typeof (args as any).tweetId !== 'string') { throw new Error('Invalid arguments: expected tweetId string'); } } export function assertUnlikeTweetArgs(args: unknown): asserts args is UnlikeTweetArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetId' in args) || typeof (args as any).tweetId !== 'string') { throw new Error('Invalid arguments: expected tweetId string'); } } export function assertGetLikedTweetsArgs(args: unknown): asserts args is GetLikedTweetsArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('userId' in args) || typeof (args as any).userId !== 'string') { throw new Error('Invalid arguments: expected userId string'); } if ('maxResults' in args) { const maxResults = (args as any).maxResults; if (typeof maxResults !== 'number' || maxResults < 1 || maxResults > 100) { throw new Error('Invalid arguments: maxResults must be a number between 1 and 100'); } } if ('tweetFields' in args) { if (!Array.isArray((args as any).tweetFields)) { throw new Error('Invalid arguments: expected tweetFields to be an array'); } for (const field of (args as any).tweetFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected tweetFields to be an array of strings'); } } } } export function assertRetweetArgs(args: unknown): asserts args is RetweetArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetId' in args) || typeof (args as any).tweetId !== 'string') { throw new Error('Invalid arguments: expected tweetId string'); } } export function assertUndoRetweetArgs(args: unknown): asserts args is UndoRetweetArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetId' in args) || typeof (args as any).tweetId !== 'string') { throw new Error('Invalid arguments: expected tweetId string'); } } export function assertGetRetweetsArgs(args: unknown): asserts args is GetRetweetsArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('tweetId' in args) || typeof (args as any).tweetId !== 'string') { throw new Error('Invalid arguments: expected tweetId string'); } if ('maxResults' in args) { const maxResults = (args as any).maxResults; if (typeof maxResults !== 'number' || maxResults < 1 || maxResults > 100) { throw new Error('Invalid arguments: maxResults must be a number between 1 and 100'); } } if ('userFields' in args) { if (!Array.isArray((args as any).userFields)) { throw new Error('Invalid arguments: expected userFields to be an array'); } for (const field of (args as any).userFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected userFields to be an array of strings'); } } } } export function assertFollowUserArgs(args: unknown): asserts args is FollowUserArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } } export function assertUnfollowUserArgs(args: unknown): asserts args is UnfollowUserArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } } export function assertGetFollowersArgs(args: unknown): asserts args is GetFollowersArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } if ('maxResults' in args) { const maxResults = (args as any).maxResults; if (typeof maxResults !== 'number' || maxResults < 1 || maxResults > 1000) { throw new Error('Invalid arguments: maxResults must be a number between 1 and 1000'); } } if ('userFields' in args) { if (!Array.isArray((args as any).userFields)) { throw new Error('Invalid arguments: expected userFields to be an array'); } for (const field of (args as any).userFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected userFields to be an array of strings'); } } } } export function assertGetFollowingArgs(args: unknown): asserts args is GetFollowingArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } if ('maxResults' in args) { const maxResults = (args as any).maxResults; if (typeof maxResults !== 'number' || maxResults < 1 || maxResults > 1000) { throw new Error('Invalid arguments: maxResults must be a number between 1 and 1000'); } } if ('userFields' in args) { if (!Array.isArray((args as any).userFields)) { throw new Error('Invalid arguments: expected userFields to be an array'); } for (const field of (args as any).userFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected userFields to be an array of strings'); } } } } export function assertCreateListArgs(args: unknown): asserts args is CreateListArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('name' in args) || typeof (args as any).name !== 'string') { throw new Error('Invalid arguments: expected name string'); } if ('description' in args && typeof (args as any).description !== 'string') { throw new Error('Invalid arguments: expected description string'); } if ('private' in args && typeof (args as any).private !== 'boolean') { throw new Error('Invalid arguments: expected private boolean'); } } export function assertAddUserToListArgs(args: unknown): asserts args is AddUserToListArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('listId' in args) || typeof (args as any).listId !== 'string') { throw new Error('Invalid arguments: expected listId string'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } } export function assertRemoveUserFromListArgs(args: unknown): asserts args is RemoveUserFromListArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('listId' in args) || typeof (args as any).listId !== 'string') { throw new Error('Invalid arguments: expected listId string'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } } export function assertGetListMembersArgs(args: unknown): asserts args is GetListMembersArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('listId' in args) || typeof (args as any).listId !== 'string') { throw new Error('Invalid arguments: expected listId string'); } if ('maxResults' in args) { const maxResults = (args as any).maxResults; if (typeof maxResults !== 'number' || maxResults < 1 || maxResults > 100) { throw new Error('Invalid arguments: maxResults must be a number between 1 and 100'); } } if ('userFields' in args) { if (!Array.isArray((args as any).userFields)) { throw new Error('Invalid arguments: expected userFields to be an array'); } for (const field of (args as any).userFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected userFields to be an array of strings'); } } } } export function assertGetUserListsArgs(args: unknown): asserts args is GetUserListsArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('username' in args) || typeof (args as any).username !== 'string') { throw new Error('Invalid arguments: expected username string'); } if ('maxResults' in args) { const maxResults = (args as any).maxResults; if (typeof maxResults !== 'number' || maxResults < 1 || maxResults > 100) { throw new Error('Invalid arguments: maxResults must be a number between 1 and 100'); } } if ('listFields' in args) { if (!Array.isArray((args as any).listFields)) { throw new Error('Invalid arguments: expected listFields to be an array'); } for (const field of (args as any).listFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected listFields to be an array of strings'); } } } } export function assertGetHashtagAnalyticsArgs(args: unknown): asserts args is GetHashtagAnalyticsArgs { if (typeof args !== 'object' || args === null) { throw new Error('Invalid arguments: expected object'); } if (!('hashtag' in args) || typeof (args as any).hashtag !== 'string') { throw new Error('Invalid arguments: expected hashtag string'); } if ('maxResults' in args) { const maxResults = (args as any).maxResults; if (typeof maxResults !== 'number' || maxResults < 10 || maxResults > 100) { throw new Error('Invalid arguments: maxResults must be a number between 10 and 100'); } } if ('tweetFields' in args) { if (!Array.isArray((args as any).tweetFields)) { throw new Error('Invalid arguments: expected tweetFields to be an array'); } for (const field of (args as any).tweetFields) { if (typeof field !== 'string') { throw new Error('Invalid arguments: expected tweetFields to be an array of strings'); } } } } ================ File: .env.example ================ # X (Twitter) API Credentials # Get these from the X Developer Portal (https://developer.twitter.com/en/portal/dashboard) X_API_KEY= X_API_SECRET= X_ACCESS_TOKEN= X_ACCESS_TOKEN_SECRET= ================ File: .gitignore ================ # Dependency directories node_modules/ # Environment variables .env # Build output dist/ # Logs *.log # IDE files .vscode/ .idea/ *.swp *.swo # OS files .DS_Store Thumbs.db ================ File: LICENSE ================ MIT License Copyright (c) 2024 MCP Twitter Server Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================ File: package.json ================ { "name": "mcp-twitter-server", "version": "0.1.0", "description": "A Model Context Protocol server implementation for Twitter API integration", "main": "dist/index.js", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsc -w", "prepare": "npm run build" }, "keywords": [ "twitter", "mcp", "model-context-protocol", "llm", "ai", "claude" ], "author": "Dennis Reimann", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/crazyrabbitLTC/mcp-twitter-server.git" }, "bugs": { "url": "https://github.com/crazyrabbitLTC/mcp-twitter-server/issues" }, "homepage": "https://github.com/crazyrabbitLTC/mcp-twitter-server#readme", "dependencies": { "@modelcontextprotocol/sdk": "^0.0.2", "dotenv": "^16.3.1", "twitter-api-v2": "^1.15.2", "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.10.5", "typescript": "^5.3.3" }, "files": [ "dist", "README.md", "LICENSE" ], "publishConfig": { "access": "public" } } ================ File: README.md ================ # Twitter MCP Server A Model Context Protocol server implementation for Twitter API integration. ## Setup 1. Clone the repository 2. Install dependencies: `npm install` 3. Copy `.env.example` to `.env` and fill in your Twitter API credentials 4. Build the project: `npm run build` 5. Start the server: `npm start` ## Environment Variables Required Twitter API credentials in `.env`: ```env X_API_KEY=your_api_key X_API_SECRET=your_api_secret X_ACCESS_TOKEN=your_access_token X_ACCESS_TOKEN_SECRET=your_access_token_secret ``` ## Available Tools ### Tweet Operations - `postTweet`: Post a new tweet ```json { "text": "Your tweet text here" } ``` - `postTweetWithMedia`: Post a tweet with media attachment ```json { "text": "Your tweet text", "mediaPath": "path/to/media/file", "mediaType": "image/jpeg|image/png|image/gif|video/mp4", "altText": "Optional alt text for accessibility" } ``` - `getTweetById`: Get a specific tweet by ID ```json { "tweetId": "tweet_id", "tweetFields": ["created_at", "public_metrics"] } ``` - `replyToTweet`: Reply to an existing tweet ```json { "tweetId": "tweet_id", "text": "Your reply text" } ``` - `deleteTweet`: Delete a tweet ```json { "tweetId": "tweet_id" } ``` ### Search & Analytics - `searchTweets`: Search for tweets ```json { "query": "search query", "maxResults": 10, "tweetFields": ["created_at", "public_metrics"] } ``` - `getHashtagAnalytics`: Get analytics for a hashtag ```json { "hashtag": "hashtag", "startTime": "ISO-8601 date", "endTime": "ISO-8601 date" } ``` ### User Operations - `getUserInfo`: Get user information ```json { "username": "twitter_username", "fields": ["description", "public_metrics"] } ``` - `getUserTimeline`: Get user's tweets ```json { "username": "twitter_username", "maxResults": 10, "tweetFields": ["created_at", "public_metrics"] } ``` - `getFollowers`: Get user's followers ```json { "username": "twitter_username", "maxResults": 100, "userFields": ["description", "public_metrics"] } ``` - `getFollowing`: Get accounts a user follows ```json { "username": "twitter_username", "maxResults": 100, "userFields": ["description", "public_metrics"] } ``` ### Engagement - `likeTweet`: Like a tweet ```json { "tweetId": "tweet_id" } ``` - `unlikeTweet`: Unlike a tweet ```json { "tweetId": "tweet_id" } ``` - `retweet`: Retweet a tweet ```json { "tweetId": "tweet_id" } ``` - `undoRetweet`: Undo a retweet ```json { "tweetId": "tweet_id" } ``` - `getRetweets`: Get users who retweeted a tweet ```json { "tweetId": "tweet_id", "maxResults": 100, "userFields": ["description", "public_metrics"] } ``` - `getLikedTweets`: Get tweets liked by a user ```json { "userId": "user_id", "maxResults": 100, "tweetFields": ["created_at", "public_metrics"] } ``` ### List Management - `createList`: Create a new list ```json { "name": "List name", "description": "List description", "isPrivate": false } ``` - `addUserToList`: Add a user to a list ```json { "listId": "list_id", "username": "twitter_username" } ``` - `removeUserFromList`: Remove a user from a list ```json { "listId": "list_id", "username": "twitter_username" } ``` - `getListMembers`: Get members of a list ```json { "listId": "list_id", "maxResults": 100, "userFields": ["description", "public_metrics"] } ``` ## Error Handling All tools return standardized error responses: - Missing parameters: `Missing required parameter: parameter_name` - API errors: Error message from Twitter API - Not found errors: Appropriate "not found" message for the resource ## Response Format All successful responses follow this format: ```json { "content": [ { "type": "text", "text": "Operation result message" } ] } ``` ## Development - Build: `npm run build` - Start: `npm start` - Watch mode: `npm run dev` ``` ================ File: tsconfig.json ================ { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }