Skip to main content
Glama

MCP Social Network

by GrahamMcBain
index.ts19.7 kB
#!/usr/bin/env node import 'dotenv/config'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { Database } from './database.js'; // Environment variables const SUPABASE_URL = process.env.SUPABASE_URL; const SUPABASE_KEY = process.env.SUPABASE_KEY; if (!SUPABASE_URL || !SUPABASE_KEY) { console.error('Error: SUPABASE_URL and SUPABASE_KEY environment variables are required'); process.exit(1); } // Initialize database const db = new Database(SUPABASE_URL, SUPABASE_KEY); // Current user context (in a real implementation, this would be handled differently) let currentUser: string | null = null; // Create server const server = new Server( { name: 'mcp-social-network', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Helper function to format posts for display function formatPost(post: any): string { const timeAgo = getTimeAgo(new Date(post.created_at)); let formatted = `@${post.username} (${timeAgo}) [ID: ${post.id.slice(0, 8)}]\n"${post.content}"`; if (post.code) { formatted += `\n\`\`\`${post.language || ''}\n${post.code}\n\`\`\``; } if (post.tags && post.tags.length > 0) { formatted += `\nTags: ${post.tags.map((tag: string) => `#${tag}`).join(' ')}`; } formatted += `\n❤️ ${post.like_count} likes | 💬 ${post.reply_count} replies`; return formatted; } // Helper function to get time ago function getTimeAgo(date: Date): string { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays > 0) { return `${diffDays}d ago`; } else if (diffHours > 0) { return `${diffHours}h ago`; } else { const diffMins = Math.floor(diffMs / (1000 * 60)); return `${Math.max(1, diffMins)}m ago`; } } // Helper function to ensure user is logged in function requireAuth(): string { if (!currentUser) { throw new Error('Please create a profile first using create_profile(username, bio)'); } return currentUser; } // Define tools const tools: Tool[] = [ { name: 'create_profile', description: 'Create a new user profile', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username (3-20 characters, must be unique)', }, bio: { type: 'string', description: 'Optional bio (max 500 characters)', }, }, required: ['username'], }, }, { name: 'get_profile', description: 'Get a user profile by username', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to look up', }, }, required: ['username'], }, }, { name: 'update_profile', description: 'Update your profile bio', inputSchema: { type: 'object', properties: { bio: { type: 'string', description: 'New bio (max 500 characters)', }, }, required: ['bio'], }, }, { name: 'search_users', description: 'Search for users by username', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', }, limit: { type: 'number', description: 'Maximum number of results (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'post_update', description: 'Post a text update', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Post content (max 280 characters)', }, tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags (without # symbol)', }, }, required: ['content'], }, }, { name: 'post_code', description: 'Post a code snippet with description', inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'Code snippet', }, language: { type: 'string', description: 'Programming language', }, description: { type: 'string', description: 'Description of the code (max 280 characters)', }, tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags (without # symbol)', }, }, required: ['code', 'language', 'description'], }, }, { name: 'get_feed', description: 'Get your personalized feed (posts from users you follow)', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of posts (default: 20)', default: 20, }, }, }, }, { name: 'get_global_feed', description: 'Get the global feed (all public posts)', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of posts (default: 20)', default: 20, }, }, }, }, { name: 'get_user_posts', description: 'Get posts from a specific user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to get posts from', }, limit: { type: 'number', description: 'Maximum number of posts (default: 20)', default: 20, }, }, required: ['username'], }, }, { name: 'follow_user', description: 'Follow a user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to follow', }, }, required: ['username'], }, }, { name: 'unfollow_user', description: 'Unfollow a user', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to unfollow', }, }, required: ['username'], }, }, { name: 'get_following', description: 'Get list of users you are following', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_followers', description: 'Get list of your followers', inputSchema: { type: 'object', properties: {}, }, }, { name: 'like_post', description: 'Like a post by post ID', inputSchema: { type: 'object', properties: { post_id: { type: 'string', description: 'ID of the post to like', }, }, required: ['post_id'], }, }, { name: 'unlike_post', description: 'Unlike a post by post ID', inputSchema: { type: 'object', properties: { post_id: { type: 'string', description: 'ID of the post to unlike', }, }, required: ['post_id'], }, }, ]; // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // Tool call handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'create_profile': { const { username, bio } = args as { username: string; bio?: string }; if (username.length < 3 || username.length > 20) { throw new Error('Username must be between 3 and 20 characters'); } const user = await db.createUser(username, bio); currentUser = username; return { content: [ { type: 'text', text: `✅ Profile created successfully!\n\nUsername: @${user.username}\nBio: ${user.bio || 'No bio yet'}\nJoined: ${new Date(user.created_at).toLocaleDateString()}\n\nYou can now start posting and following other users!`, }, ], }; } case 'get_profile': { const { username } = args as { username: string }; const user = await db.getUser(username); if (!user) { throw new Error(`User @${username} not found`); } return { content: [ { type: 'text', text: `👤 Profile: @${user.username}\n\nBio: ${user.bio || 'No bio'}\nPosts: ${user.post_count}\nFollowers: ${user.follower_count}\nFollowing: ${user.following_count}\nJoined: ${new Date(user.created_at).toLocaleDateString()}`, }, ], }; } case 'update_profile': { const { bio } = args as { bio: string }; const username = requireAuth(); if (bio.length > 500) { throw new Error('Bio must be 500 characters or less'); } const user = await db.updateUser(username, bio); return { content: [ { type: 'text', text: `✅ Profile updated!\n\nNew bio: ${user.bio}`, }, ], }; } case 'search_users': { const { query, limit = 10 } = args as { query: string; limit?: number }; const users = await db.searchUsers(query, limit); if (users.length === 0) { return { content: [ { type: 'text', text: `No users found matching "${query}"`, }, ], }; } const userList = users .map(user => `@${user.username} - ${user.bio || 'No bio'} (${user.follower_count} followers)`) .join('\n'); return { content: [ { type: 'text', text: `🔍 Found ${users.length} user(s) matching "${query}":\n\n${userList}`, }, ], }; } case 'post_update': { const { content, tags } = args as { content: string; tags?: string[] }; const username = requireAuth(); if (content.length > 280) { throw new Error('Post content must be 280 characters or less'); } const userId = await db.getUserId(username); if (!userId) throw new Error('User not found'); const post = await db.createPost(userId, content, undefined, undefined, tags); return { content: [ { type: 'text', text: `✅ Posted successfully!\n\n${formatPost(post)}`, }, ], }; } case 'post_code': { const { code, language, description, tags } = args as { code: string; language: string; description: string; tags?: string[] }; const username = requireAuth(); if (description.length > 280) { throw new Error('Description must be 280 characters or less'); } const userId = await db.getUserId(username); if (!userId) throw new Error('User not found'); const post = await db.createPost(userId, description, code, language, tags); return { content: [ { type: 'text', text: `✅ Code snippet posted successfully!\n\n${formatPost(post)}`, }, ], }; } case 'get_feed': { const { limit = 20 } = args as { limit?: number }; const username = requireAuth(); const userId = await db.getUserId(username); if (!userId) throw new Error('User not found'); const posts = await db.getUserFeed(userId, limit); if (posts.length === 0) { return { content: [ { type: 'text', text: `📱 Your feed is empty!\n\nTry following some users to see their posts here. Use search_users() to find people to follow.`, }, ], }; } const feedText = posts.map(formatPost).join('\n\n' + '─'.repeat(50) + '\n\n'); return { content: [ { type: 'text', text: `📱 Your Feed (${posts.length} posts)\n\n${feedText}`, }, ], }; } case 'get_global_feed': { const { limit = 20 } = args as { limit?: number }; const posts = await db.getGlobalFeed(limit); if (posts.length === 0) { return { content: [ { type: 'text', text: `🌍 Global feed is empty!\n\nBe the first to post something!`, }, ], }; } const feedText = posts.map(formatPost).join('\n\n' + '─'.repeat(50) + '\n\n'); return { content: [ { type: 'text', text: `🌍 Global Feed (${posts.length} posts)\n\n${feedText}`, }, ], }; } case 'get_user_posts': { const { username, limit = 20 } = args as { username: string; limit?: number }; const userId = await db.getUserId(username); if (!userId) { throw new Error(`User @${username} not found`); } const posts = await db.getUserPosts(userId, limit); if (posts.length === 0) { return { content: [ { type: 'text', text: `@${username} hasn't posted anything yet.`, }, ], }; } const feedText = posts.map(formatPost).join('\n\n' + '─'.repeat(50) + '\n\n'); return { content: [ { type: 'text', text: `📝 Posts by @${username} (${posts.length} posts)\n\n${feedText}`, }, ], }; } case 'follow_user': { const { username } = args as { username: string }; const currentUsername = requireAuth(); const userId = await db.getUserId(username); const currentUserId = await db.getUserId(currentUsername); if (!userId) { throw new Error(`User @${username} not found`); } if (!currentUserId) { throw new Error('Current user not found'); } await db.followUser(currentUserId, userId); return { content: [ { type: 'text', text: `✅ You are now following @${username}!`, }, ], }; } case 'unfollow_user': { const { username } = args as { username: string }; const currentUsername = requireAuth(); const userId = await db.getUserId(username); const currentUserId = await db.getUserId(currentUsername); if (!userId) { throw new Error(`User @${username} not found`); } if (!currentUserId) { throw new Error('Current user not found'); } await db.unfollowUser(currentUserId, userId); return { content: [ { type: 'text', text: `✅ You have unfollowed @${username}`, }, ], }; } case 'get_following': { const username = requireAuth(); const userId = await db.getUserId(username); if (!userId) throw new Error('User not found'); const following = await db.getFollowing(userId); if (following.length === 0) { return { content: [ { type: 'text', text: `You're not following anyone yet. Use search_users() to find people to follow!`, }, ], }; } const followingList = following .map(user => `@${user.username} - ${user.bio || 'No bio'} (${user.follower_count} followers)`) .join('\n'); return { content: [ { type: 'text', text: `👥 You are following ${following.length} user(s):\n\n${followingList}`, }, ], }; } case 'get_followers': { const username = requireAuth(); const userId = await db.getUserId(username); if (!userId) throw new Error('User not found'); const followers = await db.getFollowers(userId); if (followers.length === 0) { return { content: [ { type: 'text', text: `You don't have any followers yet. Keep posting great content!`, }, ], }; } const followersList = followers .map(user => `@${user.username} - ${user.bio || 'No bio'} (${user.post_count} posts)`) .join('\n'); return { content: [ { type: 'text', text: `👥 You have ${followers.length} follower(s):\n\n${followersList}`, }, ], }; } case 'like_post': { const { post_id } = args as { post_id: string }; const username = requireAuth(); const userId = await db.getUserId(username); if (!userId) throw new Error('User not found'); const post = await db.getPost(post_id); if (!post) { throw new Error('Post not found'); } await db.likePost(userId, post_id); return { content: [ { type: 'text', text: `❤️ You liked @${post.username}'s post!`, }, ], }; } case 'unlike_post': { const { post_id } = args as { post_id: string }; const username = requireAuth(); const userId = await db.getUserId(username); if (!userId) throw new Error('User not found'); const post = await db.getPost(post_id); if (!post) { throw new Error('Post not found'); } await db.unlikePost(userId, post_id); return { content: [ { type: 'text', text: `💔 You unliked @${post.username}'s post`, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `❌ Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`, }, ], isError: true, }; } }); // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Social Network server running on stdio'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/GrahamMcBain/MCP-Social'

If you have feedback or need assistance with the MCP directory API, please join our Discord server