@gonzih/exam-prep-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ export interface SetExamInput {
2
+ profileId: string;
3
+ examType: string;
4
+ examDate: string;
5
+ currentLevel: 'beginner' | 'intermediate' | 'advanced';
6
+ }
7
+ export interface GetDailySessionInput {
8
+ profileId: string;
9
+ }
10
+ export interface AnswerQuestionInput {
11
+ profileId: string;
12
+ cardId: string;
13
+ answer: string;
14
+ rating?: 1 | 2 | 3 | 4;
15
+ }
16
+ export interface StartMockExamInput {
17
+ profileId: string;
18
+ }
19
+ export interface SubmitMockExamInput {
20
+ sessionId: string;
21
+ answers: Array<{
22
+ cardId: string;
23
+ answer: string;
24
+ }>;
25
+ }
26
+ export interface GetReadinessInput {
27
+ profileId: string;
28
+ }
29
+ export interface GetWeakAreasInput {
30
+ profileId: string;
31
+ }
32
+ export declare function setExam(input: SetExamInput): Promise<{
33
+ success: boolean;
34
+ message: string;
35
+ seededCards: number;
36
+ daysRemaining: number;
37
+ topicsIdentified: string[];
38
+ studyPlan: {
39
+ examType: string;
40
+ examDate: string;
41
+ daysRemaining: number;
42
+ weeklyGoals: string[];
43
+ first7Days: import("./study-plan.js").DayPlan[];
44
+ };
45
+ }>;
46
+ export declare function getDailySession(input: GetDailySessionInput): Promise<{
47
+ questions: {
48
+ cardId: string;
49
+ subject: string;
50
+ topic: string;
51
+ question: string;
52
+ type: "multiple_choice" | "short_answer" | "free_response" | "problem_solving";
53
+ choices: string[] | undefined;
54
+ difficulty: string;
55
+ reviewCount: number;
56
+ }[];
57
+ estimatedMinutes: number;
58
+ focusAreas: string[];
59
+ totalDue: number;
60
+ examType: string;
61
+ examDate: string;
62
+ }>;
63
+ export declare function answerQuestion(input: AnswerQuestionInput): Promise<{
64
+ correct: boolean;
65
+ correctAnswer: string;
66
+ explanation: string;
67
+ nextReview: string;
68
+ intervalDays: number;
69
+ rating: 1 | 2 | 3 | 4;
70
+ weakAreaAlert: string | undefined;
71
+ topic: string;
72
+ subject: string;
73
+ }>;
74
+ export declare function startMockExam(input: StartMockExamInput): Promise<{
75
+ sessionId: string;
76
+ questions: {
77
+ cardId: string;
78
+ subject: string;
79
+ topic: string;
80
+ question: string;
81
+ type: "multiple_choice" | "short_answer" | "free_response" | "problem_solving";
82
+ choices: string[] | undefined;
83
+ }[];
84
+ timeLimitMins: number;
85
+ totalQuestions: number;
86
+ examType: string;
87
+ instructions: string;
88
+ }>;
89
+ export declare function submitMockExam(input: SubmitMockExamInput): Promise<{
90
+ score: number;
91
+ correct: number;
92
+ total: number;
93
+ passingThreshold: number;
94
+ passed: boolean;
95
+ estimatedGrade: string;
96
+ readinessScore: number | undefined;
97
+ topicBreakdown: {
98
+ topic: string;
99
+ correct: number;
100
+ total: number;
101
+ accuracy: number;
102
+ }[];
103
+ strongestTopics: string[];
104
+ weakestTopics: string[];
105
+ recommendation: string | undefined;
106
+ }>;
107
+ export declare function getReadiness(input: GetReadinessInput): Promise<{
108
+ score: number;
109
+ strongTopics: import("./readiness.js").TopicScore[];
110
+ weakTopics: import("./readiness.js").TopicScore[];
111
+ coveragePercent: number;
112
+ trend: "improving" | "declining" | "stable";
113
+ recommendation: string;
114
+ estimatedGrade: string;
115
+ profileId: string;
116
+ examType: string;
117
+ examDate: string;
118
+ }>;
119
+ export interface WeakArea {
120
+ topic: string;
121
+ subject: string;
122
+ accuracy: number;
123
+ attempts: number;
124
+ correctAttempts: number;
125
+ }
126
+ export declare function getWeakAreas(input: GetWeakAreasInput): Promise<{
127
+ profileId: string;
128
+ examType: string | undefined;
129
+ topics: WeakArea[];
130
+ suggestedFocus: string;
131
+ totalWeakTopics: number;
132
+ }>;
package/dist/tools.js ADDED
@@ -0,0 +1,424 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { insertCard, insertSession, updateSession, getSession, getCard, updateCard, getDueCards, getNewCards, getAllCardsForExam, getWeakTopics, upsertExamConfig, getExamConfig, upsertTopicStats, getTopicStats, countCardsByQuestion, } from './db.js';
3
+ import { scheduleReview } from './fsrs.js';
4
+ import { QUESTION_BANK } from './questions.js';
5
+ import { generateStudyPlan, MOCK_EXAM_QUESTIONS, EXAM_TIME_LIMITS } from './study-plan.js';
6
+ import { calculateReadiness } from './readiness.js';
7
+ // ─── Helper Functions ─────────────────────────────────────────────────────────
8
+ function questionTemplateToCard(template, profileId) {
9
+ return {
10
+ id: uuidv4(),
11
+ profile_id: profileId,
12
+ subject: template.subject,
13
+ topic: template.topic,
14
+ question: formatQuestion(template),
15
+ answer: template.answer,
16
+ difficulty: template.difficulty === 'easy' ? 0.2 : template.difficulty === 'medium' ? 0.4 : 0.7,
17
+ stability: 1.0,
18
+ retrievability: 1.0,
19
+ next_review: new Date().toISOString(),
20
+ review_count: 0,
21
+ last_rating: null,
22
+ created_at: new Date().toISOString(),
23
+ };
24
+ }
25
+ function formatQuestion(template) {
26
+ if (template.type === 'multiple_choice' && template.choices) {
27
+ return `${template.question}\n\n${template.choices.join('\n')}`;
28
+ }
29
+ return template.question;
30
+ }
31
+ function evaluateAnswer(card, studentAnswer, template) {
32
+ const correctAnswer = card.answer.trim().toLowerCase();
33
+ const studentAnswerNorm = studentAnswer.trim().toLowerCase();
34
+ // Multiple choice — exact match (single letter)
35
+ if (template?.type === 'multiple_choice') {
36
+ const correct = correctAnswer === studentAnswerNorm ||
37
+ correctAnswer.startsWith(studentAnswerNorm) ||
38
+ studentAnswerNorm === correctAnswer[0];
39
+ return {
40
+ correct,
41
+ explanation: template.explanation
42
+ };
43
+ }
44
+ // Problem solving — check numeric answer
45
+ if (template?.type === 'problem_solving') {
46
+ // Extract numbers from both answers
47
+ const correctNum = extractNumber(correctAnswer);
48
+ const studentNum = extractNumber(studentAnswerNorm);
49
+ if (correctNum !== null && studentNum !== null) {
50
+ const tolerance = Math.abs(correctNum) * 0.05 || 0.01;
51
+ const correct = Math.abs(correctNum - studentNum) <= tolerance;
52
+ return { correct, explanation: template?.explanation || '' };
53
+ }
54
+ // Fallback to keyword matching
55
+ return keywordMatch(correctAnswer, studentAnswerNorm, template?.explanation || '');
56
+ }
57
+ // Short answer / free response — keyword matching
58
+ return keywordMatch(correctAnswer, studentAnswerNorm, template?.explanation || '');
59
+ }
60
+ function extractNumber(text) {
61
+ const match = text.match(/-?\d+\.?\d*/);
62
+ if (match)
63
+ return parseFloat(match[0]);
64
+ return null;
65
+ }
66
+ function keywordMatch(correctAnswer, studentAnswer, explanation) {
67
+ // Check if student answer contains key terms from correct answer
68
+ const keyTerms = correctAnswer
69
+ .split(/[\s,;.]+/)
70
+ .filter(w => w.length > 3)
71
+ .map(w => w.toLowerCase());
72
+ if (keyTerms.length === 0) {
73
+ const correct = correctAnswer === studentAnswer;
74
+ return { correct, explanation };
75
+ }
76
+ let matched = 0;
77
+ for (const term of keyTerms) {
78
+ if (studentAnswer.includes(term))
79
+ matched++;
80
+ }
81
+ const matchRatio = matched / keyTerms.length;
82
+ const correct = matchRatio >= 0.5;
83
+ return { correct, explanation };
84
+ }
85
+ function findTemplate(question) {
86
+ // Strip choices if present
87
+ const questionText = question.split('\n\n')[0];
88
+ return QUESTION_BANK.find(t => t.question === questionText);
89
+ }
90
+ // ─── Tool Implementations ─────────────────────────────────────────────────────
91
+ export async function setExam(input) {
92
+ const { profileId, examType, examDate, currentLevel } = input;
93
+ // Store exam config
94
+ upsertExamConfig({
95
+ profile_id: profileId,
96
+ exam_type: examType,
97
+ exam_date: examDate,
98
+ current_level: currentLevel,
99
+ created_at: new Date().toISOString(),
100
+ });
101
+ // Seed cards from question bank
102
+ const questions = QUESTION_BANK.filter(q => q.subject === examType);
103
+ let seeded = 0;
104
+ for (const template of questions) {
105
+ const formattedQuestion = formatQuestion(template);
106
+ const existing = countCardsByQuestion(profileId, formattedQuestion);
107
+ if (existing === 0) {
108
+ const card = questionTemplateToCard(template, profileId);
109
+ card.question = formattedQuestion;
110
+ insertCard(card);
111
+ seeded++;
112
+ }
113
+ }
114
+ // Generate study plan
115
+ const studyPlan = generateStudyPlan(examType, examDate, currentLevel);
116
+ const now = new Date();
117
+ const examDateObj = new Date(examDate);
118
+ const daysRemaining = Math.ceil((examDateObj.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
119
+ return {
120
+ success: true,
121
+ message: `Exam configured: ${examType} on ${examDate}`,
122
+ seededCards: seeded,
123
+ daysRemaining,
124
+ topicsIdentified: studyPlan.topicsIdentified,
125
+ studyPlan: {
126
+ examType: studyPlan.examType,
127
+ examDate: studyPlan.examDate,
128
+ daysRemaining: studyPlan.daysRemaining,
129
+ weeklyGoals: studyPlan.weeklyGoals,
130
+ first7Days: studyPlan.dailySchedule.slice(0, 7),
131
+ }
132
+ };
133
+ }
134
+ export async function getDailySession(input) {
135
+ const { profileId } = input;
136
+ const config = getExamConfig(profileId);
137
+ if (!config) {
138
+ throw new Error('No exam configured. Use set_exam first.');
139
+ }
140
+ const now = new Date().toISOString();
141
+ const TARGET_QUESTIONS = 12;
142
+ // Get due cards
143
+ let dueCards = getDueCards(profileId, now, TARGET_QUESTIONS);
144
+ // Top up with new cards if needed
145
+ if (dueCards.length < 5) {
146
+ const newCards = getNewCards(profileId, TARGET_QUESTIONS - dueCards.length);
147
+ const existingIds = new Set(dueCards.map(c => c.id));
148
+ const uniqueNew = newCards.filter(c => !existingIds.has(c.id));
149
+ dueCards = [...dueCards, ...uniqueNew].slice(0, TARGET_QUESTIONS);
150
+ }
151
+ // Identify focus areas
152
+ const topicCounts = {};
153
+ for (const card of dueCards) {
154
+ topicCounts[card.topic] = (topicCounts[card.topic] || 0) + 1;
155
+ }
156
+ const focusAreas = Object.entries(topicCounts)
157
+ .sort((a, b) => b[1] - a[1])
158
+ .slice(0, 3)
159
+ .map(([topic]) => topic);
160
+ const questions = dueCards.map(card => ({
161
+ cardId: card.id,
162
+ subject: card.subject,
163
+ topic: card.topic,
164
+ question: card.question,
165
+ type: findTemplate(card.question)?.type || 'short_answer',
166
+ choices: findTemplate(card.question)?.choices,
167
+ difficulty: card.difficulty < 0.35 ? 'easy' : card.difficulty < 0.55 ? 'medium' : 'hard',
168
+ reviewCount: card.review_count,
169
+ }));
170
+ return {
171
+ questions,
172
+ estimatedMinutes: Math.ceil(dueCards.length * 2.5),
173
+ focusAreas,
174
+ totalDue: dueCards.length,
175
+ examType: config.exam_type,
176
+ examDate: config.exam_date,
177
+ };
178
+ }
179
+ export async function answerQuestion(input) {
180
+ const { profileId, cardId, answer, rating } = input;
181
+ const card = getCard(cardId);
182
+ if (!card) {
183
+ throw new Error(`Card ${cardId} not found`);
184
+ }
185
+ if (card.profile_id !== profileId) {
186
+ throw new Error('Card does not belong to this profile');
187
+ }
188
+ const template = findTemplate(card.question);
189
+ const { correct, explanation } = evaluateAnswer(card, answer, template);
190
+ // Determine FSRS rating
191
+ let fsrsRating = rating || (correct ? 3 : 1);
192
+ if (!rating) {
193
+ fsrsRating = correct ? 3 : 1;
194
+ }
195
+ // Apply FSRS scheduling
196
+ const lastReview = card.next_review ? new Date(card.next_review) : undefined;
197
+ const fsrsResult = scheduleReview(fsrsRating, card.stability, card.difficulty, card.review_count, lastReview);
198
+ // Update card
199
+ updateCard({
200
+ id: card.id,
201
+ difficulty: fsrsResult.difficulty,
202
+ stability: fsrsResult.stability,
203
+ retrievability: fsrsResult.retrievability,
204
+ next_review: fsrsResult.nextReview.toISOString(),
205
+ review_count: card.review_count + 1,
206
+ last_rating: fsrsRating,
207
+ });
208
+ // Update topic stats
209
+ upsertTopicStats({
210
+ profile_id: profileId,
211
+ topic: card.topic,
212
+ subject: card.subject,
213
+ total_attempts: 1,
214
+ correct_attempts: correct ? 1 : 0,
215
+ last_attempt: new Date().toISOString(),
216
+ });
217
+ // Check for weak area
218
+ let weakAreaAlert;
219
+ const topicStats = getTopicStats(profileId).find(s => s.topic === card.topic);
220
+ if (topicStats && topicStats.total_attempts >= 5) {
221
+ const accuracy = topicStats.correct_attempts / topicStats.total_attempts;
222
+ if (accuracy < 0.6) {
223
+ weakAreaAlert = `Weak area detected: ${card.topic} (${Math.round(accuracy * 100)}% accuracy over ${topicStats.total_attempts} attempts). Consider extra focus on this topic.`;
224
+ }
225
+ }
226
+ return {
227
+ correct,
228
+ correctAnswer: card.answer,
229
+ explanation,
230
+ nextReview: fsrsResult.nextReview.toISOString(),
231
+ intervalDays: fsrsResult.intervalDays,
232
+ rating: fsrsRating,
233
+ weakAreaAlert,
234
+ topic: card.topic,
235
+ subject: card.subject,
236
+ };
237
+ }
238
+ export async function startMockExam(input) {
239
+ const { profileId } = input;
240
+ const config = getExamConfig(profileId);
241
+ if (!config) {
242
+ throw new Error('No exam configured. Use set_exam first.');
243
+ }
244
+ const examType = config.exam_type;
245
+ const questionCount = MOCK_EXAM_QUESTIONS[examType] || 60;
246
+ const timeLimitMins = EXAM_TIME_LIMITS[examType] || 120;
247
+ // Select balanced questions across topics
248
+ const cards = getAllCardsForExam(profileId, examType, questionCount * 2);
249
+ const shuffled = [...cards].sort(() => Math.random() - 0.5);
250
+ const selected = shuffled.slice(0, questionCount);
251
+ // Create session
252
+ const sessionId = uuidv4();
253
+ insertSession({
254
+ id: sessionId,
255
+ profile_id: profileId,
256
+ exam_type: examType,
257
+ started_at: new Date().toISOString(),
258
+ ended_at: null,
259
+ questions_answered: 0,
260
+ correct: 0,
261
+ readiness_score: null,
262
+ });
263
+ const questions = selected.map(card => ({
264
+ cardId: card.id,
265
+ subject: card.subject,
266
+ topic: card.topic,
267
+ question: card.question,
268
+ type: findTemplate(card.question)?.type || 'short_answer',
269
+ choices: findTemplate(card.question)?.choices,
270
+ }));
271
+ return {
272
+ sessionId,
273
+ questions,
274
+ timeLimitMins,
275
+ totalQuestions: questions.length,
276
+ examType,
277
+ instructions: `You have ${timeLimitMins} minutes to complete ${questions.length} questions. This simulates the actual ${examType} exam. Answer each question and submit when done.`,
278
+ };
279
+ }
280
+ export async function submitMockExam(input) {
281
+ const { sessionId, answers } = input;
282
+ const session = getSession(sessionId);
283
+ if (!session) {
284
+ throw new Error(`Session ${sessionId} not found`);
285
+ }
286
+ const profileId = session.profile_id;
287
+ const config = getExamConfig(profileId);
288
+ // Grade all answers
289
+ let correct = 0;
290
+ const topicBreakdown = {};
291
+ const gradedAnswers = [];
292
+ for (const submission of answers) {
293
+ const card = getCard(submission.cardId);
294
+ if (!card)
295
+ continue;
296
+ const template = findTemplate(card.question);
297
+ const result = evaluateAnswer(card, submission.answer, template);
298
+ if (result.correct)
299
+ correct++;
300
+ // Update topic stats
301
+ upsertTopicStats({
302
+ profile_id: profileId,
303
+ topic: card.topic,
304
+ subject: card.subject,
305
+ total_attempts: 1,
306
+ correct_attempts: result.correct ? 1 : 0,
307
+ last_attempt: new Date().toISOString(),
308
+ });
309
+ // Topic breakdown
310
+ if (!topicBreakdown[card.topic]) {
311
+ topicBreakdown[card.topic] = { correct: 0, total: 0 };
312
+ }
313
+ topicBreakdown[card.topic].total++;
314
+ if (result.correct)
315
+ topicBreakdown[card.topic].correct++;
316
+ gradedAnswers.push({
317
+ cardId: submission.cardId,
318
+ correct: result.correct,
319
+ topic: card.topic,
320
+ });
321
+ }
322
+ const total = answers.length;
323
+ const score = total > 0 ? Math.round((correct / total) * 100) : 0;
324
+ // Calculate readiness
325
+ let readinessReport = null;
326
+ if (config) {
327
+ readinessReport = calculateReadiness(profileId, config.exam_type, config.exam_date);
328
+ }
329
+ // Update session
330
+ updateSession({
331
+ id: sessionId,
332
+ ended_at: new Date().toISOString(),
333
+ questions_answered: total,
334
+ correct,
335
+ readiness_score: readinessReport?.score ?? null,
336
+ });
337
+ // Format topic breakdown
338
+ const topicResults = Object.entries(topicBreakdown).map(([topic, stats]) => ({
339
+ topic,
340
+ correct: stats.correct,
341
+ total: stats.total,
342
+ accuracy: Math.round((stats.correct / stats.total) * 100),
343
+ })).sort((a, b) => a.accuracy - b.accuracy);
344
+ // Determine passing threshold
345
+ const examType = session.exam_type;
346
+ let passingThreshold = 60;
347
+ let estimatedGrade = `${score}%`;
348
+ if (examType.startsWith('AP_')) {
349
+ passingThreshold = 55;
350
+ if (score >= 85)
351
+ estimatedGrade = 'AP Score: 5/5';
352
+ else if (score >= 70)
353
+ estimatedGrade = 'AP Score: 4/5';
354
+ else if (score >= 55)
355
+ estimatedGrade = 'AP Score: 3/5';
356
+ else if (score >= 40)
357
+ estimatedGrade = 'AP Score: 2/5';
358
+ else
359
+ estimatedGrade = 'AP Score: 1/5';
360
+ }
361
+ else if (examType === 'SAT_Math') {
362
+ passingThreshold = 60;
363
+ if (score >= 90)
364
+ estimatedGrade = 'SAT Math: ~760-800';
365
+ else if (score >= 80)
366
+ estimatedGrade = 'SAT Math: ~700-750';
367
+ else if (score >= 70)
368
+ estimatedGrade = 'SAT Math: ~630-690';
369
+ else
370
+ estimatedGrade = 'SAT Math: below 630';
371
+ }
372
+ return {
373
+ score,
374
+ correct,
375
+ total,
376
+ passingThreshold,
377
+ passed: score >= passingThreshold,
378
+ estimatedGrade,
379
+ readinessScore: readinessReport?.score,
380
+ topicBreakdown: topicResults,
381
+ strongestTopics: topicResults.filter(t => t.accuracy >= 80).map(t => t.topic),
382
+ weakestTopics: topicResults.filter(t => t.accuracy < 60).map(t => t.topic),
383
+ recommendation: readinessReport?.recommendation,
384
+ };
385
+ }
386
+ export async function getReadiness(input) {
387
+ const { profileId } = input;
388
+ const config = getExamConfig(profileId);
389
+ if (!config) {
390
+ throw new Error('No exam configured. Use set_exam first.');
391
+ }
392
+ const report = calculateReadiness(profileId, config.exam_type, config.exam_date);
393
+ return {
394
+ profileId,
395
+ examType: config.exam_type,
396
+ examDate: config.exam_date,
397
+ ...report,
398
+ };
399
+ }
400
+ export async function getWeakAreas(input) {
401
+ const { profileId } = input;
402
+ const config = getExamConfig(profileId);
403
+ const weakTopicStats = getWeakTopics(profileId, 0.6);
404
+ const topics = weakTopicStats.map(stats => ({
405
+ topic: stats.topic,
406
+ subject: stats.subject,
407
+ accuracy: Math.round((stats.correct_attempts / stats.total_attempts) * 100),
408
+ attempts: stats.total_attempts,
409
+ correctAttempts: stats.correct_attempts,
410
+ }));
411
+ // Suggest focus
412
+ let suggestedFocus = 'No weak areas identified yet — keep practicing!';
413
+ if (topics.length > 0) {
414
+ const top3 = topics.slice(0, 3).map(t => t.topic).join(', ');
415
+ suggestedFocus = `Focus intensively on: ${top3}. Aim for at least 10 practice questions per topic before your exam.`;
416
+ }
417
+ return {
418
+ profileId,
419
+ examType: config?.exam_type,
420
+ topics,
421
+ suggestedFocus,
422
+ totalWeakTopics: topics.length,
423
+ };
424
+ }
package/llms.txt ADDED
@@ -0,0 +1,83 @@
1
+ # exam-prep-mcp
2
+
3
+ > AI-powered exam preparation with FSRS-5 spaced repetition
4
+
5
+ ## Description
6
+
7
+ exam-prep-mcp is a Model Context Protocol server for exam preparation. It implements the FSRS-5 (Free Spaced Repetition Scheduler) algorithm to optimally schedule practice card reviews, tracks student performance in a SQLite database, generates personalized study plans, runs mock exams, and produces readiness reports with grade estimates.
8
+
9
+ ## Tools
10
+
11
+ ### set_exam
12
+ Configure exam preparation for a student profile.
13
+ - profileId: string (required) - unique student identifier
14
+ - examType: string (required) - one of the supported exam types
15
+ - examDate: string (required) - YYYY-MM-DD format
16
+ - currentLevel: "beginner" | "intermediate" | "advanced" (required)
17
+ - Returns: studyPlan, topicsIdentified, daysRemaining, seededCards
18
+
19
+ ### get_daily_session
20
+ Get today's practice questions via FSRS-scheduled card selection.
21
+ - profileId: string (required)
22
+ - Returns: questions array (cardId, subject, topic, question, type, choices), estimatedMinutes, focusAreas
23
+
24
+ ### answer_question
25
+ Submit student answer, evaluate correctness, apply FSRS scheduling.
26
+ - profileId: string (required)
27
+ - cardId: string (required)
28
+ - answer: string (required) - "A"/"B"/"C"/"D" for multiple choice, text for others
29
+ - rating: 1|2|3|4 (optional) - 1=Again, 2=Hard, 3=Good, 4=Easy
30
+ - Returns: correct (bool), correctAnswer, explanation, nextReview, intervalDays, weakAreaAlert
31
+
32
+ ### start_mock_exam
33
+ Begin a full-length timed practice exam.
34
+ - profileId: string (required)
35
+ - Returns: sessionId, questions array, timeLimitMins, totalQuestions, instructions
36
+
37
+ ### submit_mock_exam
38
+ Grade a completed mock exam and update performance data.
39
+ - sessionId: string (required)
40
+ - answers: Array<{cardId: string, answer: string}> (required)
41
+ - Returns: score (0-100), correct, total, estimatedGrade, readinessScore, topicBreakdown, weakestTopics, recommendation
42
+
43
+ ### get_readiness
44
+ Get comprehensive readiness report with score and grade estimate.
45
+ - profileId: string (required)
46
+ - Returns: score (0-100), strongTopics, weakTopics, coveragePercent, trend, recommendation, estimatedGrade
47
+
48
+ ### get_weak_areas
49
+ Identify topics with accuracy below 60% needing focus.
50
+ - profileId: string (required)
51
+ - Returns: topics array (topic, subject, accuracy, attempts), suggestedFocus
52
+
53
+ ## Supported Exam Types
54
+
55
+ AP_Biology, AP_Chemistry, AP_US_History, AP_World_History, AP_Physics, AP_Calculus, AP_Statistics, AP_English, AP_Economics, SAT_Math, SAT_Reading, SAT_Writing, MCAT, LSAT, GRE
56
+
57
+ ## Question Coverage
58
+
59
+ - AP Biology: Cell Structure, DNA Replication, Transcription/Translation, Cell Division, Genetics, Evolution, Ecology, Photosynthesis, Cellular Respiration
60
+ - AP Chemistry: Atomic Structure, Chemical Bonding, Stoichiometry, Gas Laws, Thermodynamics, Equilibrium, Acids/Bases, Electrochemistry
61
+ - AP US History: Colonial Period, Revolution, Constitutional Convention, Civil War, Reconstruction, Progressive Era, WWI, WWII, Cold War, Civil Rights
62
+ - SAT Math: Linear Equations, Systems, Quadratics, Percentages, Statistics, Geometry, Word Problems
63
+
64
+ ## Grade Estimates
65
+
66
+ - AP exams: 1-5 scale (5=Extremely Well Qualified, 3=Qualified)
67
+ - SAT Math: score ranges 400-800
68
+ - MCAT: 472-528 scale
69
+ - LSAT: 120-180 scale
70
+ - GRE: 130-170 per section
71
+
72
+ ## Algorithm
73
+
74
+ FSRS-5 parameters model memory with stability (S), difficulty (D), and retrievability R(t) = e^(-0.9t/S). Cards are scheduled at the interval where predicted retrievability equals 90%. New cards use initial stability based on rating (Again=0.41d, Hard=1.18d, Good=3.13d, Easy=15.47d).
75
+
76
+ ## Configuration
77
+
78
+ Database: EXAM_PREP_DB environment variable, default ~/.exam-prep/study.sqlite
79
+
80
+ ## Installation
81
+
82
+ npm install -g @gonzih/exam-prep-mcp
83
+ or: npx @gonzih/exam-prep-mcp
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@gonzih/exam-prep-mcp",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered exam preparation MCP server with FSRS spaced repetition",
5
+ "license": "MIT",
6
+ "author": "Gonzih",
7
+ "type": "module",
8
+ "bin": {
9
+ "exam-prep-mcp": "./dist/index.js"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsx src/index.ts",
15
+ "start": "node dist/index.js",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "better-sqlite3": "^9.4.3",
21
+ "uuid": "^9.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/better-sqlite3": "^7.6.8",
25
+ "@types/node": "^20.11.5",
26
+ "@types/uuid": "^9.0.7",
27
+ "tsx": "^4.7.0",
28
+ "typescript": "^5.3.3"
29
+ }
30
+ }