X MCP Server
by DataWhisker
- x-mcp-server
- src
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
Tool,
TextContent
} from '@modelcontextprotocol/sdk/types.js';
import { TwitterApi } from 'twitter-api-v2';
// Track rate limit reset times
const rateLimitResets: { [key: string]: number } = {
'home': 0,
'tweet': 0,
'reply': 0
};
// Helper function for rate limit handling
async function withRateLimit<T>(endpoint: 'home' | 'tweet' | 'reply', fn: () => Promise<T>): Promise<T> {
const now = Date.now();
const resetTime = rateLimitResets[endpoint];
if (now < resetTime) {
const waitTime = resetTime - now + 1000; // Add 1 second buffer
await new Promise(resolve => setTimeout(resolve, waitTime));
}
try {
const result = await fn();
// Set next reset time to 15 minutes from now for free tier
rateLimitResets[endpoint] = now + (15 * 60 * 1000);
return result;
} catch (error: any) {
if (error?.code === 429) {
// If we get a rate limit error, wait 15 minutes before next attempt
rateLimitResets[endpoint] = now + (15 * 60 * 1000);
throw new McpError(
ErrorCode.InvalidRequest,
`Rate limit exceeded for ${endpoint}. Please try again in 15 minutes.`
);
}
throw error;
}
}
// Twitter API client setup
const client = new TwitterApi({
appKey: process.env.TWITTER_API_KEY ?? '',
appSecret: process.env.TWITTER_API_SECRET ?? '',
accessToken: process.env.TWITTER_ACCESS_TOKEN ?? '',
accessSecret: process.env.TWITTER_ACCESS_SECRET ?? '',
});
class XMcpServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: 'x-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_home_timeline',
description: 'Get the most recent tweets from your home timeline',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of tweets to retrieve (max 100)',
minimum: 1,
maximum: 100,
default: 20,
},
},
},
},
{
name: 'create_tweet',
description: 'Create a new tweet',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'The text content of the tweet',
maxLength: 280,
},
},
required: ['text'],
},
},
{
name: 'reply_to_tweet',
description: 'Reply to a tweet',
inputSchema: {
type: 'object',
properties: {
tweet_id: {
type: 'string',
description: 'The ID of the tweet to reply to',
},
text: {
type: 'string',
description: 'The text content of the reply',
maxLength: 280,
},
},
required: ['tweet_id', 'text'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case 'get_home_timeline': {
const { limit = 20 } = request.params.arguments as { limit?: number };
const timeline = await withRateLimit('home', () => client.v2.homeTimeline({
max_results: Math.min(limit, 5), // Limit to max 5 tweets for free tier
'tweet.fields': ['author_id', 'created_at', 'referenced_tweets'],
expansions: ['author_id', 'referenced_tweets.id'],
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(timeline.data, null, 2),
},
],
};
}
case 'create_tweet': {
const { text } = request.params.arguments as { text: string };
const tweet = await withRateLimit('tweet', () => client.v2.tweet(text));
return {
content: [
{
type: 'text',
text: JSON.stringify(tweet.data, null, 2),
},
],
};
}
case 'reply_to_tweet': {
const { tweet_id, text } = request.params.arguments as {
tweet_id: string;
text: string;
};
const reply = await withRateLimit('reply', () => client.v2.reply(text, tweet_id));
return {
content: [
{
type: 'text',
text: JSON.stringify(reply.data, null, 2),
},
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Twitter API error: ${(error as Error).message}`
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('X MCP server running on stdio');
}
}
const server = new XMcpServer();
server.run().catch(console.error);