ai-flashmob-mcp
Version:
MCP server for AI-powered flashcard generation
320 lines (294 loc) • 11.3 kB
JavaScript
#!/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);
});