Improve MCP tooling and draft exercise publishing
This commit is contained in:
+20
-1
@@ -10,7 +10,7 @@ The package does not parse PDFs or generate course structures. The agent does th
|
||||
Teacher / Admin
|
||||
|
|
||||
v
|
||||
Claude Code / Codex / Cursor / other MCP client
|
||||
Claude Code / Codex / Cursor / OpenCode / other MCP client
|
||||
|
|
||||
| 1. Reads prompt, PDF, or existing course goal
|
||||
| 2. Produces or edits normalized course JSON
|
||||
@@ -123,6 +123,25 @@ codex mcp add classroomio \
|
||||
-- npx -y @classroomio/mcp
|
||||
```
|
||||
|
||||
### OpenCode
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"classroomio": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@classroomio/mcp"],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"CLASSROOMIO_API_URL": "https://api.classroomio.com",
|
||||
"CLASSROOMIO_API_KEY": "<cio_mcp_key>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
```json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@classroomio/mcp",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.9",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||
const ZConfig = z.object({
|
||||
CLASSROOMIO_API_URL: z.url().default('https://api.classroomio.com'),
|
||||
CLASSROOMIO_API_KEY: z.string().min(1),
|
||||
CLASSROOMIO_USER_AGENT: z.string().min(1).default('classroomio-mcp/0.0.1')
|
||||
CLASSROOMIO_USER_AGENT: z.string().min(1).default('classroomio-mcp/0.0.9')
|
||||
});
|
||||
|
||||
export type McpServerConfig = z.infer<typeof ZConfig>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { ZCourseMetadata } from '../course/course';
|
||||
import { ZExerciseQuestionTypeId } from '../exercise/exercise';
|
||||
|
||||
const ZSupportedLocale = z.enum(['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']);
|
||||
const LESSON_BODY_HTML_DESCRIPTION =
|
||||
@@ -54,6 +55,35 @@ export const ZCourseImportDraftLessonLanguage = z.object({
|
||||
});
|
||||
export type TCourseImportDraftLessonLanguage = z.infer<typeof ZCourseImportDraftLessonLanguage>;
|
||||
|
||||
export const ZCourseImportDraftExerciseOption = z.object({
|
||||
label: z.string().min(1),
|
||||
isCorrect: z.boolean(),
|
||||
settings: z.record(z.string(), z.unknown()).optional()
|
||||
});
|
||||
export type TCourseImportDraftExerciseOption = z.infer<typeof ZCourseImportDraftExerciseOption>;
|
||||
|
||||
export const ZCourseImportDraftExerciseQuestion = z.object({
|
||||
question: z.string().min(1),
|
||||
questionTypeId: ZExerciseQuestionTypeId.optional(),
|
||||
points: z.number().int().min(0).optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
settings: z.record(z.string(), z.unknown()).optional(),
|
||||
options: z.array(ZCourseImportDraftExerciseOption).optional()
|
||||
});
|
||||
export type TCourseImportDraftExerciseQuestion = z.infer<typeof ZCourseImportDraftExerciseQuestion>;
|
||||
|
||||
export const ZCourseImportDraftExercise = z.object({
|
||||
externalId: z.string().min(1),
|
||||
lessonExternalId: z.string().min(1).optional(),
|
||||
sectionExternalId: z.string().min(1).optional(),
|
||||
title: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
dueBy: z.string().optional(),
|
||||
questions: z.array(ZCourseImportDraftExerciseQuestion).optional()
|
||||
});
|
||||
export type TCourseImportDraftExercise = z.infer<typeof ZCourseImportDraftExercise>;
|
||||
|
||||
export const ZCourseImportDraftPayload = z
|
||||
.object({
|
||||
course: ZCourseImportDraftCourse,
|
||||
@@ -61,7 +91,7 @@ export const ZCourseImportDraftPayload = z
|
||||
sections: z.array(ZCourseImportDraftSection).min(1),
|
||||
lessons: z.array(ZCourseImportDraftLesson).min(1),
|
||||
lessonLanguages: z.array(ZCourseImportDraftLessonLanguage).min(1),
|
||||
exercises: z.array(z.record(z.string(), z.unknown())).optional(),
|
||||
exercises: z.array(ZCourseImportDraftExercise).optional(),
|
||||
sourceReferences: z.array(ZCourseImportSourceReference).optional(),
|
||||
warnings: z.array(ZCourseImportWarning).default([])
|
||||
})
|
||||
@@ -120,6 +150,34 @@ export const ZCourseImportDraftPayload = z
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const exerciseIds = new Set<string>();
|
||||
value.exercises?.forEach((exercise, index) => {
|
||||
if (exerciseIds.has(exercise.externalId)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['exercises', index, 'externalId'],
|
||||
message: 'Exercise externalId must be unique'
|
||||
});
|
||||
}
|
||||
exerciseIds.add(exercise.externalId);
|
||||
|
||||
if (exercise.lessonExternalId && !lessonIds.has(exercise.lessonExternalId)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['exercises', index, 'lessonExternalId'],
|
||||
message: 'Exercise lessonExternalId must reference an existing lesson'
|
||||
});
|
||||
}
|
||||
|
||||
if (exercise.sectionExternalId && !sectionIds.has(exercise.sectionExternalId)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['exercises', index, 'sectionExternalId'],
|
||||
message: 'Exercise sectionExternalId must reference an existing section'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
export type TCourseImportDraftPayload = z.infer<typeof ZCourseImportDraftPayload>;
|
||||
|
||||
|
||||
@@ -2,6 +2,24 @@ import * as z from 'zod';
|
||||
|
||||
import { QUESTION_TYPE } from '../constants';
|
||||
|
||||
const EXERCISE_QUESTION_TYPE_ID_LITERALS = [
|
||||
z.literal(QUESTION_TYPE.RADIO),
|
||||
z.literal(QUESTION_TYPE.CHECKBOX),
|
||||
z.literal(QUESTION_TYPE.TEXTAREA),
|
||||
z.literal(QUESTION_TYPE.TRUE_FALSE),
|
||||
z.literal(QUESTION_TYPE.SHORT_ANSWER),
|
||||
z.literal(QUESTION_TYPE.NUMERIC),
|
||||
z.literal(QUESTION_TYPE.FILL_BLANK),
|
||||
z.literal(QUESTION_TYPE.FILE_UPLOAD),
|
||||
z.literal(QUESTION_TYPE.MATCHING),
|
||||
z.literal(QUESTION_TYPE.ORDERING),
|
||||
z.literal(QUESTION_TYPE.HOTSPOT),
|
||||
z.literal(QUESTION_TYPE.LINK)
|
||||
] as const;
|
||||
|
||||
export const ZExerciseQuestionTypeId = z.union(EXERCISE_QUESTION_TYPE_ID_LITERALS);
|
||||
export type TExerciseQuestionTypeId = z.infer<typeof ZExerciseQuestionTypeId>;
|
||||
|
||||
const QUESTION_VALIDATION_RULES: Record<
|
||||
number,
|
||||
Array<(question: { options?: Array<{ isCorrect: boolean }> }) => string | null>
|
||||
@@ -61,7 +79,7 @@ const QUESTION_VALIDATION_RULES: Record<
|
||||
const ZExerciseUpdateQuestionBase = z.object({
|
||||
id: z.number().int().optional(),
|
||||
question: z.string().min(1),
|
||||
questionTypeId: z.number().int().min(1).optional(), // Added to support question type updates
|
||||
questionTypeId: ZExerciseQuestionTypeId.optional(),
|
||||
points: z.number().int().min(0).optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
settings: z.record(z.string(), z.unknown()).optional(),
|
||||
@@ -98,6 +116,25 @@ function validateQuestionOptions(question: z.infer<typeof ZExerciseUpdateQuestio
|
||||
// Final schema with validation refinement
|
||||
const ZExerciseUpdateQuestion = ZExerciseUpdateQuestionBase.superRefine(validateQuestionOptions);
|
||||
|
||||
const ZExerciseCreateQuestionBase = z.object({
|
||||
question: z.string().min(1),
|
||||
questionTypeId: ZExerciseQuestionTypeId.optional(),
|
||||
points: z.number().int().min(0).optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
settings: z.record(z.string(), z.unknown()).optional(),
|
||||
options: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().min(1),
|
||||
isCorrect: z.boolean(),
|
||||
settings: z.record(z.string(), z.unknown()).optional()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
});
|
||||
|
||||
const ZExerciseCreateQuestion = ZExerciseCreateQuestionBase.superRefine(validateQuestionOptions);
|
||||
|
||||
export const ZExerciseCreate = z.object({
|
||||
title: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
@@ -106,25 +143,7 @@ export const ZExerciseCreate = z.object({
|
||||
order: z.number().int().optional(),
|
||||
courseId: z.string().min(1),
|
||||
dueBy: z.string().optional(),
|
||||
questions: z
|
||||
.array(
|
||||
z.object({
|
||||
question: z.string().min(1),
|
||||
questionTypeId: z.number().int().min(1).optional(),
|
||||
points: z.number().int().min(0).optional(),
|
||||
settings: z.record(z.string(), z.unknown()).optional(),
|
||||
options: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().min(1),
|
||||
isCorrect: z.boolean(),
|
||||
settings: z.record(z.string(), z.unknown()).optional()
|
||||
})
|
||||
)
|
||||
.min(2)
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
questions: z.array(ZExerciseCreateQuestion).optional()
|
||||
});
|
||||
export type TExerciseCreate = z.infer<typeof ZExerciseCreate>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user