social-media-mcp
by angheljf
- src
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ErrorCode,
McpError,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import dotenv from "dotenv";
import { SocialMediaPost, PostArgs, isValidPostArgs, SocialMediaPlatform, ListPostsArgs, isValidListPostsArgs } from "./types.js";
import { TwitterApi } from 'twitter-api-v2';
// import { v4 as uuidv4 } from 'uuid';
dotenv.config();
const TWITTER_API_KEY = process.env.TWITTER_API_KEY;
const TWITTER_API_SECRET = process.env.TWITTER_API_SECRET;
const TWITTER_ACCESS_TOKEN = process.env.TWITTER_ACCESS_TOKEN;
const TWITTER_ACCESS_SECRET = process.env.TWITTER_ACCESS_SECRET;
if (!TWITTER_API_KEY || !TWITTER_API_SECRET || !TWITTER_ACCESS_TOKEN || !TWITTER_ACCESS_SECRET) {
throw new Error("Twitter API keys are required in .env file");
}
const twitterClient = new TwitterApi({
appKey: TWITTER_API_KEY,
appSecret: TWITTER_API_SECRET,
accessToken: TWITTER_ACCESS_TOKEN,
accessSecret: TWITTER_ACCESS_SECRET
});
// Helper function to sleep for a specified time (in milliseconds)
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
class SocialMediaServer {
private server: Server;
private socialMediaPosts: SocialMediaPost[] = [];
private socialMediaPlatforms: SocialMediaPlatform[] = [
{
name: "X",
description: "Post to X (formerly Twitter)",
}
];
constructor() {
this.server = new Server(
{
name: "social-media-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
resources: {}
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
private setupHandlers(): void {
this.setupToolHandlers();
this.setupResourceHandlers();
}
private setupResourceHandlers(): void {
this.server.setRequestHandler(
ListResourcesRequestSchema,
async () => ({
resources: this.socialMediaPlatforms.map(platform => ({
uri: `socialmedia://platforms/${platform.name.toLowerCase()}`,
name: `Social Media: ${platform.name}`,
mimeType: "application/json",
description: `Information about the ${platform.name} platform`,
}))
})
);
this.server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
const platformName = request.params.uri.split('/').pop()?.toLowerCase();
const platform = this.socialMediaPlatforms.find(p => p.name.toLowerCase() === platformName);
if (!platform) {
throw new McpError(ErrorCode.InvalidRequest, `Unknown social media platform: ${request.params.uri}`);
}
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(platform, null, 2),
}]
}
}
)
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "post_to_x",
description: "Post a message to X (formerly Twitter)",
inputSchema: {
type: "object",
properties: {
content: { type: "string", description: "Content of the post" },
threadId: { type: "string", description: "ID of the thread to post to" },
},
required: ["content"],
},
},
{
name: "list_x_posts",
description: "List X (formerly Twitter) posts",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Maximum number of posts to return"
},
threadId: {
type: "string",
description: "ID of the thread to filter by"
}
}
}
},
{
name: "create_x_thread",
description: "Create a thread on X (formerly Twitter)",
inputSchema: {
type: "object",
properties: {
content: { type: "string", description: "Content of the first post" }
},
required: ["content"]
}
}
],
}));
this.server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
if (request.params.name === "post_to_x") {
if (!isValidPostArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid post arguments"
);
}
const { content, threadId } = request.params.arguments;
try {
let tweet;
if (threadId) {
const lastPostInThread = this.socialMediaPosts.filter(post => post.threadId === threadId).sort((a,b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0]
if(lastPostInThread) {
await sleep(2000)
tweet = await twitterClient.v2.reply(content, lastPostInThread.tweetId ?? "0")
}
else {
await sleep(2000)
tweet = await twitterClient.v2.tweet(content)
}
}
else {
await sleep(2000)
tweet = await twitterClient.v2.tweet(content)
}
const newPost: SocialMediaPost = {
content,
platform: "X",
timestamp: new Date().toISOString(),
threadId: threadId,
tweetId: tweet.data.id
};
this.socialMediaPosts.push(newPost);
return {
content: [
{
type: "text",
text: `Successfully posted to X: ${content}. Tweet ID: ${tweet.data.id}`,
},
],
};
}
catch(error: any) {
return {
isError: true,
content: [
{
type: "text",
text: `Failed to post to X: ${error.message}`,
},
],
};
}
}
else if (request.params.name === "list_x_posts") {
if(!isValidListPostsArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid list posts arguments"
)
}
const { limit, threadId } = request.params.arguments;
let filteredPosts = this.socialMediaPosts;
if(threadId) {
filteredPosts = filteredPosts.filter(post => post.threadId === threadId)
}
if(limit){
filteredPosts = filteredPosts.slice(0, limit);
}
return {
content: [{
type: "text",
text: JSON.stringify(filteredPosts, null, 2)
}]
}
}
else if (request.params.name === "create_x_thread") {
if (!isValidPostArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid post arguments"
);
}
const { content } = request.params.arguments;
try {
await sleep(2000)
const tweet = await twitterClient.v2.tweet(content)
const threadId = tweet.data.id;
const newPost: SocialMediaPost = {
content,
platform: "X",
timestamp: new Date().toISOString(),
threadId: threadId,
tweetId: tweet.data.id
};
this.socialMediaPosts.push(newPost);
return {
content: [
{
type: "text",
text: `Successfully created X thread: ${content}. Thread ID: ${threadId}`,
},
],
};
} catch (error: any) {
return {
isError: true,
content: [
{
type: "text",
text: `Failed to create X thread: ${error.message}`,
},
],
};
}
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
);
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Social Media MCP server running on stdio");
}
}
const server = new SocialMediaServer();
server.run().catch(console.error);