Twitter MCP Server
by EnesCinr
- src
import { TwitterApi } from 'twitter-api-v2';
import { Config, TwitterError, Tweet, TwitterUser, PostedTweet } from './types.js';
export class TwitterClient {
private client: TwitterApi;
private rateLimitMap = new Map<string, number>();
constructor(config: Config) {
this.client = new TwitterApi({
appKey: config.apiKey,
appSecret: config.apiSecretKey,
accessToken: config.accessToken,
accessSecret: config.accessTokenSecret,
});
console.error('Twitter API client initialized');
}
async postTweet(text: string): Promise<PostedTweet> {
try {
const endpoint = 'tweets/create';
await this.checkRateLimit(endpoint);
const response = await this.client.v2.tweet(text);
console.error(`Tweet posted successfully with ID: ${response.data.id}`);
return {
id: response.data.id,
text: response.data.text
};
} catch (error) {
this.handleApiError(error);
}
}
async searchTweets(query: string, count: number): Promise<{ tweets: Tweet[], users: TwitterUser[] }> {
try {
const endpoint = 'tweets/search';
await this.checkRateLimit(endpoint);
const response = await this.client.v2.search(query, {
max_results: count,
expansions: ['author_id'],
'tweet.fields': ['public_metrics', 'created_at'],
'user.fields': ['username', 'name', 'verified']
});
console.error(`Fetched ${response.tweets.length} tweets for query: "${query}"`);
const tweets = response.tweets.map(tweet => ({
id: tweet.id,
text: tweet.text,
authorId: tweet.author_id ?? '',
metrics: {
likes: tweet.public_metrics?.like_count ?? 0,
retweets: tweet.public_metrics?.retweet_count ?? 0,
replies: tweet.public_metrics?.reply_count ?? 0,
quotes: tweet.public_metrics?.quote_count ?? 0
},
createdAt: tweet.created_at ?? ''
}));
const users = response.includes.users.map(user => ({
id: user.id,
username: user.username,
name: user.name,
verified: user.verified ?? false
}));
return { tweets, users };
} catch (error) {
this.handleApiError(error);
}
}
private async checkRateLimit(endpoint: string): Promise<void> {
const lastRequest = this.rateLimitMap.get(endpoint);
if (lastRequest) {
const timeSinceLastRequest = Date.now() - lastRequest;
if (timeSinceLastRequest < 1000) { // Basic rate limiting
throw new TwitterError(
'Rate limit exceeded',
'rate_limit_exceeded',
429
);
}
}
this.rateLimitMap.set(endpoint, Date.now());
}
private handleApiError(error: unknown): never {
if (error instanceof TwitterError) {
throw error;
}
// Handle twitter-api-v2 errors
const apiError = error as any;
if (apiError.code) {
throw new TwitterError(
apiError.message || 'Twitter API error',
apiError.code,
apiError.status
);
}
// Handle unexpected errors
console.error('Unexpected error in Twitter client:', error);
throw new TwitterError(
'An unexpected error occurred',
'internal_error',
500
);
}
}