Popmelt MCP Server

by avantjohn
Verified
import sqlite3 from 'sqlite3'; import { open, Database } from 'sqlite'; import dotenv from 'dotenv'; import path from 'path'; import { fileURLToPath } from 'url'; dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Define types for our database entities export interface TalentProfile { id: string; name: string; description: string; created_at: Date; updated_at: Date; title?: string; summary?: string; photo?: string; contact?: ContactInfo; skills?: string[]; experience?: Experience[]; education?: Education[]; projects?: Project[]; languages?: string[]; interests?: string[]; design_profile: DesignProfile; } export interface ContactInfo { email: string; phone: string; website: string; location: string; } export interface Experience { position: string; company: string; duration: string; description: string; } export interface Education { degree: string; institution: string; year: string; } export interface Project { name: string; description: string; url: string; } export interface DesignProfile { id: string; name: string; description: string; aesthetic_characteristics: AestheticCharacteristics; design_attributes: DesignAttributes; color_palette: ColorPalette; typography: Typography; design_system: DesignSystem; } export interface AestheticCharacteristics { style: string; mood: string; complexity: number; minimalism: number; boldness: number; playfulness: number; elegance: number; } export interface DesignAttributes { whitespace_balance: number; color_harmony: number; visual_rhythm: number; layout_density: number; texture_use: number; border_use: number; shadow_use: number; } export interface ColorPalette { primary: string; secondary: string; accent: string; background: string; text: string; } export interface Typography { heading: TypographySettings; body: TypographySettings; scale: TypographyScale; } export interface TypographySettings { font: string; weight: number; letterSpacing: string; lineHeight: number; } export interface TypographyScale { base: string; ratio: number; h1: string; h2: string; h3: string; h4: string; h5: string; small: string; } export interface DesignSystem { colors: DesignSystemColors; spacing: DesignSystemSpacing; borders: DesignSystemBorders; shadows: DesignSystemShadows; transitions: DesignSystemTransitions; components: DesignSystemComponents; } export interface DesignSystemColors { base: { core: string; neutral: string; }; scales: { tint: number; shade: number; chroma: number; }; palette: { neutral: Record<string, string>; primary: Record<string, string>; }; semantic: { background: Record<string, string>; text: Record<string, string>; brand: Record<string, string>; ui: Record<string, string>; }; } export interface DesignSystemSpacing { base: string; scale: Record<string, string>; layout: { container: { max: string; padding: Record<string, string>; }; section: { max: string; padding: Record<string, string>; }; }; } export interface DesignSystemBorders { radius: Record<string, string>; width: Record<string, string>; } export interface DesignSystemShadows { none: string; sm: string; md: string; lg: string; xl: string; } export interface DesignSystemTransitions { default: string; fast: string; slow: string; } export interface DesignSystemComponents { button: { base: Record<string, string>; sizes: Record<string, Record<string, string>>; variants: Record<string, Record<string, string>>; }; card: { base: Record<string, string>; variants: Record<string, Record<string, string>>; padding: Record<string, string>; }; input: { base: Record<string, string>; states: Record<string, Record<string, string>>; sizes: Record<string, Record<string, string>>; }; navbar: { base: Record<string, string>; variants: Record<string, Record<string, string>>; responsive: Record<string, Record<string, string>>; }; modal: { overlay: Record<string, string>; content: Record<string, string>; header: Record<string, string>; body: Record<string, string>; footer: Record<string, string>; }; table: { base: Record<string, string>; head: Record<string, string>; cell: Record<string, string>; variants: Record<string, Record<string, string>>; }; } // SQLite connection let db: Database | null = null; // Initialize database connection export async function initializeDb(): Promise<void> { if (db) return; // Already initialized const dbPath = path.join(__dirname, '..', '..', 'db', 'popmelt.sqlite'); db = await open({ filename: dbPath, driver: sqlite3.Database }); console.log(`Connected to SQLite database at ${dbPath}`); } // Helper functions for database operations export async function query<T>(text: string, params: any[] = []): Promise<T[]> { if (!db) { await initializeDb(); } // Convert PostgreSQL parameter style ($1, $2) to SQLite style (?, ?) const sqliteText = text.replace(/\$(\d+)/g, '?'); // Handle JSON fields for SQLite - only use the new format paths const sqliteQuery = sqliteText .replace(/metadata->'([^']+)'/g, "json_extract(metadata, '$.$1')") .replace(/metadata->>'([^']+)'/g, "json_extract(metadata, '$.$1')"); try { // For SELECT queries if (sqliteQuery.trim().toLowerCase().startsWith('select')) { const result = await db!.all(sqliteQuery, params); // Parse JSON metadata field directly with no transformation return result.map((row: any) => { if (row.metadata && typeof row.metadata === 'string') { const metadata = JSON.parse(row.metadata); // Use the new format directly, no transformation needed return { ...row, ...metadata } as T; } return row as T; }); } else { // For non-SELECT queries await db!.run(sqliteQuery, params); return [] as T[]; } } catch (error) { console.error('Database query error:', error); throw error; } } // Database functions for Talent profiles export async function getTalentById(id: string): Promise<TalentProfile | null> { const result = await query<TalentProfile>( 'SELECT * FROM talents WHERE id = ?', [id] ); return result.length > 0 ? result[0] : null; } export async function getAllTalents(): Promise<TalentProfile[]> { return query<TalentProfile>('SELECT * FROM talents ORDER BY name'); } export async function getTalentsByAttribute( attributeName: string, attributeValue: string | number ): Promise<TalentProfile[]> { // Handle nested design_profile attributes if (attributeName.startsWith('design_profile.')) { const nestedPath = attributeName.substring('design_profile.'.length); const jsonPath = nestedPath.split('.').join('->'); return query<TalentProfile>( `SELECT * FROM talents WHERE metadata->'design_profile'->'${jsonPath}' = ? ORDER BY name`, [attributeValue] ); } // Check if attribute is a direct field in the profile const profileFields = ['title', 'summary', 'photo', 'contact', 'skills', 'experience', 'education', 'projects', 'languages', 'interests', 'design_profile']; if (profileFields.includes(attributeName)) { return query<TalentProfile>( `SELECT * FROM talents WHERE metadata->'${attributeName}' IS NOT NULL ORDER BY name`, [] ); } // Handle non-nested fields return query<TalentProfile>( `SELECT * FROM talents WHERE ${attributeName} = ? ORDER BY name`, [attributeValue] ); } /** * Get the default talent ID from the config table * @returns The default talent ID, defaults to 'talent_001' (Olivia Gray) if not set */ export async function getDefaultTalentId(): Promise<string> { if (!db) { await initializeDb(); } try { // Check if config table exists const configTableExists = await db!.get("SELECT name FROM sqlite_master WHERE type='table' AND name='config'"); if (configTableExists) { // Get default_talent from config const config = await db!.get('SELECT value FROM config WHERE key = ?', ['default_talent']); if (config && config.value) { return config.value; } } // If no config or default talent found, return Olivia Gray's ID return 'talent_001'; } catch (error) { console.error('Error getting default talent ID:', error); // Fallback to Olivia Gray's ID return 'talent_001'; } } export async function queryTalents(queryParams: Record<string, any>): Promise<TalentProfile[]> { // Build a dynamic query based on provided parameters const conditions = []; const values = []; for (const [key, value] of Object.entries(queryParams)) { // Handle all metadata attributes including design_profile if (key.includes('.')) { const parts = key.split('.'); let jsonPath = parts.join('->'); conditions.push(`metadata->'${jsonPath}' = ?`); values.push(value); } else { // Handle regular fields conditions.push(`${key} = ?`); values.push(value); } } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; return query<TalentProfile>( `SELECT * FROM talents ${whereClause} ORDER BY name`, values ); } // Function to close the database connection when shutting down export async function closePool(): Promise<void> { if (db) { await db.close(); db = null; } } // Initialize database on module load initializeDb().catch(console.error); export default { query, getTalentById, getAllTalents, getTalentsByAttribute, queryTalents, getDefaultTalentId, closePool, initializeDb };