UNPKG

ai-flashmob-mcp

Version:

MCP server for AI-powered flashcard generation

320 lines (294 loc) 11.3 kB
#!/usr/bin/env node /** * Flashcard Generator MCP Server * * This Model Context Protocol (MCP) server provides tools for generating * AI-powered flashcards from text or images using the AI Flashmob backend. * * Usage: * - Configure with your PUBLIC_USER_ID and SECRET_KEY * - Connect to Claude Desktop or other MCP-compatible clients * - Use flashcard generation and management tools */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { FlashcardGenerator } from './tools/flashcard-generator.js'; import { FlashcardManager } from './tools/flashcard-manager.js'; class FlashcardMcpServer { constructor() { this.server = new Server( { name: 'ai-flashmob-mcp', version: '0.0.1', }, { capabilities: { tools: {}, }, } ); this.flashcardGenerator = new FlashcardGenerator(); this.flashcardManager = new FlashcardManager(); this.setupToolHandlers(); } setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'generate_flashcards_from_text', description: 'Generate AI flashcards from text content. Creates educational flashcards and saves them to your account.', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'The text content to generate flashcards from (10-4000 characters)', minLength: 10, maxLength: 4000 }, maxCards: { type: 'integer', description: 'Maximum number of flashcards to generate (1-10)', minimum: 1, maximum: 10, default: 5 }, deckId: { type: 'integer', description: 'Optional deck ID to organize the flashcards', minimum: 1 } }, required: ['text'] } }, { name: 'generate_flashcards_from_image', description: 'Generate AI flashcards from image content. Analyzes visual content and creates educational flashcards.', inputSchema: { type: 'object', properties: { image: { type: 'string', description: 'Base64-encoded image data (with or without data URL prefix)' }, imageFormat: { type: 'string', description: 'Image format (png, jpeg, jpg, webp)', enum: ['png', 'jpeg', 'jpg', 'webp'], default: 'png' }, maxCards: { type: 'integer', description: 'Maximum number of flashcards to generate (1-10)', minimum: 1, maximum: 10, default: 5 }, deckId: { type: 'integer', description: 'Optional deck ID to organize the flashcards', minimum: 1 } }, required: ['image'] } }, { name: 'get_decks', description: 'Get all decks for the authenticated user. Shows deck details including flashcard counts.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } }, { name: 'get_deck_cards', description: 'Get all flashcards from a specific deck. Shows detailed flashcard content and study progress.', inputSchema: { type: 'object', properties: { deckId: { type: 'integer', description: 'The ID of the deck to get flashcards from', minimum: 1 } }, required: ['deckId'] } }, { name: 'create_deck', description: 'Create a new deck to organize flashcards. Returns deck ID for use with create_cards_bulk. Example: create_deck({name: "Spanish Vocab", description: "Common phrases"})', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Deck name (required, 1-100 characters)', minLength: 1, maxLength: 100 }, description: { type: 'string', description: 'Deck description (optional, max 500 characters)', maxLength: 500 }, colorTheme: { type: 'string', description: 'Color theme: blue, green, red, yellow, purple, orange (optional, defaults to blue)', enum: ['blue', 'green', 'red', 'yellow', 'purple', 'orange', 'pink', 'cyan', 'gray'] } }, required: ['name'] } }, { name: 'create_cards_bulk', description: 'Bulk create flashcards from your LLM-generated content (non-AI, no credits consumed). Workflow: (1) Generate Q&A with your LLM, (2) Create deck if needed, (3) Call this to save cards.', inputSchema: { type: 'object', properties: { flashcards: { type: 'array', description: 'Array of flashcard objects (1-3000 items)', items: { type: 'object', properties: { frontText: { type: 'string', description: 'Question/prompt (1-500 characters)', minLength: 1, maxLength: 500 }, backText: { type: 'string', description: 'Answer/content (1-10000 characters)', minLength: 1, maxLength: 10000 } }, required: ['frontText', 'backText'] }, minItems: 1, maxItems: 3000 }, deckIds: { type: 'array', description: 'Optional deck IDs to assign cards to (get IDs from create_deck or get_decks)', items: { type: 'integer', minimum: 1 } } }, required: ['flashcards'] } } ] }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'generate_flashcards_from_text': return await this.flashcardGenerator.generateFromText(args); case 'generate_flashcards_from_image': return await this.flashcardGenerator.generateFromImage(args); case 'get_decks': return await this.flashcardManager.getDecks(args); case 'get_deck_cards': return await this.flashcardManager.getDeckCards(args); case 'create_deck': return await this.flashcardManager.createDeck(args); case 'create_cards_bulk': return await this.flashcardManager.createCardsBulk(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { // If it's already an McpError, re-throw it if (error instanceof McpError) { throw error; } // Convert other errors to McpError throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error.message}` ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); // Log startup message to stderr (won't interfere with MCP protocol) console.error('🤖 AI Flashmob MCP Server started'); console.error('📚 Ready to generate and manage AI-powered flashcards!'); console.error('🔑 Make sure PUBLIC_USER_ID and SECRET_KEY environment variables are set'); console.error('🛠️ Available tools:'); console.error(' • generate_flashcards_from_text - AI generation (uses credits)'); console.error(' • generate_flashcards_from_image - AI generation (uses credits)'); console.error(' • create_deck - Create deck (no AI, no credits)'); console.error(' • create_cards_bulk - Bulk create cards (no AI, no credits)'); console.error(' • get_decks - View all decks'); console.error(' • get_deck_cards - View cards in deck'); } } // Configuration validation function validateConfig() { const requiredVars = ['PUBLIC_USER_ID', 'SECRET_KEY']; const missing = requiredVars.filter(varName => !process.env[varName]); if (missing.length > 0) { console.error('❌ Missing required environment variables:', missing.join(', ')); console.error('📝 Please set these variables before starting the MCP server:'); console.error(' export PUBLIC_USER_ID="your-public-user-id"'); console.error(' export SECRET_KEY="your-secret-key"'); console.error(' export API_BASE_URL="https://your-api.com" # Optional, defaults to https://api.ai-flashmob.com'); process.exit(1); } // Validate format if (process.env.SECRET_KEY.length !== 64) { console.error('❌ SECRET_KEY should be 64 characters long'); process.exit(1); } // Log configuration (without sensitive data) console.error('✅ Configuration validated'); console.error(`🆔 Public User ID: ${process.env.PUBLIC_USER_ID}`); console.error(`🔐 Secret Key: ${process.env.SECRET_KEY.substring(0, 8)}...`); console.error(`🌐 API Base URL: ${process.env.API_BASE_URL || 'https://api.ai-flashmob.com'}`); } // Error handling for unhandled rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); }); process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); process.exit(1); }); // Main execution async function main() { validateConfig(); const server = new FlashcardMcpServer(); await server.run(); } // Start the server main().catch((error) => { console.error('Failed to start MCP server:', error); process.exit(1); });