@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.
package/dist/fsrs.js ADDED
@@ -0,0 +1,101 @@
1
+ // FSRS-5 Spaced Repetition Algorithm Implementation
2
+ const FSRS_PARAMS = {
3
+ w: [
4
+ 0.4072, 1.1829, 3.1262, 15.4722, 7.2102, 0.5316, 1.0651, 0.0589,
5
+ 1.5330, 0.1544, 1.0071, 1.9299, 0.1100, 0.2900, 2.2700, 0.2500, 2.9898
6
+ ]
7
+ };
8
+ export function scheduleReview(rating, currentStability, currentDifficulty, reviewCount, lastReview) {
9
+ const w = FSRS_PARAMS.w;
10
+ let stability;
11
+ let difficulty;
12
+ let retrievability;
13
+ if (reviewCount === 0) {
14
+ // Initial scheduling for new cards
15
+ switch (rating) {
16
+ case 1:
17
+ stability = w[0];
18
+ break; // Again
19
+ case 2:
20
+ stability = w[1];
21
+ break; // Hard
22
+ case 3:
23
+ stability = w[2];
24
+ break; // Good
25
+ case 4:
26
+ stability = w[3];
27
+ break; // Easy
28
+ }
29
+ // Initial difficulty
30
+ difficulty = w[4] - Math.exp(w[5] * (rating - 1)) + 1;
31
+ difficulty = Math.max(1, Math.min(10, difficulty));
32
+ retrievability = 1.0;
33
+ }
34
+ else {
35
+ // Review scheduling
36
+ const now = new Date();
37
+ const elapsed = lastReview
38
+ ? (now.getTime() - lastReview.getTime()) / (1000 * 60 * 60 * 24)
39
+ : 1;
40
+ // R(t) = e^(-0.9 * t / S)
41
+ retrievability = calculateRetrievability(currentStability, elapsed);
42
+ // Update difficulty using mean reversion
43
+ const deltaDifficulty = -w[6] * (rating - 3);
44
+ difficulty = currentDifficulty + deltaDifficulty * (10 - currentDifficulty) / 9;
45
+ difficulty = Math.max(1, Math.min(10, difficulty));
46
+ // Calculate new stability based on rating
47
+ let stabilityMultiplier;
48
+ if (rating === 1) {
49
+ // Forgetting — reset stability
50
+ stability = w[11] * Math.pow(difficulty, -w[12]) *
51
+ (Math.pow(currentStability + 1, w[13]) - 1) *
52
+ Math.exp(w[14] * (1 - retrievability));
53
+ }
54
+ else {
55
+ // Recall — increase stability
56
+ const ratingFactor = rating === 2 ? w[15] : (rating === 4 ? w[16] : 1.0);
57
+ stabilityMultiplier = Math.exp(w[8]) *
58
+ (11 - difficulty) *
59
+ Math.pow(currentStability, -w[9]) *
60
+ (Math.exp(w[10] * (1 - retrievability)) - 1) *
61
+ ratingFactor;
62
+ stability = currentStability * (1 + stabilityMultiplier);
63
+ }
64
+ stability = Math.max(0.1, stability);
65
+ }
66
+ // Calculate next review interval
67
+ // For target retrievability of 0.9: interval = S * ln(0.9) / ln(R_target)
68
+ // Simplified: interval ≈ stability (days to reach 90% retrievability)
69
+ const targetRetrievability = 0.9;
70
+ let intervalDays = Math.round(stability * Math.log(targetRetrievability) / Math.log(0.9));
71
+ // Clamp interval
72
+ intervalDays = Math.max(1, Math.min(36500, intervalDays));
73
+ // For "Again" ratings, short review
74
+ if (rating === 1) {
75
+ intervalDays = 1;
76
+ }
77
+ const nextReview = new Date();
78
+ nextReview.setDate(nextReview.getDate() + intervalDays);
79
+ return {
80
+ stability,
81
+ difficulty,
82
+ retrievability,
83
+ nextReview,
84
+ intervalDays
85
+ };
86
+ }
87
+ // R(t) = e^(-t/S) where t = days since last review, S = stability
88
+ export function calculateRetrievability(stability, daysSinceReview) {
89
+ if (daysSinceReview <= 0)
90
+ return 1.0;
91
+ const r = Math.exp(-0.9 * daysSinceReview / stability);
92
+ return Math.max(0, Math.min(1, r));
93
+ }
94
+ // Map a 0-1 accuracy to a rating
95
+ export function accuracyToRating(correct, responseTime) {
96
+ if (!correct)
97
+ return 1; // Again
98
+ if (responseTime && responseTime > 60)
99
+ return 2; // Hard (took long)
100
+ return 3; // Good (default)
101
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { initDb } from './db.js';
6
+ import { setExam, getDailySession, answerQuestion, startMockExam, submitMockExam, getReadiness, getWeakAreas, } from './tools.js';
7
+ const server = new Server({
8
+ name: 'exam-prep-mcp',
9
+ version: '0.1.0',
10
+ }, {
11
+ capabilities: {
12
+ tools: {},
13
+ },
14
+ });
15
+ // ─── List Tools ───────────────────────────────────────────────────────────────
16
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
17
+ return {
18
+ tools: [
19
+ {
20
+ name: 'set_exam',
21
+ description: 'Configure an exam for a student profile. Seeds question cards and generates a study plan.',
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ profileId: {
26
+ type: 'string',
27
+ description: 'Unique identifier for the student (e.g., "alice", "student-001")',
28
+ },
29
+ examType: {
30
+ type: 'string',
31
+ enum: [
32
+ 'AP_Biology', 'AP_Chemistry', 'AP_US_History', 'AP_World_History',
33
+ 'AP_Physics', 'AP_Calculus', 'AP_Statistics', 'AP_English',
34
+ 'AP_Economics', 'SAT_Math', 'SAT_Reading', 'SAT_Writing',
35
+ 'MCAT', 'LSAT', 'GRE',
36
+ ],
37
+ description: 'Type of exam to prepare for',
38
+ },
39
+ examDate: {
40
+ type: 'string',
41
+ description: 'Exam date in YYYY-MM-DD format (e.g., "2025-05-15")',
42
+ },
43
+ currentLevel: {
44
+ type: 'string',
45
+ enum: ['beginner', 'intermediate', 'advanced'],
46
+ description: 'Current knowledge level of the student',
47
+ },
48
+ },
49
+ required: ['profileId', 'examType', 'examDate', 'currentLevel'],
50
+ },
51
+ },
52
+ {
53
+ name: 'get_daily_session',
54
+ description: 'Get today\'s personalized practice questions based on spaced repetition scheduling.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ profileId: {
59
+ type: 'string',
60
+ description: 'Student profile ID',
61
+ },
62
+ },
63
+ required: ['profileId'],
64
+ },
65
+ },
66
+ {
67
+ name: 'answer_question',
68
+ description: 'Submit an answer to a practice question. Updates FSRS scheduling and topic stats.',
69
+ inputSchema: {
70
+ type: 'object',
71
+ properties: {
72
+ profileId: {
73
+ type: 'string',
74
+ description: 'Student profile ID',
75
+ },
76
+ cardId: {
77
+ type: 'string',
78
+ description: 'The card ID from get_daily_session or start_mock_exam',
79
+ },
80
+ answer: {
81
+ type: 'string',
82
+ description: 'The student\'s answer (for multiple choice: "A", "B", "C", or "D")',
83
+ },
84
+ rating: {
85
+ type: 'number',
86
+ enum: [1, 2, 3, 4],
87
+ description: 'Optional manual difficulty rating: 1=Again, 2=Hard, 3=Good, 4=Easy',
88
+ },
89
+ },
90
+ required: ['profileId', 'cardId', 'answer'],
91
+ },
92
+ },
93
+ {
94
+ name: 'start_mock_exam',
95
+ description: 'Begin a timed full-length practice exam simulating real exam conditions.',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ profileId: {
100
+ type: 'string',
101
+ description: 'Student profile ID',
102
+ },
103
+ },
104
+ required: ['profileId'],
105
+ },
106
+ },
107
+ {
108
+ name: 'submit_mock_exam',
109
+ description: 'Submit all answers for a mock exam and receive scoring and analysis.',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ sessionId: {
114
+ type: 'string',
115
+ description: 'Session ID from start_mock_exam',
116
+ },
117
+ answers: {
118
+ type: 'array',
119
+ description: 'Array of answers for each question',
120
+ items: {
121
+ type: 'object',
122
+ properties: {
123
+ cardId: { type: 'string', description: 'Card ID' },
124
+ answer: { type: 'string', description: 'Student\'s answer' },
125
+ },
126
+ required: ['cardId', 'answer'],
127
+ },
128
+ },
129
+ },
130
+ required: ['sessionId', 'answers'],
131
+ },
132
+ },
133
+ {
134
+ name: 'get_readiness',
135
+ description: 'Get a comprehensive readiness report with score, grade estimate, trends, and recommendations.',
136
+ inputSchema: {
137
+ type: 'object',
138
+ properties: {
139
+ profileId: {
140
+ type: 'string',
141
+ description: 'Student profile ID',
142
+ },
143
+ },
144
+ required: ['profileId'],
145
+ },
146
+ },
147
+ {
148
+ name: 'get_weak_areas',
149
+ description: 'Identify topics with low accuracy that need focused study.',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: {
153
+ profileId: {
154
+ type: 'string',
155
+ description: 'Student profile ID',
156
+ },
157
+ },
158
+ required: ['profileId'],
159
+ },
160
+ },
161
+ ],
162
+ };
163
+ });
164
+ // ─── Call Tool ────────────────────────────────────────────────────────────────
165
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
166
+ const { name, arguments: args } = request.params;
167
+ if (!args) {
168
+ throw new McpError(ErrorCode.InvalidParams, 'No arguments provided');
169
+ }
170
+ try {
171
+ let result;
172
+ switch (name) {
173
+ case 'set_exam':
174
+ result = await setExam(args);
175
+ break;
176
+ case 'get_daily_session':
177
+ result = await getDailySession(args);
178
+ break;
179
+ case 'answer_question':
180
+ result = await answerQuestion(args);
181
+ break;
182
+ case 'start_mock_exam':
183
+ result = await startMockExam(args);
184
+ break;
185
+ case 'submit_mock_exam':
186
+ result = await submitMockExam(args);
187
+ break;
188
+ case 'get_readiness':
189
+ result = await getReadiness(args);
190
+ break;
191
+ case 'get_weak_areas':
192
+ result = await getWeakAreas(args);
193
+ break;
194
+ default:
195
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
196
+ }
197
+ return {
198
+ content: [
199
+ {
200
+ type: 'text',
201
+ text: JSON.stringify(result, null, 2),
202
+ },
203
+ ],
204
+ };
205
+ }
206
+ catch (error) {
207
+ if (error instanceof McpError) {
208
+ throw error;
209
+ }
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${message}`);
212
+ }
213
+ });
214
+ // ─── Start Server ─────────────────────────────────────────────────────────────
215
+ async function main() {
216
+ // Initialize database
217
+ initDb();
218
+ const transport = new StdioServerTransport();
219
+ await server.connect(transport);
220
+ // Log to stderr so stdout remains clean for MCP protocol
221
+ process.stderr.write('exam-prep-mcp server started\n');
222
+ }
223
+ main().catch((error) => {
224
+ process.stderr.write(`Fatal error: ${error}\n`);
225
+ process.exit(1);
226
+ });
@@ -0,0 +1,17 @@
1
+ export interface QuestionTemplate {
2
+ subject: string;
3
+ topic: string;
4
+ type: 'multiple_choice' | 'short_answer' | 'free_response' | 'problem_solving';
5
+ question: string;
6
+ answer: string;
7
+ explanation: string;
8
+ choices?: string[];
9
+ difficulty: 'easy' | 'medium' | 'hard';
10
+ examWeight: number;
11
+ }
12
+ export declare const QUESTION_BANK: QuestionTemplate[];
13
+ export declare function getQuestionsBySubject(subject: string): QuestionTemplate[];
14
+ export declare function getQuestionsByTopic(subject: string, topic: string): QuestionTemplate[];
15
+ export declare function getRandomQuestions(subject: string, count: number): QuestionTemplate[];
16
+ export declare function getAllSubjects(): string[];
17
+ export declare function getTopicsForSubject(subject: string): string[];