@basicsu/courses-mcp
Version:
Interactive programming courses from Basics - MCP server for Cursor
423 lines • 13.6 kB
JavaScript
// 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