UNPKG

@basicsu/courses-mcp

Version:

Interactive programming courses from Basics - MCP server for Cursor

423 lines 13.6 kB
// Real database helper functions for MCP server using Supabase import { createClient } from '@supabase/supabase-js'; // Built-in configuration - no user setup required! // These are publishable keys that are safe to share (protected by Row Level Security) const SUPABASE_URL = 'https://mqvlccclucfewgtmhyiv.supabase.co'; const SUPABASE_ANON_KEY = 'sb_publishable_MiRSlSxgvZ_D_VeB3-73Mw_gtReq_yT'; // The publishable key is safe to use and expose // Row Level Security (RLS) ensures users only access appropriate data // Create a new Supabase client for each request to avoid caching function getSupabaseClient() { return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { auth: { persistSession: false, autoRefreshToken: false, }, db: { schema: 'public' }, global: { fetch: (...args) => fetch(...args), headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' } } }); } export async function getUserProfile(email) { try { const supabase = getSupabaseClient(); const { data, error } = await supabase .from('user_profiles') .select('id, email, full_name, username, user_id, development_experience, media_influence, analogy_preferences') .eq('email', email) .single(); if (error) { return null; } return data; } catch (error) { return null; } } export async function getUserPreferences(userProfileId) { try { const supabase = getSupabaseClient(); const { data, error } = await supabase .from('user_profiles') .select('development_experience, media_influence, analogy_preferences') .eq('id', userProfileId) .single(); if (error) { return null; } return { developmentExperience: data.development_experience, mediaInfluence: data.media_influence, analogyPreferences: data.analogy_preferences }; } catch (error) { return null; } } export async function getUserEnrollments(email) { try { // First get user profile to get user_id const userProfile = await getUserProfile(email); if (!userProfile) { return []; } const supabase = getSupabaseClient(); const { data, error } = await supabase .from('course_enrollments') .select(` id, course_id, user_id, progress, started_at, completed_at, last_accessed_at, courses!inner ( title, description, difficulty_level, slug, tags, primary_category, estimated_duration ) `) .eq('user_id', userProfile.id) .order('last_accessed_at', { ascending: false }); if (error) { return []; } // Flatten the response const enrollments = data?.map((enrollment) => ({ ...enrollment, title: enrollment.courses.title, description: enrollment.courses.description, difficulty_level: enrollment.courses.difficulty_level, slug: enrollment.courses.slug, tags: enrollment.courses.tags, primary_category: enrollment.courses.primary_category, estimated_duration: enrollment.courses.estimated_duration, })) || []; return enrollments; } catch (error) { return []; } } export async function getPublishedCourses() { try { const supabase = getSupabaseClient(); const { data, error } = await supabase .from('courses') .select('*') .eq('status', 'published') .order('created_at', { ascending: false }); if (error) { return []; } return data || []; } catch (error) { return []; } } export async function getCourseByIdentifier(identifier) { try { const supabase = getSupabaseClient(); // Try to find by ID first let { data, error } = await supabase .from('courses') .select('*') .eq('id', identifier) .single(); // If not found by ID, try by slug if (!data) { const slugResult = await supabase .from('courses') .select('*') .eq('slug', identifier) .single(); data = slugResult.data; error = slugResult.error; } // If still not found, try by title (case insensitive, partial match) if (!data) { const titleResult = await supabase .from('courses') .select('*') .ilike('title', `%${identifier}%`) .single(); data = titleResult.data; error = titleResult.error; } if (error) { return null; } return data; } catch (error) { return null; } } export async function getCourseModules(courseId) { try { const supabase = getSupabaseClient(); const { data, error } = await supabase .from('course_modules') .select('*') .eq('course_id', courseId) .order('order_index'); if (error) { return []; } return data || []; } catch (error) { return []; } } export async function getCourseLessons(moduleId) { try { const supabase = getSupabaseClient(); const { data, error } = await supabase .from('course_lessons') .select('*') .eq('module_id', moduleId) .order('order_index'); if (error) { return []; } return data || []; } catch (error) { return []; } } export async function getCourseFirstLesson(courseId) { try { // First get the first module const modules = await getCourseModules(courseId); if (!modules || modules.length === 0) { return null; } // Then get the first lesson from the first module const firstModule = modules[0]; const lessons = await getCourseLessons(firstModule.id); if (!lessons || lessons.length === 0) { return null; } return lessons[0]; } catch (error) { return null; } } export async function enrollUserInCourse(userId, courseId) { try { const supabase = getSupabaseClient(); const { data, error } = await supabase .from('course_enrollments') .insert({ user_id: userId, course_id: courseId, progress: 0.0, started_at: new Date().toISOString(), last_accessed_at: new Date().toISOString() }); if (error) { return false; } return true; } catch (error) { return false; } } export async function updateUserProgress(userId, courseId, progress) { try { const supabase = getSupabaseClient(); const { error } = await supabase .from('course_enrollments') .update({ progress, last_accessed_at: new Date().toISOString(), completed_at: progress >= 1.0 ? new Date().toISOString() : null }) .eq('user_id', userId) .eq('course_id', courseId); if (error) { return false; } return true; } catch (error) { return false; } } export async function verifyAuthCode(email, authCode) { try { const supabase = getSupabaseClient(); // Check if the auth code matches what's stored in the user profile const { data, error } = await supabase .from('user_profiles') .select('id, email, auth_code') .eq('email', email) .single(); if (error || !data) { return { success: false, message: 'User not found.' }; } // Verify the auth code if (data.auth_code === authCode) { return { success: true, message: 'Authentication successful' }; } return { success: false, message: 'Invalid auth code.' }; } catch (error) { return { success: false, message: 'Authentication error. Please try again.' }; } } // New functions for handling lesson steps export async function getLessonSteps(lessonId) { try { const supabase = getSupabaseClient(); // Debug logging removed - was breaking MCP JSON protocol const { data, error } = await supabase .from('lesson_steps') .select('*') .eq('lesson_id', lessonId) .order('order_index', { ascending: true }); if (error) { return []; } return data || []; } catch (error) { return []; } } export async function getStepById(stepId) { try { const supabase = getSupabaseClient(); const { data, error } = await supabase .from('lesson_steps') .select('*') .eq('id', stepId) .single(); if (error) { return null; } return data; } catch (error) { return null; } } export async function markStepComplete(userProfileId, stepId) { const opStart = Date.now(); console.error(`[DB OPERATION] markStepComplete started at ${new Date().toISOString()}`); console.error(`[DB SECURITY] Profile ID: ${userProfileId.substring(0, 8)}***`); console.error(`[DB SECURITY] Step ID: ${stepId.substring(0, 8)}***`); try { const supabase = getSupabaseClient(); // Now using profile ID directly since we fixed the foreign key const { data, error } = await supabase .from('user_step_progress') .upsert({ user_id: userProfileId, // Use profile ID directly step_id: stepId, completed_at: new Date().toISOString() }, { onConflict: 'user_id,step_id' }) .select(); if (error) { // Error marking step complete console.error('[Progress DB] Error marking step complete:', error); return false; } // Step marked complete successfully console.error('[Progress DB] ✅ Step marked complete in database:', stepId); console.error('[Progress DB] Triggers will update enrollment progress automatically'); return true; } catch (error) { // Exception in markStepComplete console.error('[Progress DB] Exception in markStepComplete:', error); return false; } } export async function getUserStepProgress(userProfileId, courseId) { try { const supabase = getSupabaseClient(); // Now using profile ID directly // Get module IDs for the course const { data: modules } = await supabase .from('course_modules') .select('id') .eq('course_id', courseId); const moduleIds = modules?.map(m => m.id) || []; // Get lesson IDs for the modules const { data: lessons } = await supabase .from('course_lessons') .select('id') .in('module_id', moduleIds); const lessonIds = lessons?.map(l => l.id) || []; // Get total steps for the course const { data: totalStepsData, error: totalError } = await supabase .from('lesson_steps') .select('id', { count: 'exact' }) .in('lesson_id', lessonIds); const totalSteps = totalError ? 0 : (totalStepsData?.length || 0); // Get step IDs const stepIds = totalStepsData?.map(s => s.id) || []; // Get completed steps for the user (using profile ID directly) const { data: completedStepsData, error: completedError } = await supabase .from('user_step_progress') .select('step_id', { count: 'exact' }) .eq('user_id', userProfileId) // Use profile ID directly .in('step_id', stepIds); const completedSteps = completedError ? 0 : (completedStepsData?.length || 0); const progress = totalSteps > 0 ? completedSteps / totalSteps : 0; return { totalSteps, completedSteps, progress }; } catch (error) { return { totalSteps: 0, completedSteps: 0, progress: 0 }; } } export async function getNextStep(userId, lessonId, currentStepId) { try { const steps = await getLessonSteps(lessonId); if (!steps.length) { return null; } // If no current step, return the first step if (!currentStepId) { return steps[0]; } // Find the current step index const currentIndex = steps.findIndex(s => s.id === currentStepId); // Return next step if available if (currentIndex >= 0 && currentIndex < steps.length - 1) { return steps[currentIndex + 1]; } return null; } catch (error) { return null; } } //# sourceMappingURL=database-helper.js.map