@tinyweb_dev/oe-exam-sdk 0.1.19 → 0.1.20

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tinyweb_dev/oe-exam-sdk
2
2
 
3
- SDK tái sử dụng cho hệ thống OE Exam: màn tạo đề, quản lý câu hỏi, làm bài, xem kết quả theme giao diện.
3
+ SDK tái sử dụng cho hệ thống OE Exam: màn tạo đề, quản lý câu hỏi, làm bài, xem kết quả, theme giao diện và MCP tools.
4
4
 
5
5
  ## Cài đặt
6
6
 
@@ -91,6 +91,7 @@ export function ResultReviewPage({ attemptId, token, router }) {
91
91
  @tinyweb_dev/oe-exam-sdk # export tổng hợp thường dùng
92
92
  @tinyweb_dev/oe-exam-sdk/api # API adapters, constants, types
93
93
  @tinyweb_dev/oe-exam-sdk/shared # shared constants/types/helpers
94
+ @tinyweb_dev/oe-exam-sdk/mcp # MCP server entry và tools
94
95
  @tinyweb_dev/oe-exam-sdk/components # shared UI components
95
96
  @tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài / vào phòng
96
97
  @tinyweb_dev/oe-exam-sdk/components/exams/entry # màn vào phòng (entry) + state machine
@@ -237,16 +238,16 @@ Các report shell, renderer, transform helper, polling/status helper và result-
237
238
 
238
239
  ## MCP server
239
240
 
240
- MCP đã tách sang package riêng:
241
+ SDK expose bin:
241
242
 
242
243
  ```bash
243
- npm install @tinyweb_dev/oe-exam-mcp-server
244
+ oe-exam-mcp
244
245
  ```
245
246
 
246
247
  Chạy bằng PAT/API token. Nếu token có dạng `oeexam_...`, MCP tự gửi qua `X-API-Key`; token khác sẽ gửi qua `Authorization: Bearer`.
247
248
 
248
249
  ```bash
249
- OE_API_URL=http://localhost:3000 OE_API_TOKEN=... npx @tinyweb_dev/oe-exam-mcp-server
250
+ OE_API_URL=http://localhost:3000 OE_API_TOKEN=... npx oe-exam-mcp
250
251
  ```
251
252
 
252
253
  Tools hiện có:
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * as Api from './api';
2
2
  export * as Components from './components';
3
+ export * as Mcp from './mcp';
3
4
  export * as Exams from './components/exams';
4
5
  export * as Questions from './components/questions';
5
6
  export * as Results from './components/results';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*as Api from"./api";export*as Components from"./components";export*as Exams from"./components/exams";export*as Questions from"./components/questions";export*as Results from"./components/results";export*as Themes from"./components/themes";export*as Shared from"./shared";export{buildExamTemplatePayload,createContestRoomsApi,createExamCreateApi,createExamEntryApi,createExamQuestionsApi,createExamQuestionsFetchApi,createExamTakingApi,createMockContestRoomsApi,createMockCreateExamApi,createMockExamEntryApi,createMockExamQuestionsApi,createMockExamTakingApi,createMockResultReviewApi,createResultReviewApi}from"./api";export{CreateExamPageContainer,ExamPreviewDialog,ExamQuestionsPageContainer,ExamTakingPageContainer,ExamViewDialog,StudentContestRoomsPageContainer,StudentExamEntryPageContainer}from"./components/exams";export{ResultReviewPageContainer}from"./components/results";export{QuestionCreator}from"./components/questions/creator/QuestionCreator";export{QuestionGroupCreator}from"./components/questions/creator/QuestionGroupCreator";export{renderDedicatedComponent}from"./components/questions/creator/dedicated-component-router";export{questionTypeRegistry}from"./components/questions/creator/question-type-registry";export{QUESTION_TYPE_CONFIG,QUESTION_TYPES_WITH_DEDICATED_COMPONENTS}from"./components/questions/_shared/config/question-types.config";export{validateQuestion}from"./components/questions/_shared/utils/question-validation";export{buildAnswerPayload}from"./components/questions/_shared/utils/answer-builder";export*from"./components/questions/_shared/types";
1
+ export*as Api from"./api";export*as Components from"./components";export*as Mcp from"./mcp";export*as Exams from"./components/exams";export*as Questions from"./components/questions";export*as Results from"./components/results";export*as Themes from"./components/themes";export*as Shared from"./shared";export{buildExamTemplatePayload,createContestRoomsApi,createExamCreateApi,createExamEntryApi,createExamQuestionsApi,createExamQuestionsFetchApi,createExamTakingApi,createMockContestRoomsApi,createMockCreateExamApi,createMockExamEntryApi,createMockExamQuestionsApi,createMockExamTakingApi,createMockResultReviewApi,createResultReviewApi}from"./api";export{CreateExamPageContainer,ExamPreviewDialog,ExamQuestionsPageContainer,ExamTakingPageContainer,ExamViewDialog,StudentContestRoomsPageContainer,StudentExamEntryPageContainer}from"./components/exams";export{ResultReviewPageContainer}from"./components/results";export{QuestionCreator}from"./components/questions/creator/QuestionCreator";export{QuestionGroupCreator}from"./components/questions/creator/QuestionGroupCreator";export{renderDedicatedComponent}from"./components/questions/creator/dedicated-component-router";export{questionTypeRegistry}from"./components/questions/creator/question-type-registry";export{QUESTION_TYPE_CONFIG,QUESTION_TYPES_WITH_DEDICATED_COMPONENTS}from"./components/questions/_shared/config/question-types.config";export{validateQuestion}from"./components/questions/_shared/utils/question-validation";export{buildAnswerPayload}from"./components/questions/_shared/utils/answer-builder";export*from"./components/questions/_shared/types";
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ declare const schema: z.ZodObject<{
3
+ OE_API_URL: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
4
+ OE_API_TOKEN: z.ZodString;
5
+ OE_HTTP_TIMEOUT_MS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
6
+ }, z.core.$strip>;
7
+ export type McpConfig = z.infer<typeof schema> & {
8
+ packageName: string;
9
+ packageVersion: string;
10
+ };
11
+ export declare function loadMcpConfig(): McpConfig;
12
+ export {};
@@ -0,0 +1 @@
1
+ import{readFileSync as e}from"node:fs";import{dirname as r,join as o}from"node:path";import{fileURLToPath as n}from"node:url";import{z as t}from"zod";const s=function(){try{const t=r(n(import.meta.url));return JSON.parse(e(o(t,"..","..","package.json"),"utf8"))}catch{return{name:"@tinyweb_dev/oe-exam-sdk",version:"0.0.0"}}}(),a=t.object({OE_API_URL:t.string().url().transform(e=>e.replace(/\/+$/,"")),OE_API_TOKEN:t.string().min(1),OE_HTTP_TIMEOUT_MS:t.coerce.number().int().positive().default(15e3)});let i=null;export function loadMcpConfig(){if(i)return i;const e=a.safeParse(process.env);if(!e.success){const r=e.error.issues.map(e=>` • ${e.path.join(".")||"<root>"}: ${e.message}`).join("\n");console.error(`\n[oe-exam-mcp] Configuration error:\n${r}\n\nRequired: OE_API_URL and OE_API_TOKEN.\n`),process.exit(1)}return i={...e.data,packageName:s.name,packageVersion:s.version},i}
@@ -0,0 +1,6 @@
1
+ export interface McpRequestOptions<TBody = unknown> {
2
+ method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
3
+ url: string;
4
+ body?: TBody;
5
+ }
6
+ export declare function mcpRequest<T>(opts: McpRequestOptions): Promise<T>;
@@ -0,0 +1 @@
1
+ import{ErrorCode as t,McpError as o}from"@modelcontextprotocol/sdk/types.js";import{loadMcpConfig as r}from"./config.js";export async function mcpRequest(n){const s=r(),a=new AbortController,i=setTimeout(()=>a.abort(),s.OE_HTTP_TIMEOUT_MS);try{const r=await fetch(`${s.OE_API_URL}${n.url}`,{method:n.method,signal:a.signal,headers:{Accept:"application/json","Content-Type":"application/json",...e(s.OE_API_TOKEN)},body:void 0===n.body?void 0:JSON.stringify(n.body)}),i=await r.text(),c=i?JSON.parse(i):void 0;if(!r.ok){const e=function(t){if(!t||"object"!=typeof t)return;const o=t.message;return Array.isArray(o)?o.join("; "):"string"==typeof o?o:void 0}(c)??r.statusText,s=400===r.status||404===r.status||422===r.status?t.InvalidParams:t.InternalError;throw new o(s,`[${n.method} ${n.url}] ${r.status}: ${e}`)}return c}catch(r){if(r instanceof o)throw r;throw new o(t.InternalError,r instanceof Error?r.message:"Unknown HTTP error")}finally{clearTimeout(i)}}function e(t){return t?t.startsWith("oeexam_")?{"X-API-Key":t}:{Authorization:`Bearer ${t}`}:{}}
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ export declare function startOeExamMcpServer(): Promise<void>;
3
+ export { loadMcpConfig } from './config.js';
4
+ export { registerAllMcpTools } from './tools/index.js';
5
+ export { registerExamTools } from './tools/exams.js';
6
+ export { registerResultTools } from './tools/results.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as o}from"@modelcontextprotocol/sdk/server/stdio.js";import{loadMcpConfig as r}from"./config.js";import{registerAllMcpTools as s}from"./tools/index.js";export async function startOeExamMcpServer(){const t=r(),a=new e({name:t.packageName,version:t.packageVersion},{capabilities:{tools:{},logging:{}},instructions:"OE Exam MCP server. Use exam_json_template_generate then exam_create_from_json to create an exam, and exam_result_get to view a result review."});s(a),await a.connect(new o);const c=async()=>{await a.close().catch(e=>console.error("[oe-exam-mcp] server.close failed",e)),process.exit(0)};process.on("SIGINT",()=>{c()}),process.on("SIGTERM",()=>{c()})}import.meta.url===`file://${process.argv[1]}`&&startOeExamMcpServer().catch(e=>{console.error("[oe-exam-mcp] Fatal startup error",e),process.exit(1)});export{loadMcpConfig}from"./config.js";export{registerAllMcpTools}from"./tools/index.js";export{registerExamTools}from"./tools/exams.js";export{registerResultTools}from"./tools/results.js";
@@ -0,0 +1,4 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare function buildToolResult<T>(value: T): CallToolResult;
3
+ export declare function wrapHandler<TInput extends Record<string, unknown>>(toolName: string, fn: (input: TInput) => Promise<CallToolResult>): (input: TInput) => Promise<CallToolResult>;
4
+ export declare function unwrapData<T>(value: unknown): T;
@@ -0,0 +1 @@
1
+ import{ErrorCode as t,McpError as r}from"@modelcontextprotocol/sdk/types.js";export function buildToolResult(t){return{content:[{type:"text",text:"string"==typeof t?t:JSON.stringify(t,null,2)}],...t&&"object"==typeof t?{structuredContent:t}:{}}}export function wrapHandler(e,o){return async n=>{try{return await o(n)}catch(o){if(o instanceof r)throw o;throw console.error(`[oe-exam-mcp] ${e} failed`,o),new r(t.InternalError,o instanceof Error?o.message:"Unknown error")}}}export function unwrapData(t){return t&&"object"==typeof t&&"data"in t?t.data:t}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerExamTools(server: McpServer): void;
@@ -0,0 +1 @@
1
+ import{mkdir as e,writeFile as t}from"node:fs/promises";import{dirname as n,resolve as i}from"node:path";import{z as a}from"zod";import{mcpRequest as o}from"../http-client.js";import{buildToolResult as r,unwrapData as s,wrapHandler as l}from"./_helpers.js";const p=a.record(a.string(),a.unknown()).describe("Full API question payload or short form: { question, options, answer, points?, partId?, partNo? }."),u=(a.record(a.string(),a.unknown()).describe("Part payload: { name?, partId?, questions: [...] }."),a.record(a.string(),a.unknown()).describe("Template payload accepted by PATCH /manage/exams/:id/template.")),m=a.enum(["PRE_A1","A1","A2","B1","B2","C1","C2"]);export function registerExamTools(g){g.registerTool("exam_templates_search",{title:"Search exam templates",description:"Search available OE Exam templates. Use this before exam_create_with_questions to choose examTemplateId.",inputSchema:{search:a.string().optional().describe("Template name keyword."),page:a.number().int().positive().default(1).describe("1-based page number."),limit:a.number().int().positive().max(50).default(9).describe("Page size sent as API size.")}},l("exam_templates_search",async e=>{const t=new URLSearchParams({page:String(e.page??1),size:String(e.limit??9)});e.search&&t.set("filter",`name:like:${e.search}`);const n=await o({method:"GET",url:`/api/v1/exam-templates?${t}`});return r({templates:k(n)})})),g.registerTool("exam_template_get",{title:"Get exam template",description:"Get one OE Exam template by id. The returned templatePayload can be passed as template to exam_create_with_questions.",inputSchema:{id:a.string().min(1).describe("Template id from exam_templates_search.")}},l("exam_template_get",async e=>{const t=s(await o({method:"GET",url:`/api/v1/exam-templates/${encodeURIComponent(String(e.id))}`}));return r({template:t,templatePayload:P(t)})})),g.registerTool("exam_question_template_get",{title:"Get question payload template",description:"Return easy-to-fill question JSON templates for exam_create_with_questions. Use this before creating questions if you need the expected shape.",inputSchema:{templateId:a.string().min(1).optional().describe("Template id. If provided, generate question payloads from template.parts."),type:a.string().default("CHOOSE_THE_CORRECT_ANSWER").describe("Fallback question type when templateId is omitted."),count:a.number().int().positive().max(100).default(1).describe("Fallback count when templateId is omitted."),partId:a.union([a.string(),a.number()]).default(1),partNo:a.number().int().positive().default(1),skill:a.string().default("READING")}},l("exam_question_template_get",async e=>{const t="string"==typeof e.templateId?s(await o({method:"GET",url:`/api/v1/exam-templates/${encodeURIComponent(e.templateId)}`})):void 0;return r({usage:"Pass questions directly into exam_create_with_questions.questions. Fill content/correctAnswer for each generated API-format payload.",templateId:t?.id,questions:t?b(t):S(e)})})),g.registerTool("exam_json_template_generate",{title:"Get exam JSON template",description:"Generate a user-friendly examJson skeleton from templateTitle or templateId. Edit this JSON, then pass it to exam_create_from_json.",inputSchema:{templateTitle:a.string().optional().describe("Template title to search, e.g. Achievers 2026 Round 1."),templateId:a.string().optional().describe("Exact template id. Preferred when known."),name:a.string().optional().describe("Exam name to put in the JSON."),examLevel:m.optional().describe("Optional exam level for the generated JSON."),difficulty:a.enum(["EASY","MEDIUM","HARD"]).optional().describe("Optional difficulty for the generated JSON."),durationMinutes:a.number().int().positive().optional().describe("Optional duration for the generated JSON.")}},l("exam_json_template_generate",async a=>{const o=await f({templateId:a.templateId,templateTitle:a.templateTitle}),s=function(e,t){const n=Array.isArray(e.parts)?e.parts.filter(c):[];return{name:v(t.name,`${e.title??e.name??"OE Exam"} Draft`),templateId:e.id,templateTitle:e.title??e.name,totalScore:O(e.totalScore??e.total_questions,y(n)),examLevel:_(t.examLevel)??_(e.examLevel??e.exam_level??e.level)??"A1",difficulty:E(t.difficulty)??"MEDIUM",durationMinutes:O(t.durationMinutes,45),isComplete:!1,isActive:!1,parts:n.map((e,t)=>function(e,t){const n=O(e.startIndex,0),i=O(e.questionCount,O(e.endIndex,n)-n+1);return{id:e.id??t+1,name:e.name??`Part ${t+1}`,questionType:v(e.questionType,"CHOOSE_THE_CORRECT_ANSWER"),skill:v(e.skill,"READING"),questions:Array.from({length:Math.max(i,0)},(t,i)=>function(e,t){const n=v(e.questionType,"CHOOSE_THE_CORRECT_ANSWER"),i=(Array.isArray(e.defaultValue)&&e.defaultValue.length>0?e.defaultValue:Array.from({length:O(e.optionCount,4)},(e,t)=>String.fromCharCode(65+t))).map(String);return n.includes("FILL")||n.includes("WRITE")?{question:`Question ${t}`,answer:""}:n.includes("SPEAK")?{prompt:`Question ${t}`,expectedAnswer:""}:{question:`Question ${t}`,options:i,answer:i[0]??"A"}}(e,n+i+1))}}(e,t))}}(o,a),l=JSON.stringify(s,null,2),p=i(`${u=String(a.name??o.title??o.name??"exam"),u.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,80)||"exam"}.json`);var u;return await e(n(p),{recursive:!0}),await t(p,`${l}\n`,"utf8"),r({usage:"Edit filePath, then call exam_create_from_json with the edited JSON string.",templateId:o.id,templateTitle:o.title??o.name,filePath:p,examJson:s,examJsonString:l})})),g.registerTool("exam_create_from_json",{title:"Create exam from JSON",description:"Create an exam from a user-friendly JSON payload grouped by parts. Searches template by title, patches template, maps questions to template.parts, then creates questions.",inputSchema:{examJson:a.string().optional().describe("JSON string with { name, templateTitle/templateId, parts: [{ questions: [...] }] }. Use this for copy/paste."),exam:a.record(a.string(),a.unknown()).optional().describe("Same payload as object if the client supports object arguments.")}},l("exam_create_from_json",async e=>{const t=function(e,t){if(c(t))return t;if("string"!=typeof e||!e.trim())throw new Error("examJson or exam is required");const n=JSON.parse(e);if(!c(n))throw new Error("examJson must be a JSON object");return n}(e.examJson,e.exam),n=await f(t),i=function(e,t){const n=Array.isArray(t.parts)?t.parts.filter(c):[],i=Array.isArray(e.parts)?e.parts.filter(c):[{questions:e.questions}];return i.flatMap((e,t)=>{const i=function(e,t,n){const i=A(e.partId??e.id),a=A(e.name);return t.find(e=>String(e.id)===i)??t.find(e=>"string"==typeof e.name&&e.name===a)??t[n]??{id:n+1,questionType:"CHOOSE_THE_CORRECT_ANSWER",skill:"READING",optionCount:4}}(e,n,t);return(Array.isArray(e.questions)?e.questions.filter(c):[]).map((e,n)=>function(e,t,n,i){const a=O(t.startIndex,0),o=w({questionType:v(e.questionType??e.type??t.questionType,"CHOOSE_THE_CORRECT_ANSWER"),questionNumber:O(e.questionNumber,a+i+1),partId:String(e.partId??t.id??n+1),partNo:O(e.partNo,n+1),sectionId:R(e.sectionId??t.sectionId),skill:v(e.questionSkillType??e.skill??t.skill,"READING"),optionCount:O(t.optionCount,4),defaultValue:Array.isArray(t.defaultValue)?t.defaultValue:void 0});return q(function(e){const t=c(e.content)?{...e.content}:{};"string"==typeof e.question&&(t.question=e.question);"string"==typeof e.text&&(t.question=e.text);Array.isArray(e.options)&&(t.optionType=t.optionType??"text",t.options=e.options);return d({...e,content:c(t)?t:e.content,correctAnswer:void 0===e.answer&&void 0===e.expectedAnswer?e.correctAnswer:x(e.questionType,e.answer??e.expectedAnswer,t.options)})}({...o,...e}),i)}(e,i,t,n))})}(t,n),a=function(e,t,n){return{exam:d({name:v(e.name,"New OE Exam"),code:A(e.code)??h(v(e.name,"New OE Exam")),totalScore:O(e.totalScore,n.length||100),description:A(e.description)??A(e.templateTitle)??"",isComplete:T(e.isComplete,!1),isActive:T(e.isActive,!1),examTemplateId:t,examLevel:_(e.examLevel),difficulty:E(e.difficulty),durationMinutes:R(e.durationMinutes)})}}(t,n.id,i),l=s(await o({method:"POST",url:"/api/v1/manage/exams",body:a.exam}));await o({method:"PATCH",url:`/api/v1/manage/exams/${encodeURIComponent(l.id)}/template`,body:{template:P(n)}});const p=0===i.length?null:await o({method:"POST",url:"/api/v1/manage/questions/batch",body:{examId:l.id,questions:i}});return r({exam:l,templateId:n.id,questions:p,generatedQuestionCount:i.length})})),g.registerTool("exam_create_with_questions",{title:"Create exam with questions",description:"Create an exam, optionally patch template, then bulk create questions via /api/v1/manage/questions/batch. If you do not know examTemplateId/template, call exam_templates_search then exam_template_get first.",inputSchema:{name:a.string().min(1),code:a.string().min(1).optional(),examLevel:m.optional().describe("Valid exam level only: PRE_A1, A1, A2, B1, B2, C1, C2. Do not use template title/id here."),difficulty:a.enum(["EASY","MEDIUM","HARD"]).optional(),totalScore:a.number().nonnegative().optional(),durationMinutes:a.number().int().positive().optional(),description:a.string().optional(),isComplete:a.boolean().optional(),isActive:a.boolean().optional(),examTemplateId:a.string().min(1).optional(),template:u.optional(),questions:a.array(p).default([])}},l("exam_create_with_questions",async e=>{const{template:t,questions:n,...i}=e,a=s(await o({method:"POST",url:"/api/v1/manage/exams",body:d(i)})),l=c(t)?t:await async function(e){if("string"!=typeof e||!e)return;const t=s(await o({method:"GET",url:`/api/v1/exam-templates/${encodeURIComponent(e)}`}));return P(t)}(i.examTemplateId);c(l)&&await o({method:"PATCH",url:`/api/v1/manage/exams/${encodeURIComponent(a.id)}/template`,body:{template:l}});const p=n.map(q),u=0===p.length?null:await o({method:"POST",url:"/api/v1/manage/questions/batch",body:{examId:a.id,questions:p}});return r({exam:a,questions:u})}))}function d(e){return Object.fromEntries(Object.entries(e).filter(([,e])=>void 0!==e))}function c(e){return Boolean(e&&"object"==typeof e&&!Array.isArray(e)&&Object.keys(e).length>0)}async function f(e){const t=A(e.templateId);if(t)return s(await o({method:"GET",url:`/api/v1/exam-templates/${encodeURIComponent(t)}`}));const n=A(e.templateTitle);if(!n)throw new Error("templateTitle or templateId is required");const i=new URLSearchParams({page:"1",size:"10",filter:`name:like:${n}`}),a=k(await o({method:"GET",url:`/api/v1/exam-templates?${i}`})),r=a.find(e=>(e.title??e.name)===n),l=r??a[0];if(!l?.id)throw new Error(`No exam template found for title: ${n}`);return s(await o({method:"GET",url:`/api/v1/exam-templates/${encodeURIComponent(l.id)}`}))}function y(e){return e.reduce((e,t)=>e+O(t.questionCount,0),0)}function x(e,t,n){if(void 0===t)return;const i=String(e??"");return i.includes("FILL")||i.includes("WRITE")?{answers:[String(t)]}:i.includes("SPEAK")?{expectedAnswer:String(t)}:{answer:g(t,n)}}function g(e,t){if("string"==typeof e&&Array.isArray(t)){const n=t.map(String).findIndex(t=>t===e);if(n>=0)return n}return I(e)}function h(e){return e.toUpperCase().replace(/[^A-Z0-9]+/g,"_").replace(/^_|_$/g,"").slice(0,40)||"OE_EXAM"}function A(e){return"string"==typeof e&&e?e:void 0}function T(e,t){return"boolean"==typeof e?e:t}function _(e){return"string"==typeof e&&m.safeParse(e).success?e:void 0}function E(e){return"string"==typeof e&&["EASY","MEDIUM","HARD"].includes(e)?e:void 0}function q(e,t){if(e.questionType)return d({...e,partId:void 0===e.partId?void 0:String(e.partId),partNo:e.partNo??1,questionNumber:e.questionNumber??t+1,points:e.points??1,addToQuestionBank:e.addToQuestionBank??!1,isExample:e.isExample??!1});const n=Array.isArray(e.options)?e.options:["A","B","C","D"];return d({questionType:e.type??"CHOOSE_THE_CORRECT_ANSWER",content:e.content??{question:String(e.question??e.text??`Question ${t+1}`),optionType:"text",options:n},correctAnswer:e.correctAnswer??{answer:I(e.answer??e.correctAnswerIndex??0)},points:e.points??1,explanation:e.explanation??"",questionNumber:e.questionNumber??t+1,partId:String(e.partId??1),partNo:e.partNo??1,sectionId:e.sectionId,questionSkillType:e.questionSkillType??e.skill??"READING",addToQuestionBank:e.addToQuestionBank??!1,isExample:e.isExample??!1})}function I(e){if("number"==typeof e)return e;if("string"==typeof e&&/^[A-E]$/i.test(e))return e.toUpperCase().charCodeAt(0)-65;const t=Number(e);return Number.isFinite(t)?t:0}function S(e){return Array.from({length:Number(e.count??1)},(t,n)=>w({questionType:String(e.type??"CHOOSE_THE_CORRECT_ANSWER"),questionNumber:n+1,partId:String(e.partId??1),partNo:Number(e.partNo??1),skill:String(e.skill??"READING")}))}function b(e){return(Array.isArray(e.parts)?e.parts.filter(c):[]).flatMap((e,t)=>{const n=O(e.startIndex,0),i=O(e.questionCount,O(e.endIndex,n)-n+1);return Array.from({length:Math.max(i,0)},(i,a)=>w({questionType:v(e.questionType,"CHOOSE_THE_CORRECT_ANSWER"),questionNumber:n+a+1,partId:String(e.id??t+1),partNo:t+1,sectionId:R(e.sectionId),skill:v(e.skill,"READING"),optionCount:O(e.optionCount,4),defaultValue:Array.isArray(e.defaultValue)?e.defaultValue:void 0}))})}function w(e){return d({questionType:e.questionType,content:C(e.questionType,e.questionNumber,e.optionCount,e.defaultValue),correctAnswer:N(e.questionType),points:1,explanation:"",questionNumber:e.questionNumber,partId:e.partId,partNo:e.partNo,sectionId:e.sectionId,questionSkillType:e.skill,addToQuestionBank:!1,isExample:!1})}function C(e,t,n=4,i){const a=(i?.length?i:Array.from({length:n},(e,t)=>String.fromCharCode(65+t))).map(String);return e.includes("FILL")||e.includes("WRITE")?{question:`Question ${t}`,text:""}:e.includes("SPEAK")?{prompt:`Question ${t}`}:e.includes("LISTEN")?{question:`Question ${t}`,audioUrl:"",optionType:"text",options:a}:{question:`Question ${t}`,optionType:"text",options:a}}function N(e){return e.includes("FILL")||e.includes("WRITE")?{answers:[""]}:e.includes("SPEAK")?{expectedAnswer:""}:{answer:0}}function v(e,t){return"string"==typeof e&&e?e:t}function O(e,t){const n=Number(e);return Number.isFinite(n)?n:t}function R(e){const t=Number(e);return Number.isFinite(t)?t:void 0}function k(e){const t=s(e);return Array.isArray(t)?t:t&&"object"==typeof t&&"data"in t&&Array.isArray(t.data)?t.data:[]}function P(e){const t=Array.isArray(e.parts)?e.parts:[];return d({id:e.id,title:e.title??e.name,subTitle:e.subTitle??e.sub_title??"",theme:e.theme,totalQuestions:e.totalQuestions??e.total_questions,totalParts:e.totalParts??e.total_parts??t.length,description:e.description??"",examLevel:e.examLevel??e.exam_level??e.level??"",type:e.type??null,parts:t})}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerAllMcpTools(server: McpServer): void;
@@ -0,0 +1 @@
1
+ import{registerExamTools as o}from"./exams.js";import{registerResultTools as r}from"./results.js";export function registerAllMcpTools(s){o(s),r(s)}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerResultTools(server: McpServer): void;
@@ -0,0 +1 @@
1
+ import{z as t}from"zod";import{mcpRequest as e}from"../http-client.js";import{buildToolResult as n,wrapHandler as o}from"./_helpers.js";export function registerResultTools(r){r.registerTool("exam_result_get",{title:"Get exam result review",description:"Fetch student exam result review by attemptId. Optional token supports public result review links.",inputSchema:{attemptId:t.string().min(1),token:t.string().min(1).optional(),includeAttempt:t.boolean().default(!0)}},o("exam_result_get",async({attemptId:t,token:o,includeAttempt:r})=>{const i=o?`?token=${encodeURIComponent(String(o))}`:"",s=await e({method:"GET",url:`/api/v1/student/results/${encodeURIComponent(String(t))}${i}`}),l=!1===r?null:await e({method:"GET",url:`/api/v1/student/attempts/${encodeURIComponent(String(t))}${i}`});return n({result:s,attempt:l})}))}
package/package.json CHANGED
@@ -1,155 +1,163 @@
1
- {
2
- "name": "@tinyweb_dev/oe-exam-sdk",
3
- "version": "0.1.19",
4
- "description": "Reusable OceanEdu question components.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "files": [
10
- "dist",
11
- "README.md"
12
- ],
13
- "sideEffects": [
14
- "**/*.css"
15
- ],
16
- "scripts": {
17
- "clean": "rm -rf dist",
18
- "build:compile": "tsc -p tsconfig.build.json",
19
- "build:alias": "tsc-alias -p tsconfig.build.json",
20
- "build:ts": "npm run build:compile && npm run build:assets && npm run build:alias",
21
- "build:assets": "for d in cambridge-yle english-certification summer-sky; do [ -d src/components/themes/$d/assets ] && cp -R src/components/themes/$d/assets dist/components/themes/$d/ || true; done; [ -d src/components/shared/audio/assets ] && cp -R src/components/shared/audio/assets dist/components/shared/audio/ || true; [ -d src/components/results/report/assets ] && mkdir -p dist/components/results/report && cp -R src/components/results/report/assets dist/components/results/report/ || true; (cd src && find . -name '*.module.css' -exec cp --parents {} ../dist/ \\;); cp src/global.d.ts dist/global.d.ts",
22
- "minify:js": "find dist -name '*.js' -type f -exec sh -c 'for f do terser \"$f\" --compress directives=false --mangle --module --comments false --output \"$f\"; done' sh {} +",
23
- "build": "npm run clean && npm run build:ts && npm run minify:js",
24
- "pack:local": "npm pack",
25
- "release": "npm run release:patch",
26
- "release:patch": "npm version patch --no-git-tag-version && npm run release:push",
27
- "release:minor": "npm version minor --no-git-tag-version && npm run release:push",
28
- "release:major": "npm version major --no-git-tag-version && npm run release:push",
29
- "release:push": "npm run build && npm pack --dry-run && VERSION=$(node -p \"require('./package.json').version\") && git add package.json package-lock.json && git commit -m \"chore(sdk): release v$VERSION\" && git tag \"sdk-v$VERSION\" && git push origin HEAD && git push origin \"sdk-v$VERSION\"",
30
- "prepack": "npm run build"
31
- },
32
- "exports": {
33
- ".": {
34
- "types": "./dist/index.d.ts",
35
- "import": "./dist/index.js"
36
- },
37
- "./api": {
38
- "types": "./dist/api/index.d.ts",
39
- "import": "./dist/api/index.js"
40
- },
41
- "./components": {
42
- "types": "./dist/components/index.d.ts",
43
- "import": "./dist/components/index.js"
44
- },
45
- "./components/exams": {
46
- "types": "./dist/components/exams/index.d.ts",
47
- "import": "./dist/components/exams/index.js"
48
- },
49
- "./components/exams/take": {
50
- "types": "./dist/components/exams/take/index.d.ts",
51
- "import": "./dist/components/exams/take/index.js"
52
- },
53
- "./components/exams/entry": {
54
- "types": "./dist/components/exams/entry/index.d.ts",
55
- "import": "./dist/components/exams/entry/index.js"
56
- },
57
- "./components/exams/rooms": {
58
- "types": "./dist/components/exams/rooms/index.d.ts",
59
- "import": "./dist/components/exams/rooms/index.js"
60
- },
61
- "./components/questions": {
62
- "types": "./dist/components/questions/index.d.ts",
63
- "import": "./dist/components/questions/index.js"
64
- },
65
- "./components/themes": {
66
- "types": "./dist/components/themes/index.d.ts",
67
- "import": "./dist/components/themes/index.js"
68
- },
69
- "./components/results": {
70
- "types": "./dist/components/results/index.d.ts",
71
- "import": "./dist/components/results/index.js"
72
- },
73
- "./components/questions/*": {
74
- "types": "./dist/components/questions/*.d.ts",
75
- "import": "./dist/components/questions/*.js"
76
- },
77
- "./shared": {
78
- "types": "./dist/shared/index.d.ts",
79
- "import": "./dist/shared/index.js"
80
- },
81
- "./*": {
82
- "types": "./dist/components/questions/*.d.ts",
83
- "import": "./dist/components/questions/*.js"
84
- },
85
- "./package.json": "./package.json"
86
- },
87
- "publishConfig": {
88
- "access": "public"
89
- },
90
- "peerDependencies": {
91
- "next": ">=15",
92
- "react": ">=19",
93
- "react-dom": ">=19"
94
- },
95
- "devDependencies": {
96
- "@types/node": "^25.9.3",
97
- "terser": "^5.48.0",
98
- "tsc-alias": "^1.8.17",
99
- "typescript": "~5.9.2"
100
- },
101
- "dependencies": {
102
- "@dnd-kit/core": "^6.3.1",
103
- "@dnd-kit/sortable": "^10.0.0",
104
- "@dnd-kit/utilities": "^3.2.2",
105
- "@fortawesome/free-solid-svg-icons": "^7.3.1",
106
- "@fortawesome/react-fontawesome": "^3.3.1",
107
- "@hookform/resolvers": "^5.2.2",
108
- "@radix-ui/react-alert-dialog": "^1.1.15",
109
- "@radix-ui/react-avatar": "^1.1.10",
110
- "@radix-ui/react-checkbox": "^1.3.3",
111
- "@radix-ui/react-collapsible": "^1.1.12",
112
- "@radix-ui/react-dialog": "^1.1.15",
113
- "@radix-ui/react-dropdown-menu": "^2.1.16",
114
- "@radix-ui/react-hover-card": "^1.1.15",
115
- "@radix-ui/react-label": "^2.1.7",
116
- "@radix-ui/react-popover": "^1.1.15",
117
- "@radix-ui/react-progress": "^1.1.7",
118
- "@radix-ui/react-radio-group": "^1.3.8",
119
- "@radix-ui/react-scroll-area": "^1.2.10",
120
- "@radix-ui/react-select": "^2.2.6",
121
- "@radix-ui/react-separator": "^1.1.7",
122
- "@radix-ui/react-slot": "^1.2.3",
123
- "@radix-ui/react-switch": "^1.2.6",
124
- "@radix-ui/react-tabs": "^1.1.13",
125
- "@radix-ui/react-tooltip": "^1.2.8",
126
- "@tanstack/react-query": "^5.90.2",
127
- "@tiptap/extension-table": "^3.22.3",
128
- "@tiptap/extension-table-cell": "^3.22.3",
129
- "@tiptap/extension-table-header": "^3.22.3",
130
- "@tiptap/extension-table-row": "^3.22.3",
131
- "@tiptap/extension-text-align": "^3.22.3",
132
- "@tiptap/pm": "^3.22.3",
133
- "@tiptap/react": "^3.22.3",
134
- "@tiptap/starter-kit": "^3.22.3",
135
- "@types/qrcode": "^1.5.6",
136
- "@xyflow/react": "^12.8.6",
137
- "axios": "^1.12.2",
138
- "class-variance-authority": "^0.7.1",
139
- "clsx": "^2.1.1",
140
- "cmdk": "^1.1.1",
141
- "date-fns": "^4.1.0",
142
- "embla-carousel-react": "^8.6.0",
143
- "howler": "^2.2.4",
144
- "lucide-react": "^0.544.0",
145
- "qrcode": "^1.5.4",
146
- "radix-ui": "^1.4.3",
147
- "react-day-picker": "^9.14.0",
148
- "react-hook-form": "^7.65.0",
149
- "sonner": "^2.0.7",
150
- "tailwind-merge": "^3.3.1",
151
- "vaul": "^1.1.2",
152
- "zod": "^4.1.11",
153
- "zustand": "^5.0.8"
154
- }
155
- }
1
+ {
2
+ "name": "@tinyweb_dev/oe-exam-sdk",
3
+ "version": "0.1.20",
4
+ "description": "Reusable OceanEdu question components.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "oe-exam-mcp": "./dist/mcp/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "sideEffects": [
17
+ "**/*.css"
18
+ ],
19
+ "scripts": {
20
+ "clean": "rm -rf dist",
21
+ "build:compile": "tsc -p tsconfig.build.json",
22
+ "build:alias": "tsc-alias -p tsconfig.build.json",
23
+ "build:ts": "npm run build:compile && npm run build:assets && npm run build:alias",
24
+ "build:assets": "for d in cambridge-yle english-certification summer-sky; do [ -d src/components/themes/$d/assets ] && cp -R src/components/themes/$d/assets dist/components/themes/$d/ || true; done; [ -d src/components/shared/audio/assets ] && cp -R src/components/shared/audio/assets dist/components/shared/audio/ || true; [ -d src/components/results/report/assets ] && mkdir -p dist/components/results/report && cp -R src/components/results/report/assets dist/components/results/report/ || true; (cd src && find . -name '*.module.css' -exec cp --parents {} ../dist/ \\;); cp src/global.d.ts dist/global.d.ts",
25
+ "minify:js": "find dist -name '*.js' -type f -exec sh -c 'for f do terser \"$f\" --compress directives=false --mangle --module --comments false --output \"$f\"; done' sh {} +",
26
+ "build": "npm run clean && npm run build:ts && npm run minify:js",
27
+ "pack:local": "npm pack",
28
+ "release": "npm run release:patch",
29
+ "release:patch": "npm version patch --no-git-tag-version && npm run release:push",
30
+ "release:minor": "npm version minor --no-git-tag-version && npm run release:push",
31
+ "release:major": "npm version major --no-git-tag-version && npm run release:push",
32
+ "release:push": "npm run build && npm pack --dry-run && VERSION=$(node -p \"require('./package.json').version\") && git add package.json package-lock.json && git commit -m \"chore(sdk): release v$VERSION\" && git tag \"sdk-v$VERSION\" && git push origin HEAD && git push origin \"sdk-v$VERSION\"",
33
+ "prepack": "npm run build"
34
+ },
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./api": {
41
+ "types": "./dist/api/index.d.ts",
42
+ "import": "./dist/api/index.js"
43
+ },
44
+ "./components": {
45
+ "types": "./dist/components/index.d.ts",
46
+ "import": "./dist/components/index.js"
47
+ },
48
+ "./mcp": {
49
+ "types": "./dist/mcp/index.d.ts",
50
+ "import": "./dist/mcp/index.js"
51
+ },
52
+ "./components/exams": {
53
+ "types": "./dist/components/exams/index.d.ts",
54
+ "import": "./dist/components/exams/index.js"
55
+ },
56
+ "./components/exams/take": {
57
+ "types": "./dist/components/exams/take/index.d.ts",
58
+ "import": "./dist/components/exams/take/index.js"
59
+ },
60
+ "./components/exams/entry": {
61
+ "types": "./dist/components/exams/entry/index.d.ts",
62
+ "import": "./dist/components/exams/entry/index.js"
63
+ },
64
+ "./components/exams/rooms": {
65
+ "types": "./dist/components/exams/rooms/index.d.ts",
66
+ "import": "./dist/components/exams/rooms/index.js"
67
+ },
68
+ "./components/questions": {
69
+ "types": "./dist/components/questions/index.d.ts",
70
+ "import": "./dist/components/questions/index.js"
71
+ },
72
+ "./components/themes": {
73
+ "types": "./dist/components/themes/index.d.ts",
74
+ "import": "./dist/components/themes/index.js"
75
+ },
76
+ "./components/results": {
77
+ "types": "./dist/components/results/index.d.ts",
78
+ "import": "./dist/components/results/index.js"
79
+ },
80
+ "./components/questions/*": {
81
+ "types": "./dist/components/questions/*.d.ts",
82
+ "import": "./dist/components/questions/*.js"
83
+ },
84
+ "./shared": {
85
+ "types": "./dist/shared/index.d.ts",
86
+ "import": "./dist/shared/index.js"
87
+ },
88
+ "./*": {
89
+ "types": "./dist/components/questions/*.d.ts",
90
+ "import": "./dist/components/questions/*.js"
91
+ },
92
+ "./package.json": "./package.json"
93
+ },
94
+ "publishConfig": {
95
+ "access": "public"
96
+ },
97
+ "peerDependencies": {
98
+ "next": ">=15",
99
+ "react": ">=19",
100
+ "react-dom": ">=19"
101
+ },
102
+ "devDependencies": {
103
+ "@types/node": "^25.9.3",
104
+ "terser": "^5.48.0",
105
+ "tsc-alias": "^1.8.17",
106
+ "typescript": "~5.9.2"
107
+ },
108
+ "dependencies": {
109
+ "@dnd-kit/core": "^6.3.1",
110
+ "@dnd-kit/sortable": "^10.0.0",
111
+ "@dnd-kit/utilities": "^3.2.2",
112
+ "@fortawesome/free-solid-svg-icons": "^7.3.1",
113
+ "@fortawesome/react-fontawesome": "^3.3.1",
114
+ "@hookform/resolvers": "^5.2.2",
115
+ "@modelcontextprotocol/sdk": "^1.29.0",
116
+ "@radix-ui/react-alert-dialog": "^1.1.15",
117
+ "@radix-ui/react-avatar": "^1.1.10",
118
+ "@radix-ui/react-checkbox": "^1.3.3",
119
+ "@radix-ui/react-collapsible": "^1.1.12",
120
+ "@radix-ui/react-dialog": "^1.1.15",
121
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
122
+ "@radix-ui/react-hover-card": "^1.1.15",
123
+ "@radix-ui/react-label": "^2.1.7",
124
+ "@radix-ui/react-popover": "^1.1.15",
125
+ "@radix-ui/react-progress": "^1.1.7",
126
+ "@radix-ui/react-radio-group": "^1.3.8",
127
+ "@radix-ui/react-scroll-area": "^1.2.10",
128
+ "@radix-ui/react-select": "^2.2.6",
129
+ "@radix-ui/react-separator": "^1.1.7",
130
+ "@radix-ui/react-slot": "^1.2.3",
131
+ "@radix-ui/react-switch": "^1.2.6",
132
+ "@radix-ui/react-tabs": "^1.1.13",
133
+ "@radix-ui/react-tooltip": "^1.2.8",
134
+ "@tanstack/react-query": "^5.90.2",
135
+ "@tiptap/extension-table": "^3.22.3",
136
+ "@tiptap/extension-table-cell": "^3.22.3",
137
+ "@tiptap/extension-table-header": "^3.22.3",
138
+ "@tiptap/extension-table-row": "^3.22.3",
139
+ "@tiptap/extension-text-align": "^3.22.3",
140
+ "@tiptap/pm": "^3.22.3",
141
+ "@tiptap/react": "^3.22.3",
142
+ "@tiptap/starter-kit": "^3.22.3",
143
+ "@types/qrcode": "^1.5.6",
144
+ "@xyflow/react": "^12.8.6",
145
+ "axios": "^1.12.2",
146
+ "class-variance-authority": "^0.7.1",
147
+ "clsx": "^2.1.1",
148
+ "cmdk": "^1.1.1",
149
+ "date-fns": "^4.1.0",
150
+ "embla-carousel-react": "^8.6.0",
151
+ "howler": "^2.2.4",
152
+ "lucide-react": "^0.544.0",
153
+ "qrcode": "^1.5.4",
154
+ "radix-ui": "^1.4.3",
155
+ "react-day-picker": "^9.14.0",
156
+ "react-hook-form": "^7.65.0",
157
+ "sonner": "^2.0.7",
158
+ "tailwind-merge": "^3.3.1",
159
+ "vaul": "^1.1.2",
160
+ "zod": "^4.1.11",
161
+ "zustand": "^5.0.8"
162
+ }
163
+ }