Twitter MCP Server
by EnesCinr
- src
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
Tool,
ErrorCode,
McpError,
TextContent
} from '@modelcontextprotocol/sdk/types.js';
import { TwitterClient } from './twitter-api.js';
import { ResponseFormatter } from './formatter.js';
import {
Config, ConfigSchema,
PostTweetSchema, SearchTweetsSchema,
TwitterError
} from './types.js';
import dotenv from 'dotenv';
export class TwitterServer {
private server: Server;
private client: TwitterClient;
constructor(config: Config) {
// Validate config
const result = ConfigSchema.safeParse(config);
if (!result.success) {
throw new Error(`Invalid configuration: ${result.error.message}`);
}
this.client = new TwitterClient(config);
this.server = new Server({
name: 'twitter-mcp',
version: '1.0.0'
}, {
capabilities: {
tools: {}
}
});
this.setupHandlers();
}
private setupHandlers(): void {
// Error handler
this.server.onerror = (error) => {
console.error('[MCP Error]:', error);
};
// Graceful shutdown
process.on('SIGINT', async () => {
console.error('Shutting down server...');
await this.server.close();
process.exit(0);
});
// Register tool handlers
this.setupToolHandlers();
}
private setupToolHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'post_tweet',
description: 'Post a new tweet to Twitter',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'The content of your tweet',
maxLength: 280
}
},
required: ['text']
}
} as Tool,
{
name: 'search_tweets',
description: 'Search for tweets on Twitter',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
count: {
type: 'number',
description: 'Number of tweets to return (10-100)',
minimum: 10,
maximum: 100
}
},
required: ['query', 'count']
}
} as Tool
]
}));
// Handle tool execution
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
console.error(`Tool called: ${name}`, args);
try {
switch (name) {
case 'post_tweet':
return await this.handlePostTweet(args);
case 'search_tweets':
return await this.handleSearchTweets(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
return this.handleError(error);
}
});
}
private async handlePostTweet(args: unknown) {
const result = PostTweetSchema.safeParse(args);
if (!result.success) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid parameters: ${result.error.message}`
);
}
const tweet = await this.client.postTweet(result.data.text);
return {
content: [{
type: 'text',
text: `Tweet posted successfully!\nURL: https://twitter.com/status/${tweet.id}`
}] as TextContent[]
};
}
private async handleSearchTweets(args: unknown) {
const result = SearchTweetsSchema.safeParse(args);
if (!result.success) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid parameters: ${result.error.message}`
);
}
const { tweets, users } = await this.client.searchTweets(
result.data.query,
result.data.count
);
const formattedResponse = ResponseFormatter.formatSearchResponse(
result.data.query,
tweets,
users
);
return {
content: [{
type: 'text',
text: ResponseFormatter.toMcpResponse(formattedResponse)
}] as TextContent[]
};
}
private handleError(error: unknown) {
if (error instanceof McpError) {
throw error;
}
if (error instanceof TwitterError) {
if (TwitterError.isRateLimit(error)) {
return {
content: [{
type: 'text',
text: 'Rate limit exceeded. Please wait a moment before trying again.',
isError: true
}] as TextContent[]
};
}
return {
content: [{
type: 'text',
text: `Twitter API error: ${(error as TwitterError).message}`,
isError: true
}] as TextContent[]
};
}
console.error('Unexpected error:', error);
throw new McpError(
ErrorCode.InternalError,
'An unexpected error occurred'
);
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Twitter MCP server running on stdio');
}
}
// Start the server
dotenv.config();
const config = {
apiKey: process.env.API_KEY!,
apiSecretKey: process.env.API_SECRET_KEY!,
accessToken: process.env.ACCESS_TOKEN!,
accessTokenSecret: process.env.ACCESS_TOKEN_SECRET!
};
const server = new TwitterServer(config);
server.start().catch(error => {
console.error('Failed to start server:', error);
process.exit(1);
});