feat: implement automation feature

This commit is contained in:
Rotimi Best
2026-03-18 23:39:12 +01:00
parent 576d0366de
commit ef4424e267
121 changed files with 4544 additions and 1055 deletions
+20 -2
View File
@@ -43,6 +43,7 @@ Current tools:
- `list_org_courses`
- `get_course_structure`
- `reorder_course_content`
- `update_course_landing_page`
- `create_course_draft`
- `create_course_draft_from_course`
@@ -167,7 +168,9 @@ Drafts support:
Lesson authoring rule:
- `lessonLanguages[].content` should contain only the lesson body HTML
- do not include the lesson title or a top-level heading that repeats `lessons[].title`
- do not include the lesson title
- do not use `h1` or `h2` in lesson content
- start headings at `h3`
- ClassroomIO already renders the lesson title in the course UI
## After Publish
@@ -323,7 +326,22 @@ What this publish does today:
- upserts lesson language content
- merges draft tags into the existing course
### Flow 6: Add exercises to a live course
### Flow 6: Reorder live course content
User says:
```text
Move the quiz before lesson 2 and put the recap lesson into the last section.
```
Expected tool sequence:
1. Agent calls `reorder_course_content`.
Use this direct path when the live course only needs section ordering or lesson or exercise moves between sections.
It avoids the full draft round-trip for simple structural changes.
### Flow 7: Add exercises to a live course
User says:
+8 -1
View File
@@ -6,7 +6,7 @@ import type {
TCourseImportDraftPublishToCourse,
TCourseImportDraftUpdate
} from '@cio/utils/validation/course-import';
import type { TCourseLandingPageUpdate } from '@cio/utils/validation/course';
import type { TCourseContentReorder, TCourseLandingPageUpdate, TCourseUpdateParam } from '@cio/utils/validation/course';
import type {
TExerciseCreate,
TExerciseFromTemplate,
@@ -78,6 +78,13 @@ export class ClassroomIoApiClient {
});
}
async reorderCourseContent(courseId: TCourseUpdateParam['courseId'], payload: TCourseContentReorder) {
return this.request(`/course/${courseId}/content/reorder`, {
method: 'PUT',
body: payload
});
}
async getCourseDraft(draftId: string) {
return this.request(`/organization/course-import/drafts/${draftId}`, {
method: 'GET'
+23 -2
View File
@@ -20,7 +20,11 @@ import {
ZCourseImportDraftPublishToCourse,
ZCourseImportDraftUpdate
} from '@cio/utils/validation/course-import';
import { ZCourseLandingPageUpdate } from '@cio/utils/validation/course';
import {
ZCourseContentReorder,
ZCourseContentReorderBase,
ZCourseLandingPageUpdate
} from '@cio/utils/validation/course';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
@@ -61,6 +65,10 @@ export const ZUpdateCourseLandingPageToolInput = ZCourseLandingPageUpdate.extend
courseId: ZCourseImportCourseParam.shape.courseId
});
export const ZReorderCourseContentToolInput = ZCourseContentReorderBase.extend({
courseId: ZCourseImportCourseParam.shape.courseId
});
export const ZPublishCourseDraftToolInput = ZCourseImportDraftPublish.extend({
draftId: ZCourseImportDraftGetParam.shape.draftId
});
@@ -83,6 +91,7 @@ const createCourseExerciseFromTemplateShape =
ZCreateCourseExerciseFromTemplateToolInput.shape as unknown as ZodRawShapeCompat;
const updateCourseExerciseShape = ZUpdateCourseExerciseToolInput.shape as unknown as ZodRawShapeCompat;
const updateCourseLandingPageShape = ZUpdateCourseLandingPageToolInput.shape as unknown as ZodRawShapeCompat;
const reorderCourseContentShape = ZReorderCourseContentToolInput.shape as unknown as ZodRawShapeCompat;
const publishCourseDraftShape = ZPublishCourseDraftToolInput.shape as unknown as ZodRawShapeCompat;
const publishCourseDraftToExistingCourseShape =
ZPublishCourseDraftToExistingCourseToolInput.shape as unknown as ZodRawShapeCompat;
@@ -91,7 +100,7 @@ const tagCoursesShape = ZAutomationCourseTagAssignment.shape as unknown as ZodRa
const SUPPORTED_QUESTION_TYPES_GUIDE =
'Supported questionTypeId values: RADIO=1 (Single answer), CHECKBOX=2 (Multiple answers), TEXTAREA=3 (Paragraph), TRUE_FALSE=4 (True/False), SHORT_ANSWER=5 (Short answer), NUMERIC=6 (Numeric answer), FILL_BLANK=7 (Fill in the blank), FILE_UPLOAD=8 (File upload), MATCHING=9 (Matching), ORDERING=10 (Ordering), HOTSPOT=11 (Hotspot), LINK=12 (Links). Use the numeric ids in exercise payloads.';
const LESSON_HTML_GUIDE =
'For draft lesson HTML, put only the lesson body in lessonLanguages[].content. Do not include the lesson title or a top-level heading that repeats lessons[].title because the ClassroomIO UI already renders the lesson title.';
'For draft lesson HTML, put only the lesson body in lessonLanguages[].content. Do not include the lesson title. Do not use h1 or h2 anywhere in lesson HTML. Start headings at h3 because that is the highest heading level allowed in lesson content.';
export function registerCourseDraftTools(server: McpServer, apiClient: ClassroomIoApiClient) {
server.tool(
@@ -127,6 +136,18 @@ export function registerCourseDraftTools(server: McpServer, apiClient: Classroom
}
);
server.tool(
'reorder_course_content',
'Batch reorder live course sections and lesson or exercise content without creating a draft. Use this for section ordering or moving/reordering lessons and exercises between sections. For items, pass type as LESSON or EXERCISE and use sectionId to move between sections or null for ungrouped content.',
reorderCourseContentShape,
async (args) => {
const { courseId, ...rawPayload } = ZReorderCourseContentToolInput.parse(args);
const payload = ZCourseContentReorder.parse(rawPayload);
const result = await apiClient.reorderCourseContent(courseId, payload);
return jsonContent(result);
}
);
server.tool(
'create_course_draft',
`Create a new unpublished course draft from structured course JSON. Use this before the first publish. ${LESSON_HTML_GUIDE}`,