quiz-mcp 0.0.1

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.
Files changed (114) hide show
  1. package/README.md +85 -0
  2. package/bin/quiz +16 -0
  3. package/bin/quiz-runner +18 -0
  4. package/bin/quiz-runner-mcp +18 -0
  5. package/dist/cli.js +805 -0
  6. package/dist/client/_app/immutable/assets/0.BdUfdP7M.css +1 -0
  7. package/dist/client/_app/immutable/assets/0.BdUfdP7M.css.br +0 -0
  8. package/dist/client/_app/immutable/assets/0.BdUfdP7M.css.gz +0 -0
  9. package/dist/client/_app/immutable/chunks/BBbVH-DX.js +2 -0
  10. package/dist/client/_app/immutable/chunks/BBbVH-DX.js.br +0 -0
  11. package/dist/client/_app/immutable/chunks/BBbVH-DX.js.gz +0 -0
  12. package/dist/client/_app/immutable/chunks/BYAtt_Gu.js +1 -0
  13. package/dist/client/_app/immutable/chunks/BYAtt_Gu.js.br +0 -0
  14. package/dist/client/_app/immutable/chunks/BYAtt_Gu.js.gz +0 -0
  15. package/dist/client/_app/immutable/chunks/C9b1VZtB.js +1 -0
  16. package/dist/client/_app/immutable/chunks/C9b1VZtB.js.br +0 -0
  17. package/dist/client/_app/immutable/chunks/C9b1VZtB.js.gz +0 -0
  18. package/dist/client/_app/immutable/chunks/DnSqigra.js +1 -0
  19. package/dist/client/_app/immutable/chunks/DnSqigra.js.br +0 -0
  20. package/dist/client/_app/immutable/chunks/DnSqigra.js.gz +0 -0
  21. package/dist/client/_app/immutable/chunks/DqHG5Xll.js +3 -0
  22. package/dist/client/_app/immutable/chunks/DqHG5Xll.js.br +0 -0
  23. package/dist/client/_app/immutable/chunks/DqHG5Xll.js.gz +0 -0
  24. package/dist/client/_app/immutable/chunks/fMaWj-vy.js +1 -0
  25. package/dist/client/_app/immutable/chunks/fMaWj-vy.js.br +0 -0
  26. package/dist/client/_app/immutable/chunks/fMaWj-vy.js.gz +0 -0
  27. package/dist/client/_app/immutable/chunks/hqRb622q.js +1 -0
  28. package/dist/client/_app/immutable/chunks/hqRb622q.js.br +0 -0
  29. package/dist/client/_app/immutable/chunks/hqRb622q.js.gz +0 -0
  30. package/dist/client/_app/immutable/chunks/qgn7L4n5.js +1 -0
  31. package/dist/client/_app/immutable/chunks/qgn7L4n5.js.br +0 -0
  32. package/dist/client/_app/immutable/chunks/qgn7L4n5.js.gz +0 -0
  33. package/dist/client/_app/immutable/entry/app.BsKq4Thd.js +2 -0
  34. package/dist/client/_app/immutable/entry/app.BsKq4Thd.js.br +0 -0
  35. package/dist/client/_app/immutable/entry/app.BsKq4Thd.js.gz +0 -0
  36. package/dist/client/_app/immutable/entry/start.CKTlgCvu.js +1 -0
  37. package/dist/client/_app/immutable/entry/start.CKTlgCvu.js.br +2 -0
  38. package/dist/client/_app/immutable/entry/start.CKTlgCvu.js.gz +0 -0
  39. package/dist/client/_app/immutable/nodes/0.R6s8931r.js +9 -0
  40. package/dist/client/_app/immutable/nodes/0.R6s8931r.js.br +0 -0
  41. package/dist/client/_app/immutable/nodes/0.R6s8931r.js.gz +0 -0
  42. package/dist/client/_app/immutable/nodes/1.CBs3iFLr.js +1 -0
  43. package/dist/client/_app/immutable/nodes/1.CBs3iFLr.js.br +0 -0
  44. package/dist/client/_app/immutable/nodes/1.CBs3iFLr.js.gz +0 -0
  45. package/dist/client/_app/immutable/nodes/2.B9MLTE_H.js +2 -0
  46. package/dist/client/_app/immutable/nodes/2.B9MLTE_H.js.br +0 -0
  47. package/dist/client/_app/immutable/nodes/2.B9MLTE_H.js.gz +0 -0
  48. package/dist/client/_app/immutable/nodes/3.w4AbulQF.js +1 -0
  49. package/dist/client/_app/immutable/nodes/3.w4AbulQF.js.br +0 -0
  50. package/dist/client/_app/immutable/nodes/3.w4AbulQF.js.gz +0 -0
  51. package/dist/client/_app/immutable/nodes/4.BAhm-aFq.js +4 -0
  52. package/dist/client/_app/immutable/nodes/4.BAhm-aFq.js.br +0 -0
  53. package/dist/client/_app/immutable/nodes/4.BAhm-aFq.js.gz +0 -0
  54. package/dist/client/_app/version.json +1 -0
  55. package/dist/client/_app/version.json.br +0 -0
  56. package/dist/client/_app/version.json.gz +0 -0
  57. package/dist/client/robots.txt +3 -0
  58. package/dist/env.js +94 -0
  59. package/dist/handler.js +1435 -0
  60. package/dist/index.js +345 -0
  61. package/dist/server/chunks/0-bFxjafp8.js +44 -0
  62. package/dist/server/chunks/0-bFxjafp8.js.map +1 -0
  63. package/dist/server/chunks/1-D1qe0uHb.js +9 -0
  64. package/dist/server/chunks/1-D1qe0uHb.js.map +1 -0
  65. package/dist/server/chunks/2-NzZFDUaQ.js +60 -0
  66. package/dist/server/chunks/2-NzZFDUaQ.js.map +1 -0
  67. package/dist/server/chunks/3-DJAG62O1.js +37 -0
  68. package/dist/server/chunks/3-DJAG62O1.js.map +1 -0
  69. package/dist/server/chunks/4-zigWBLLG.js +37 -0
  70. package/dist/server/chunks/4-zigWBLLG.js.map +1 -0
  71. package/dist/server/chunks/_error.svelte-BHNrphys.js +80 -0
  72. package/dist/server/chunks/_error.svelte-BHNrphys.js.map +1 -0
  73. package/dist/server/chunks/_layout.svelte-BUJDEr5D.js +104 -0
  74. package/dist/server/chunks/_layout.svelte-BUJDEr5D.js.map +1 -0
  75. package/dist/server/chunks/_page.svelte-CRU5jaGZ.js +653 -0
  76. package/dist/server/chunks/_page.svelte-CRU5jaGZ.js.map +1 -0
  77. package/dist/server/chunks/_page.svelte-Cm8ocLsi.js +572 -0
  78. package/dist/server/chunks/_page.svelte-Cm8ocLsi.js.map +1 -0
  79. package/dist/server/chunks/_page.svelte-Dvh1k6R9.js +71 -0
  80. package/dist/server/chunks/_page.svelte-Dvh1k6R9.js.map +1 -0
  81. package/dist/server/chunks/_server.ts-BZp77VsF.js +101 -0
  82. package/dist/server/chunks/_server.ts-BZp77VsF.js.map +1 -0
  83. package/dist/server/chunks/_server.ts-C2NKc-VD.js +219 -0
  84. package/dist/server/chunks/_server.ts-C2NKc-VD.js.map +1 -0
  85. package/dist/server/chunks/_server.ts-CHyNcOmK.js +39 -0
  86. package/dist/server/chunks/_server.ts-CHyNcOmK.js.map +1 -0
  87. package/dist/server/chunks/_server.ts-CakPvrry.js +51 -0
  88. package/dist/server/chunks/_server.ts-CakPvrry.js.map +1 -0
  89. package/dist/server/chunks/_server.ts-uSNr19lE.js +35 -0
  90. package/dist/server/chunks/_server.ts-uSNr19lE.js.map +1 -0
  91. package/dist/server/chunks/client-Dw0t4rDL.js +25 -0
  92. package/dist/server/chunks/client-Dw0t4rDL.js.map +1 -0
  93. package/dist/server/chunks/exports-BNC8dbq3.js +326 -0
  94. package/dist/server/chunks/exports-BNC8dbq3.js.map +1 -0
  95. package/dist/server/chunks/hooks.server-B6FrjICA.js +66 -0
  96. package/dist/server/chunks/hooks.server-B6FrjICA.js.map +1 -0
  97. package/dist/server/chunks/hooks.universal-B3lncnqn.js +6 -0
  98. package/dist/server/chunks/hooks.universal-B3lncnqn.js.map +1 -0
  99. package/dist/server/chunks/html-FW6Ia4bL.js +8 -0
  100. package/dist/server/chunks/html-FW6Ia4bL.js.map +1 -0
  101. package/dist/server/chunks/index-Ch5Bqlgz.js +1411 -0
  102. package/dist/server/chunks/index-Ch5Bqlgz.js.map +1 -0
  103. package/dist/server/chunks/index-wpIsICWW.js +219 -0
  104. package/dist/server/chunks/index-wpIsICWW.js.map +1 -0
  105. package/dist/server/chunks/runtime-Bf5HkYyl.js +318 -0
  106. package/dist/server/chunks/runtime-Bf5HkYyl.js.map +1 -0
  107. package/dist/server/chunks/sessions-CEbRx2vt.js +169 -0
  108. package/dist/server/chunks/sessions-CEbRx2vt.js.map +1 -0
  109. package/dist/server/index.js +7609 -0
  110. package/dist/server/index.js.map +1 -0
  111. package/dist/server/manifest.js +97 -0
  112. package/dist/server/manifest.js.map +1 -0
  113. package/dist/shims.js +32 -0
  114. package/package.json +122 -0
