UNPKG

ai-flashmob-mcp

Version:

MCP server for AI-powered flashcard generation

286 lines (235 loc) • 9.09 kB
/** * Flashcard Generator Tool * * Implements MCP tools for generating flashcards from text and images * using the AI Flashmob backend API with HMAC authentication. */ import { signRequest, validateCredentials, getApiBaseUrl, logRequest } from '../utils/auth.js'; export class FlashcardGenerator { constructor() { this.apiBaseUrl = getApiBaseUrl(); this.publicUserId = process.env.PUBLIC_USER_ID; this.secretKey = process.env.SECRET_KEY; // Validate credentials on initialization validateCredentials(this.publicUserId, this.secretKey); } /** * Generates flashcards from text content * * @param {Object} args - Arguments from MCP tool call * @param {string} args.text - Text content to generate flashcards from * @param {number} [args.maxCards=5] - Maximum number of flashcards to generate * @param {number} [args.deckId] - Optional deck ID * @returns {Object} MCP tool response */ async generateFromText(args) { try { const { text, maxCards = 5, deckId } = args; // Validate input if (!text || typeof text !== 'string') { throw new Error('Text content is required and must be a string'); } if (text.length < 10 || text.length > 4000) { throw new Error('Text must be between 10 and 4000 characters'); } if (maxCards && (maxCards < 1 || maxCards > 10)) { throw new Error('maxCards must be between 1 and 10'); } if (deckId && (!Number.isInteger(deckId) || deckId < 1)) { throw new Error('deckId must be a positive integer'); } // Prepare request body const requestBody = { text, maxCards, ...(deckId && { deckId }) }; // Sign the request const headers = signRequest(this.publicUserId, this.secretKey, requestBody); // Log request for debugging logRequest('POST', `${this.apiBaseUrl}/api/v1/external/ai/generate-cards`, requestBody); // Make API request const response = await fetch(`${this.apiBaseUrl}/api/v1/external/ai/generate-cards`, { method: 'POST', headers, body: JSON.stringify(requestBody) }); // Handle response const result = await this.handleApiResponse(response); // Format success response for MCP return { content: [ { type: 'text', text: this.formatSuccessResponse(result, 'text') } ] }; } catch (error) { return this.formatErrorResponse(error); } } /** * Generates flashcards from image content * * @param {Object} args - Arguments from MCP tool call * @param {string} args.image - Base64-encoded image data * @param {string} [args.imageFormat='png'] - Image format * @param {number} [args.maxCards=5] - Maximum number of flashcards to generate * @param {number} [args.deckId] - Optional deck ID * @returns {Object} MCP tool response */ async generateFromImage(args) { try { const { image, imageFormat = 'png', maxCards = 5, deckId } = args; // Validate input if (!image || typeof image !== 'string') { throw new Error('Image data is required and must be a base64 string'); } if (image.length < 100) { throw new Error('Image data appears to be too short. Please provide valid base64 image data.'); } if (!['png', 'jpeg', 'jpg', 'webp'].includes(imageFormat)) { throw new Error('imageFormat must be one of: png, jpeg, jpg, webp'); } if (maxCards && (maxCards < 1 || maxCards > 10)) { throw new Error('maxCards must be between 1 and 10'); } if (deckId && (!Number.isInteger(deckId) || deckId < 1)) { throw new Error('deckId must be a positive integer'); } // Prepare request body const requestBody = { image, imageFormat, maxCards, ...(deckId && { deckId }) }; // Sign the request const headers = signRequest(this.publicUserId, this.secretKey, requestBody); // Log request for debugging (without full image data) logRequest('POST', `${this.apiBaseUrl}/api/v1/external/ai/generate-cards`, { ...requestBody, image: `[base64 image: ${image.length} chars]` }); // Make API request const response = await fetch(`${this.apiBaseUrl}/api/v1/external/ai/generate-cards`, { method: 'POST', headers, body: JSON.stringify(requestBody) }); // Handle response const result = await this.handleApiResponse(response); // Format success response for MCP return { content: [ { type: 'text', text: this.formatSuccessResponse(result, 'image') } ] }; } catch (error) { return this.formatErrorResponse(error); } } /** * Handles API response and extracts data or throws appropriate errors * * @param {Response} response - Fetch response object * @returns {Object} Parsed response data * @throws {Error} If response indicates an error */ async handleApiResponse(response) { let responseData; try { responseData = await response.json(); } catch (parseError) { throw new Error(`Failed to parse API response: ${parseError.message}`); } // Handle error responses if (!response.ok) { switch (response.status) { case 401: throw new Error(`Authentication failed: ${responseData.message || 'Invalid credentials'}`); case 402: throw new Error(`Insufficient credits: ${responseData.message || 'You need more AI credits to generate flashcards'}`); case 429: throw new Error(`Rate limit exceeded: ${responseData.message || 'Too many requests. Please wait before trying again.'}`); case 400: throw new Error(`Invalid request: ${responseData.message || 'Bad request parameters'}`); case 404: if (responseData.message && responseData.message.includes('Deck')) { throw new Error(`Deck not found: The specified deck does not exist or does not belong to your account`); } throw new Error(`Not found: ${responseData.message || 'Endpoint not found'}`); case 503: throw new Error(`Service unavailable: ${responseData.message || 'AI service is temporarily unavailable'}`); default: throw new Error(`API error (${response.status}): ${responseData.message || response.statusText}`); } } // Validate successful response structure if (!responseData.success) { throw new Error(`API request failed: ${responseData.message || 'Unknown error'}`); } if (!responseData.flashcards || !Array.isArray(responseData.flashcards)) { throw new Error('Invalid API response: missing or invalid flashcards data'); } return responseData; } /** * Formats a successful API response for MCP display * * @param {Object} result - API response data * @param {string} source - Source type ('text' or 'image') * @returns {string} Formatted response text */ formatSuccessResponse(result, source) { const { flashcards, creditsUsed, remainingCredits, deckId, siblingGroupId } = result; let output = `šŸŽ‰ Successfully generated ${flashcards.length} flashcard${flashcards.length === 1 ? '' : 's'} from ${source}!\n\n`; // Display each flashcard flashcards.forEach((card, index) => { output += `šŸ“š **Flashcard ${index + 1}**${card.type === 'summary' ? ' (Summary)' : ''}\n`; output += `**Q:** ${card.front}\n`; output += `**A:** ${card.back}\n\n`; }); // Display metadata output += `šŸ“Š **Generation Details:**\n`; output += `• AI Credits Used: ${creditsUsed}\n`; output += `• Remaining Credits: ${remainingCredits}\n`; if (deckId) { output += `• Deck ID: ${deckId}\n`; } if (siblingGroupId) { output += `• Group ID: ${siblingGroupId}\n`; } output += `\nāœ… All flashcards have been saved to your account and are ready for study!`; return output; } /** * Formats an error response for MCP display * * @param {Error} error - The error that occurred * @returns {Object} MCP error response */ formatErrorResponse(error) { const errorMessage = error.message || 'Unknown error occurred'; // Log error for debugging console.error(`Flashcard generation error: ${errorMessage}`); return { content: [ { type: 'text', text: `āŒ **Flashcard Generation Failed**\n\n${errorMessage}\n\nšŸ”§ **Troubleshooting Tips:**\n• Check your API credentials (PUBLIC_USER_ID and SECRET_KEY)\n• Ensure you have sufficient AI credits\n• Verify your internet connection\n• Try again with different content if the issue persists` } ], isError: true }; } }