package/dist/cli.js ADDED
@@ -0,0 +1,805 @@
1
+ // src/cli/cli.ts
2
+ import { readFileSync, writeFileSync, existsSync as existsSync2 } from "node:fs";
3
+ import { join as join2, dirname as dirname2 } from "node:path";
4
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
5
+ import { Command } from "commander";
6
+ import openFn from "open";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { z } from "zod";
10
+
11
+ // src/server/index.ts
12
+ import { createServer } from "node:http";
13
+
14
+ // src/lib/server/sessions.ts
15
+ var SessionStore = class {
16
+ sessions = /* @__PURE__ */ new Map();
17
+ sessionDurationMs;
18
+ storagePath;
19
+ constructor(options = {}) {
20
+ const durationMinutes = options.sessionDurationMinutes || 60;
21
+ this.sessionDurationMs = durationMinutes * 60 * 1e3;
22
+ this.storagePath = options.storagePath;
23
+ if (typeof setInterval !== "undefined") {
24
+ setInterval(() => this.cleanup(), 6e4);
25
+ }
26
+ }
27
+ /**
28
+ * Generate a unique ID
29
+ */
30
+ generateId(prefix) {
31
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
32
+ }
33
+ /**
34
+ * Create a new quiz session
35
+ */
36
+ createSession(quiz, config = {}, customSessionId) {
37
+ const quizId = this.generateId("quiz");
38
+ const sessionId = customSessionId || this.generateId("session");
39
+ const now = /* @__PURE__ */ new Date();
40
+ const expiresAt = new Date(now.getTime() + this.sessionDurationMs);
41
+ const session = {
42
+ quizId,
43
+ sessionId,
44
+ quiz,
45
+ config,
46
+ status: "active",
47
+ startedAt: now.toISOString(),
48
+ expiresAt: expiresAt.toISOString()
49
+ };
50
+ this.sessions.set(`${quizId}:${sessionId}`, session);
51
+ return session;
52
+ }
53
+ /**
54
+ * Get a session by quiz ID and session ID
55
+ */
56
+ getSession(quizId, sessionId) {
57
+ const key = `${quizId}:${sessionId}`;
58
+ const session = this.sessions.get(key);
59
+ if (session) {
60
+ if (new Date(session.expiresAt) < /* @__PURE__ */ new Date()) {
61
+ session.status = "expired";
62
+ this.sessions.delete(key);
63
+ return void 0;
64
+ }
65
+ }
66
+ return session;
67
+ }
68
+ /**
69
+ * Get session by full key
70
+ */
71
+ getSessionByKey(key) {
72
+ const session = this.sessions.get(key);
73
+ if (!session) return void 0;
74
+ if (new Date(session.expiresAt) < /* @__PURE__ */ new Date()) {
75
+ session.status = "expired";
76
+ this.sessions.delete(key);
77
+ return void 0;
78
+ }
79
+ return session;
80
+ }
81
+ /**
82
+ * Update session status
83
+ */
84
+ updateStatus(quizId, sessionId, status) {
85
+ const session = this.getSession(quizId, sessionId);
86
+ if (!session) return void 0;
87
+ session.status = status;
88
+ if (status === "completed") {
89
+ session.completedAt = (/* @__PURE__ */ new Date()).toISOString();
90
+ } else if (status === "expired") {
91
+ session.completedAt = session.completedAt || (/* @__PURE__ */ new Date()).toISOString();
92
+ }
93
+ return session;
94
+ }
95
+ /**
96
+ * Save answers to session
97
+ */
98
+ saveAnswers(quizId, sessionId, answers) {
99
+ const session = this.getSession(quizId, sessionId);
100
+ if (!session || session.status !== "active") return void 0;
101
+ session.answers = answers;
102
+ return session;
103
+ }
104
+ /**
105
+ * Complete session with results
106
+ */
107
+ completeSession(quizId, sessionId, results) {
108
+ const session = this.getSession(quizId, sessionId);
109
+ if (!session) return void 0;
110
+ session.status = "completed";
111
+ session.completedAt = (/* @__PURE__ */ new Date()).toISOString();
112
+ session.results = results;
113
+ return session;
114
+ }
115
+ /**
116
+ * Cancel session
117
+ */
118
+ cancelSession(quizId, sessionId, reason) {
119
+ const session = this.getSession(quizId, sessionId);
120
+ if (!session || session.status !== "active") return void 0;
121
+ session.status = "completed";
122
+ session.cancelledAt = (/* @__PURE__ */ new Date()).toISOString();
123
+ session.cancelReason = reason;
124
+ return session;
125
+ }
126
+ /**
127
+ * Get all active sessions
128
+ */
129
+ getActiveSessions() {
130
+ const now = /* @__PURE__ */ new Date();
131
+ return Array.from(this.sessions.values()).filter(
132
+ (s) => s.status === "active" && new Date(s.expiresAt) > now
133
+ );
134
+ }
135
+ /**
136
+ * Get session count
137
+ */
138
+ getSessionCount() {
139
+ const now = /* @__PURE__ */ new Date();
140
+ const active = Array.from(this.sessions.values()).filter(
141
+ (s) => s.status === "active" && new Date(s.expiresAt) > now
142
+ ).length;
143
+ return { active, total: this.sessions.size };
144
+ }
145
+ /**
146
+ * Clean up expired sessions
147
+ */
148
+ cleanup() {
149
+ const now = /* @__PURE__ */ new Date();
150
+ let removed = 0;
151
+ for (const [key, session] of this.sessions.entries()) {
152
+ if (new Date(session.expiresAt) < now) {
153
+ session.status = "expired";
154
+ this.sessions.delete(key);
155
+ removed++;
156
+ }
157
+ }
158
+ return removed;
159
+ }
160
+ /**
161
+ * Clear all sessions
162
+ */
163
+ clear() {
164
+ this.sessions.clear();
165
+ }
166
+ };
167
+ var globalStore = null;
168
+ function getSessionStore() {
169
+ const globalKey = "quiz-mcp-session-store";
170
+ const existingStore = globalThis[globalKey];
171
+ if (existingStore) {
172
+ return existingStore;
173
+ }
174
+ if (!globalStore) {
175
+ globalStore = new SessionStore();
176
+ }
177
+ globalThis[globalKey] = globalStore;
178
+ return globalStore;
179
+ }
180
+
181
+ // src/server/index.ts
182
+ import { dirname, join } from "node:path";
183
+ import { fileURLToPath } from "node:url";
184
+ import { getRequest, setResponse } from "@sveltejs/kit/node";
185
+ import { existsSync, createReadStream } from "node:fs";
186
+ var __dirname = dirname(fileURLToPath(import.meta.url));
187
+ var serverInstance = null;
188
+ async function getServer() {
189
+ if (serverInstance === null) {
190
+ const buildDir = join(__dirname, "server");
191
+ const serverModule = await import(`${buildDir}/index.js`);
192
+ const manifestModule = await import(`${buildDir}/manifest.js`);
193
+ const server = new serverModule.Server(manifestModule.manifest);
194
+ await server.init({
195
+ env: process.env,
196
+ read: (file) => {
197
+ const stream = createReadStream(join(__dirname, "client", file));
198
+ return stream;
199
+ }
200
+ });
201
+ serverInstance = server;
202
+ }
203
+ return serverInstance;
204
+ }
205
+ function calculateProgress(session) {
206
+ const total = session.quiz.questions.length;
207
+ const answered = session.answers?.size || 0;
208
+ return { answered, total, percentage: total > 0 ? Math.round(answered / total * 100) : 0 };
209
+ }
210
+ async function createRequestHandler(server) {
211
+ const buildDir = join(__dirname, "server");
212
+ const clientDir = join(__dirname, "client");
213
+ const serveStaticFile = async (req, res) => {
214
+ const url = req.url || "";
215
+ if (!url.startsWith("/_app/")) {
216
+ return false;
217
+ }
218
+ const filePath = join(clientDir, url);
219
+ try {
220
+ if (!existsSync(filePath)) {
221
+ res.writeHead(404);
222
+ res.end("Not Found");
223
+ return true;
224
+ }
225
+ const ext = url.split(".").pop()?.toLowerCase();
226
+ const contentTypes = {
227
+ js: "application/javascript",
228
+ css: "text/css",
229
+ json: "application/json",
230
+ png: "image/png",
231
+ jpg: "image/jpeg",
232
+ gif: "image/gif",
233
+ svg: "image/svg+xml",
234
+ woff: "font/woff",
235
+ woff2: "font/woff2",
236
+ ico: "image/x-icon"
237
+ };
238
+ const contentType = contentTypes[ext || ""] || "application/octet-stream";
239
+ res.writeHead(200, {
240
+ "Content-Type": contentType,
241
+ "Cache-Control": "public, max-age=31536000, immutable"
242
+ });
243
+ const stream = createReadStream(filePath);
244
+ stream.pipe(res);
245
+ return true;
246
+ } catch (error) {
247
+ console.error("Static file error:", error);
248
+ res.writeHead(500);
249
+ res.end("Internal Server Error");
250
+ return true;
251
+ }
252
+ };
253
+ return async (req, res) => {
254
+ const ip = req.headers["x-forwarded-for"]?.toString() || req.socket.remoteAddress?.replace("::ffff:", "") || "127.0.0.1";
255
+ try {
256
+ const wasStatic = await serveStaticFile(req, res);
257
+ if (wasStatic) return;
258
+ const origin = `http://${req.headers.host}`;
259
+ const request = await getRequest({
260
+ base: origin,
261
+ request: req
262
+ });
263
+ const response = await server.respond(request, {
264
+ platform: { req },
265
+ getClientAddress: () => ip
266
+ });
267
+ await setResponse(res, response);
268
+ } catch (error) {
269
+ console.error("Request error:", error);
270
+ res.writeHead(500, { "Content-Type": "application/json" });
271
+ res.end(JSON.stringify({ error: "Internal server error" }));
272
+ }
273
+ };
274
+ }
275
+ async function createQuizServer(options = {}) {
276
+ const port = options.port || Number(process.env.PORT) || Number(process.env.QUIZ_PORT) || 3e3;
277
+ const host = options.host || process.env.HOST || process.env.QUIZ_HOST || "localhost";
278
+ if (options.configPath) {
279
+ process.env.QUIZ_CONFIG = options.configPath;
280
+ }
281
+ const sessionStore = getSessionStore();
282
+ const server = await getServer();
283
+ const requestHandler = await createRequestHandler(server);
284
+ const httpServer = createServer(requestHandler);
285
+ let serverUrl = "";
286
+ let isStarted = false;
287
+ const quizServer = {
288
+ server: httpServer,
289
+ get url() {
290
+ return serverUrl;
291
+ },
292
+ port,
293
+ host,
294
+ async start() {
295
+ if (isStarted) return;
296
+ await new Promise((resolve, reject) => {
297
+ httpServer.on("error", reject);
298
+ httpServer.listen(port, "127.0.0.1", () => {
299
+ isStarted = true;
300
+ const address = httpServer.address();
301
+ serverUrl = `http://127.0.0.1:${address.port}`;
302
+ resolve();
303
+ });
304
+ });
305
+ options.onReady?.(serverUrl);
306
+ },
307
+ async stop() {
308
+ return new Promise((resolve) => {
309
+ httpServer.close(() => resolve());
310
+ });
311
+ },
312
+ async createSession(quiz, config, open, customSessionId) {
313
+ const session = sessionStore.createSession(quiz, config || {}, customSessionId);
314
+ const quizUrl = `${serverUrl}/${session.quizId}/${session.sessionId}`;
315
+ return {
316
+ quizId: session.quizId,
317
+ sessionId: session.sessionId,
318
+ url: open !== false ? quizUrl : "",
319
+ expiresAt: session.expiresAt
320
+ };
321
+ },
322
+ async getSessionStatus(quizId, sessionId, includeAnswers) {
323
+ const session = sessionStore.getSession(quizId, sessionId);
324
+ if (!session) {
325
+ return { status: "not_found", startedAt: "" };
326
+ }
327
+ if (session.status === "expired") {
328
+ return {
329
+ status: "expired",
330
+ startedAt: session.startedAt,
331
+ completedAt: session.completedAt
332
+ };
333
+ }
334
+ if (includeAnswers && session.results) {
335
+ return {
336
+ status: session.status,
337
+ startedAt: session.startedAt,
338
+ completedAt: session.completedAt,
339
+ progress: calculateProgress(session),
340
+ answers: session.results
341
+ };
342
+ }
343
+ return {
344
+ status: session.status,
345
+ startedAt: session.startedAt,
346
+ completedAt: session.completedAt,
347
+ progress: calculateProgress(session)
348
+ };
349
+ },
350
+ async cancelSession(quizId, sessionId, reason) {
351
+ const session = sessionStore.cancelSession(quizId, sessionId, reason);
352
+ if (!session) {
353
+ return { ok: false, message: "Session not found or already completed", cancelledAt: "" };
354
+ }
355
+ return {
356
+ ok: true,
357
+ message: reason || "Session cancelled",
358
+ cancelledAt: session.cancelledAt || (/* @__PURE__ */ new Date()).toISOString()
359
+ };
360
+ }
361
+ };
362
+ return quizServer;
363
+ }
364
+
365
+ // src/cli/cli.ts
366
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
367
+ var QuestionTypeSchema = z.enum(["single", "multiple", "remove-extra", "order", "match", "text"]);
368
+ var QuestionSchema = z.object({
369
+ id: z.string(),
370
+ type: QuestionTypeSchema,
371
+ question: z.string(),
372
+ explanation: z.string().optional(),
373
+ options: z.array(z.string()).optional(),
374
+ points: z.number().optional(),
375
+ required: z.boolean().optional()
376
+ });
377
+ var QuizDataSchema = z.object({
378
+ title: z.string().optional(),
379
+ description: z.string().optional(),
380
+ version: z.string().optional(),
381
+ metadata: z.object({
382
+ author: z.string().optional(),
383
+ difficulty: z.enum(["easy", "medium", "hard"]).optional(),
384
+ estimatedTime: z.number().optional(),
385
+ tags: z.array(z.string()).optional()
386
+ }).optional(),
387
+ questions: z.array(QuestionSchema).min(1)
388
+ });
389
+ var QuizConfigSchema = z.object({
390
+ theme: z.enum(["light", "dark", "auto"]).optional(),
391
+ shuffleQuestions: z.boolean().optional(),
392
+ shuffleOptions: z.boolean().optional(),
393
+ timerEnabled: z.boolean().optional(),
394
+ timerDuration: z.number().optional()
395
+ }).optional();
396
+ function loadConfig(path) {
397
+ if (!path) return void 0;
398
+ try {
399
+ const content = readFileSync(path, "utf-8");
400
+ return JSON.parse(content);
401
+ } catch (error) {
402
+ throw new Error(`Failed to load config file: ${path}
403
+ ${error}`);
404
+ }
405
+ }
406
+ function loadMCPConfig(path) {
407
+ if (!path) return void 0;
408
+ try {
409
+ const content = readFileSync(path, "utf-8");
410
+ return JSON.parse(content);
411
+ } catch (error) {
412
+ throw new Error(`Failed to load MCP config file: ${path}
413
+ ${error}`);
414
+ }
415
+ }
416
+ function loadQuiz(source, file) {
417
+ if (file) {
418
+ try {
419
+ const content = readFileSync(file, "utf-8");
420
+ return JSON.parse(content);
421
+ } catch (error) {
422
+ throw new Error(`Failed to load quiz file: ${file}
423
+ ${error}`);
424
+ }
425
+ }
426
+ if (source) {
427
+ try {
428
+ return JSON.parse(source);
429
+ } catch {
430
+ throw new Error("Invalid quiz JSON. Provide valid JSON with --quiz flag.");
431
+ }
432
+ }
433
+ throw new Error("No quiz provided. Use --file <path> or --quiz <json>");
434
+ }
435
+ async function waitForServer(server, timeout = 3e4) {
436
+ const start = Date.now();
437
+ while (Date.now() - start < timeout) {
438
+ try {
439
+ const response = await fetch(`${server.url}/api/quiz`, { method: "GET" });
440
+ if (response.ok) return;
441
+ } catch {
442
+ }
443
+ await new Promise((r) => setTimeout(r, 500));
444
+ }
445
+ throw new Error("Server failed to start");
446
+ }
447
+ async function pollForResults(server, quizId, sessionId, interval = 1e3) {
448
+ while (true) {
449
+ try {
450
+ const status = await server.getSessionStatus(quizId, sessionId);
451
+ if (status.status === "completed" || status.status === "not_found") {
452
+ break;
453
+ }
454
+ } catch {
455
+ }
456
+ await new Promise((r) => setTimeout(r, interval));
457
+ }
458
+ }
459
+ async function ensureBuildExists() {
460
+ const entry = join2(__dirname2, "index.js");
461
+ if (!existsSync2(entry)) {
462
+ console.log("Building quiz-mcp...");
463
+ const { spawn } = await import("node:child_process");
464
+ await new Promise((resolve, reject) => {
465
+ const proc = spawn("pnpm", ["run", "build"], { stdio: "inherit" });
466
+ proc.on("close", (code) => {
467
+ if (code === 0) resolve();
468
+ else reject(new Error(`Build failed with exit code ${code}`));
469
+ });
470
+ proc.on("error", reject);
471
+ });
472
+ }
473
+ }
474
+ async function runRunnerMode(args) {
475
+ const quiz = loadQuiz(args.quiz, args.file);
476
+ const config = loadConfig(args.config);
477
+ const port = Number(args.port) || parseInt(process.env.QUIZ_PORT || "3000", 10);
478
+ const host = args.host ? String(args.host) : process.env.QUIZ_HOST || "localhost";
479
+ const shouldOpen = args.open === true || args.open === "true" || process.env.QUIZ_OPEN === "true";
480
+ const outputPath = args.output ? String(args.output) : process.env.QUIZ_OUTPUT || void 0;
481
+ const webhookUrl = args.webhook ? String(args.webhook) : config?.webhook || process.env.QUIZ_WEBHOOK || void 0;
482
+ const configPath = args.config ? String(args.config) : process.env.QUIZ_CONFIG;
483
+ if (configPath) {
484
+ process.env.QUIZ_CONFIG = configPath;
485
+ }
486
+ await ensureBuildExists();
487
+ console.log(`Starting quiz server at http://${host}:${port}`);
488
+ const server = await createQuizServer({
489
+ port,
490
+ host,
491
+ configPath,
492
+ onReady: (url) => console.log(`Server ready at ${url}`)
493
+ });
494
+ await server.start();
495
+ await waitForServer(server);
496
+ const session = await server.createSession(quiz, config, true);
497
+ console.log(`Quiz session created: ${session.quizId}/${session.sessionId}`);
498
+ console.log(`Quiz URL: ${session.url}`);
499
+ if (shouldOpen) {
500
+ await openFn(session.url);
501
+ console.log("Browser opened");
502
+ }
503
+ console.log("Waiting for quiz completion...");
504
+ await pollForResults(server, session.quizId, session.sessionId);
505
+ const status = await server.getSessionStatus(session.quizId, session.sessionId, true);
506
+ if (status.answers) {
507
+ const results = status.answers;
508
+ if (outputPath) {
509
+ writeFileSync(outputPath, JSON.stringify(results, null, 2));
510
+ console.log(`Results saved to: ${outputPath}`);
511
+ }
512
+ if (webhookUrl) {
513
+ try {
514
+ await fetch(webhookUrl, {
515
+ method: "POST",
516
+ headers: { "Content-Type": "application/json" },
517
+ body: JSON.stringify(results)
518
+ });
519
+ console.log(`Results sent to webhook: ${webhookUrl}`);
520
+ } catch (error) {
521
+ console.error(`Failed to send to webhook: ${error}`);
522
+ }
523
+ }
524
+ console.log("\nQuiz Results:");
525
+ console.log(` Total Questions: ${results.summary.totalQuestions}`);
526
+ console.log(` Answered: ${results.summary.answeredQuestions}`);
527
+ console.log(
528
+ ` Required Answered: ${results.summary.requiredAnswered}/${results.summary.requiredQuestions}`
529
+ );
530
+ console.log(` Time Spent: ${results.metadata.timeSpent}s`);
531
+ }
532
+ console.log("\nShutting down server...");
533
+ await server.stop();
534
+ console.log("Done!");
535
+ process.exit(0);
536
+ }
537
+ async function runMCPMode(mcpConfig, args) {
538
+ const port = Number(args.port) || mcpConfig?.port || Number(process.env.QUIZ_PORT ?? "3000");
539
+ const host = String(args.host ?? mcpConfig?.host ?? process.env.QUIZ_HOST ?? "localhost");
540
+ const quizConfigPath = args.quizConfig;
541
+ const baseUrl = `http://${host}:${port}`;
542
+ console.error(`Starting quiz server at ${baseUrl}`);
543
+ await ensureBuildExists();
544
+ const server = await createQuizServer({
545
+ port,
546
+ host,
547
+ configPath: quizConfigPath,
548
+ onReady: (url) => console.error(`Quiz server ready at ${url}`)
549
+ });
550
+ await server.start();
551
+ await waitForServer(server);
552
+ console.error("Quiz server is ready");
553
+ const mcpServer = new McpServer({ name: "quiz-mcp", version: "1.0.0" });
554
+ mcpServer.registerTool(
555
+ "get_quiz_format",
556
+ {
557
+ title: "Quiz Schema Format",
558
+ description: "Get the quiz JSON schema and examples for AI agents",
559
+ inputSchema: z.object({})
560
+ },
561
+ async () => {
562
+ const examples = [
563
+ {
564
+ type: "single",
565
+ example: {
566
+ id: "q1",
567
+ type: "single",
568
+ question: "Capital of France?",
569
+ options: ["London", "Paris"],
570
+ points: 1
571
+ },
572
+ description: "Single choice"
573
+ },
574
+ {
575
+ type: "multiple",
576
+ example: {
577
+ id: "q2",
578
+ type: "multiple",
579
+ question: "Which are programming languages?",
580
+ options: ["Python", "Java", "HTML", "JavaScript"],
581
+ points: 2
582
+ },
583
+ description: "Multiple choice"
584
+ },
585
+ {
586
+ type: "text",
587
+ example: {
588
+ id: "q3",
589
+ type: "text",
590
+ question: "Explain in your own words what is MCP"
591
+ },
592
+ description: "Open text answer"
593
+ }
594
+ ];
595
+ const payload = {
596
+ schema: QuizDataSchema.shape,
597
+ examples,
598
+ version: "1.0.0"
599
+ };
600
+ return {
601
+ content: [
602
+ {
603
+ type: "text",
604
+ text: JSON.stringify(payload, null, 2)
605
+ }
606
+ ]
607
+ };
608
+ }
609
+ );
610
+ mcpServer.registerTool(
611
+ "start_quiz",
612
+ {
613
+ title: "Start Quiz",
614
+ description: "Start a new quiz session from provided quiz data",
615
+ inputSchema: z.object({
616
+ quiz: z.union([z.string(), QuizDataSchema]),
617
+ config: QuizConfigSchema,
618
+ open: z.boolean().optional().describe("Open browser after quiz start").default(false)
619
+ })
620
+ },
621
+ async ({ quiz, config, open }) => {
622
+ let quizData;
623
+ try {
624
+ quizData = typeof quiz === "string" ? QuizDataSchema.parse(JSON.parse(quiz)) : QuizDataSchema.parse(quiz);
625
+ } catch (err) {
626
+ return {
627
+ content: [{ type: "text", text: `Invalid quiz format: ${String(err)}` }],
628
+ isError: true
629
+ };
630
+ }
631
+ try {
632
+ const response = await server.createSession(quizData, config);
633
+ const url = `${server.url}/${response.quizId}/${response.sessionId}`;
634
+ if (open) {
635
+ await openFn(url);
636
+ }
637
+ return {
638
+ content: [
639
+ {
640
+ type: "text",
641
+ text: JSON.stringify(
642
+ {
643
+ ok: true,
644
+ openUrl: url,
645
+ quizId: response.quizId,
646
+ sessionId: response.sessionId,
647
+ expiresAt: response.expiresAt
648
+ },
649
+ null,
650
+ 2
651
+ )
652
+ }
653
+ ]
654
+ };
655
+ } catch (err) {
656
+ return {
657
+ content: [
658
+ {
659
+ type: "text",
660
+ text: `Failed to create quiz session: ${err instanceof Error ? err.message : String(err)}`
661
+ }
662
+ ],
663
+ isError: true
664
+ };
665
+ }
666
+ }
667
+ );
668
+ mcpServer.registerTool(
669
+ "get_quiz_status",
670
+ {
671
+ title: "Get Quiz Status",
672
+ description: "Check the status of a quiz session",
673
+ inputSchema: z.object({
674
+ quizId: z.string(),
675
+ sessionId: z.string(),
676
+ includeAnswers: z.boolean().optional().default(false)
677
+ })
678
+ },
679
+ async ({ quizId, sessionId, includeAnswers }) => {
680
+ try {
681
+ const status = await server.getSessionStatus(quizId, sessionId, includeAnswers);
682
+ return {
683
+ content: [
684
+ {
685
+ type: "text",
686
+ text: JSON.stringify(status, null, 2)
687
+ }
688
+ ]
689
+ };
690
+ } catch (err) {
691
+ return {
692
+ content: [
693
+ {
694
+ type: "text",
695
+ text: JSON.stringify(
696
+ {
697
+ status: "error",
698
+ startedAt: "",
699
+ error: `Failed to get status: ${err instanceof Error ? err.message : String(err)}`
700
+ },
701
+ null,
702
+ 2
703
+ )
704
+ }
705
+ ],
706
+ isError: true
707
+ };
708
+ }
709
+ }
710
+ );
711
+ mcpServer.registerTool(
712
+ "cancel_quiz",
713
+ {
714
+ title: "Cancel Quiz",
715
+ description: "Cancel an active quiz session",
716
+ inputSchema: z.object({
717
+ quizId: z.string(),
718
+ sessionId: z.string(),
719
+ reason: z.string().optional()
720
+ })
721
+ },
722
+ async ({ quizId, sessionId, reason }) => {
723
+ try {
724
+ const response = await server.cancelSession(quizId, sessionId, reason);
725
+ return {
726
+ content: [
727
+ {
728
+ type: "text",
729
+ text: JSON.stringify(response, null, 2)
730
+ }
731
+ ]
732
+ };
733
+ } catch (err) {
734
+ return {
735
+ content: [
736
+ {
737
+ type: "text",
738
+ text: JSON.stringify(
739
+ {
740
+ ok: false,
741
+ message: "",
742
+ cancelledAt: "",
743
+ error: `Failed to cancel: ${err instanceof Error ? err.message : String(err)}`
744
+ },
745
+ null,
746
+ 2
747
+ )
748
+ }
749
+ ],
750
+ isError: true
751
+ };
752
+ }
753
+ }
754
+ );
755
+ const transport = new StdioServerTransport();
756
+ await mcpServer.connect(transport);
757
+ const shutdown = async (signal) => {
758
+ console.error(`
759
+ ${signal}, shutting down...`);
760
+ await server.stop();
761
+ process.exit(0);
762
+ };
763
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
764
+ process.on("SIGINT", () => shutdown("SIGINT"));
765
+ }
766
+ async function main(argv) {
767
+ const scriptName = argv[1] || "quiz";
768
+ const programName = scriptName.includes("quiz-runner-mcp") ? "quiz-runner-mcp" : scriptName.includes("quiz-runner") ? "quiz-runner" : "quiz";
769
+ const program = new Command();
770
+ program.name(programName).description("Quiz MCP - Interactive quiz runner and MCP server");
771
+ program.option("-p, --port <NUMBER>", "Server port (default: 3000)");
772
+ program.option("-h, --host <STRING>", "Server host (default: localhost)");
773
+ program.option("-v, --verbose", "Verbose logging");
774
+ const runnerCmd = program.command("run").description("Run interactive quizzes via CLI with browser interface").option("-f, --file <PATH>", "Path to quiz JSON file").option("-q, --quiz <JSON>", "Direct JSON quiz data").option("-c, --config <PATH>", "Path to configuration file").option("-o, --output <PATH>", "Output results file path").option("-w, --webhook-url <URL>", "Webhook URL for results").option("-O, --open", "Auto-open browser");
775
+ runnerCmd.action(async (opts) => {
776
+ try {
777
+ await runRunnerMode(opts);
778
+ } catch (error) {
779
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
780
+ process.exit(1);
781
+ }
782
+ });
783
+ const mcpCmd = program.command("mcp").description("Run as MCP server for AI agent integration").option("-c, --config <PATH>", "MCP configuration file path").option(
784
+ "--quiz-config <PATH>",
785
+ "Quiz configuration file path (layout, theme, language, webhook)"
786
+ ).option("-s, --storage <PATH>", "Session storage directory").option("-m, --max-sessions <NUMBER>", "Maximum concurrent sessions").option("-q, --quiz <PATH>", "Default quiz file path").option("-l, --log-level <LEVEL>", "Log level (debug, info, warn, error)");
787
+ mcpCmd.action(async (opts) => {
788
+ try {
789
+ const configPath = opts.config;
790
+ const config = loadMCPConfig(configPath);
791
+ await runMCPMode(config, opts);
792
+ } catch (err) {
793
+ console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
794
+ process.exit(1);
795
+ }
796
+ });
797
+ program.parse(argv);
798
+ }
799
+ main(process.argv).catch((err) => {
800
+ console.error("Unhandled error:", err);
801
+ process.exit(1);
802
+ });
803
+ export {
804
+ main
805
+ };