Files
classroomio/packages/mcp/src/tools/course-drafts.ts
T

305 lines
13 KiB
TypeScript

import {
ZAutomationCourseTagAssignment,
ZAutomationDraftTagAssignment,
ZAutomationDraftTagParam
} from '@cio/utils/validation/tag';
import { ZGetOrganizationCoursesQuery } from '@cio/utils/validation/organization';
import {
ZExerciseCreate,
ZExerciseFromTemplate,
ZExerciseGetParam,
ZExerciseListQuery,
ZExerciseUpdate
} from '@cio/utils/validation/exercise';
import {
ZCourseImportCourseParam,
ZCourseImportDraftCreate,
ZCourseImportDraftCreateFromCourse,
ZCourseImportDraftGetParam,
ZCourseImportDraftPublish,
ZCourseImportDraftPublishToCourse,
ZCourseImportDraftUpdate
} from '@cio/utils/validation/course-import';
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';
import type { ClassroomIoApiClient } from '../api-client.js';
export const ZUpdateCourseDraftToolInput = ZCourseImportDraftUpdate.extend({
draftId: ZCourseImportDraftGetParam.shape.draftId
});
export const ZTagCourseDraftToolInput = ZAutomationDraftTagAssignment.extend({
draftId: ZAutomationDraftTagParam.shape.draftId
});
export const ZListCourseExercisesToolInput = ZExerciseListQuery.extend({
courseId: ZCourseImportCourseParam.shape.courseId
});
export const ZGetCourseExerciseToolInput = ZExerciseGetParam.extend({
courseId: ZCourseImportCourseParam.shape.courseId
});
export const ZCreateCourseExerciseToolInput = ZExerciseCreate.omit({
courseId: true
}).extend({
courseId: ZCourseImportCourseParam.shape.courseId
});
export const ZCreateCourseExerciseFromTemplateToolInput = ZExerciseFromTemplate.extend({
courseId: ZCourseImportCourseParam.shape.courseId
});
export const ZUpdateCourseExerciseToolInput = ZExerciseUpdate.extend({
courseId: ZCourseImportCourseParam.shape.courseId,
exerciseId: ZExerciseGetParam.shape.exerciseId
});
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
});
export const ZPublishCourseDraftToExistingCourseToolInput = ZCourseImportDraftPublishToCourse.extend({
draftId: ZCourseImportDraftGetParam.shape.draftId
});
const createCourseDraftShape = ZCourseImportDraftCreate.shape as unknown as ZodRawShapeCompat;
const createCourseDraftFromCourseShape = ZCourseImportDraftCreateFromCourse.shape as unknown as ZodRawShapeCompat;
const listOrganizationCoursesShape = ZGetOrganizationCoursesQuery.shape as unknown as ZodRawShapeCompat;
const getCourseStructureShape = ZCourseImportCourseParam.shape as unknown as ZodRawShapeCompat;
const getCourseDraftShape = ZCourseImportDraftGetParam.shape as unknown as ZodRawShapeCompat;
const updateCourseDraftShape = ZUpdateCourseDraftToolInput.shape as unknown as ZodRawShapeCompat;
const tagCourseDraftShape = ZTagCourseDraftToolInput.shape as unknown as ZodRawShapeCompat;
const listCourseExercisesShape = ZListCourseExercisesToolInput.shape as unknown as ZodRawShapeCompat;
const getCourseExerciseShape = ZGetCourseExerciseToolInput.shape as unknown as ZodRawShapeCompat;
const createCourseExerciseShape = ZCreateCourseExerciseToolInput.shape as unknown as ZodRawShapeCompat;
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;
const tagCoursesShape = ZAutomationCourseTagAssignment.shape as unknown as ZodRawShapeCompat;
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. 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(
'list_org_courses',
'List all courses in the authenticated organization. Use this when the user refers to a course by name and you need to discover the available course IDs first.',
listOrganizationCoursesShape,
async (args) => {
const query = ZGetOrganizationCoursesQuery.parse(args);
const result = await apiClient.listOrganizationCourses(query);
return jsonContent(result);
}
);
server.tool(
'get_course_structure',
'Read the current live course structure. Use this before creating a fresh draft for post-publish edits.',
getCourseStructureShape,
async (args) => {
const { courseId } = ZCourseImportCourseParam.parse(args);
const result = await apiClient.getCourseStructure(courseId);
return jsonContent(result);
}
);
server.tool(
'update_course_landing_page',
'Update landing-page-facing course fields on a live course, including headline copy, overview, requirements, goals, pricing, reviews, instructor information, and the course image. Use imageUrl to set an explicit cover image, or set generateImage/imageQuery to fetch a random Unsplash-based image.',
updateCourseLandingPageShape,
async (args) => {
const { courseId, ...payload } = ZUpdateCourseLandingPageToolInput.parse(args);
const result = await apiClient.updateCourseLandingPage(courseId, payload);
return jsonContent(result);
}
);
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}`,
createCourseDraftShape,
async (args) => {
const payload = ZCourseImportDraftCreate.parse(args);
const result = await apiClient.createCourseDraft(payload);
return jsonContent(result);
}
);
server.tool(
'create_course_draft_from_course',
'Create a fresh draft from an existing live course. Use this after a course has already been published and the user wants more changes.',
createCourseDraftFromCourseShape,
async (args) => {
const payload = ZCourseImportDraftCreateFromCourse.parse(args);
const result = await apiClient.createCourseDraftFromCourse(payload);
return jsonContent(result);
}
);
server.tool(
'get_course_draft',
'Fetch an existing draft. Draft responses include status and publishedCourseId so you can decide whether to keep editing the draft or seed a fresh one from the live course.',
getCourseDraftShape,
async (args) => {
const { draftId } = ZCourseImportDraftGetParam.parse(args);
const result = await apiClient.getCourseDraft(draftId);
return jsonContent(result);
}
);
server.tool(
'list_course_exercises',
'List exercises for a live course, optionally filtered by lessonId or sectionId.',
listCourseExercisesShape,
async (args) => {
const { courseId, ...query } = ZListCourseExercisesToolInput.parse(args);
const result = await apiClient.listCourseExercises(courseId, query);
return jsonContent(result);
}
);
server.tool(
'get_course_exercise',
'Fetch a single exercise from a live course, including its questions and options.',
getCourseExerciseShape,
async (args) => {
const { courseId, exerciseId } = ZGetCourseExerciseToolInput.parse(args);
const result = await apiClient.getCourseExercise(courseId, exerciseId);
return jsonContent(result);
}
);
server.tool(
'create_course_exercise',
`Create a new exercise directly on a live course. Use this for adding exercises after a course has already been published. ${SUPPORTED_QUESTION_TYPES_GUIDE}`,
createCourseExerciseShape,
async (args) => {
const { courseId, ...payload } = ZCreateCourseExerciseToolInput.parse(args);
const result = await apiClient.createCourseExercise(courseId, payload);
return jsonContent(result);
}
);
server.tool(
'create_course_exercise_from_template',
'Create a new exercise on a live course from an existing template.',
createCourseExerciseFromTemplateShape,
async (args) => {
const { courseId, ...payload } = ZCreateCourseExerciseFromTemplateToolInput.parse(args);
const result = await apiClient.createCourseExerciseFromTemplate(courseId, payload);
return jsonContent(result);
}
);
server.tool(
'update_course_exercise',
`Update an existing exercise on a live course, including questions and options. ${SUPPORTED_QUESTION_TYPES_GUIDE}`,
updateCourseExerciseShape,
async (args) => {
const { courseId, exerciseId, ...payload } = ZUpdateCourseExerciseToolInput.parse(args);
const result = await apiClient.updateCourseExercise(courseId, exerciseId, payload);
return jsonContent(result);
}
);
server.tool(
'update_course_draft',
`Update an unpublished draft. If the draft is already published, do not keep editing it blindly; create a fresh draft from the live course first. ${LESSON_HTML_GUIDE}`,
updateCourseDraftShape,
async (args) => {
const { draftId, ...payload } = ZUpdateCourseDraftToolInput.parse(args);
const result = await apiClient.updateCourseDraft(draftId, payload);
return jsonContent(result);
}
);
server.tool(
'tag_course_draft',
'Attach human-readable tags to a draft before publish. The tags are stored on the draft and applied during publish.',
tagCourseDraftShape,
async (args) => {
const { draftId, ...payload } = ZTagCourseDraftToolInput.parse(args);
const result = await apiClient.tagCourseDraft(draftId, payload);
return jsonContent(result);
}
);
server.tool(
'publish_course_draft',
'Publish an unpublished draft as a new live course. The result includes courseId and courseUrl. After publish, future major edits should usually start from a fresh draft created from the live course.',
publishCourseDraftShape,
async (args) => {
const { draftId, ...payload } = ZPublishCourseDraftToolInput.parse(args);
const result = await apiClient.publishCourseDraft(draftId, payload);
return jsonContent(result);
}
);
server.tool(
'publish_course_draft_to_existing_course',
'Apply a fresh draft back onto an existing live course. Use this after reading the live course and creating a new draft from it.',
publishCourseDraftToExistingCourseShape,
async (args) => {
const { draftId, ...payload } = ZPublishCourseDraftToExistingCourseToolInput.parse(args);
const result = await apiClient.publishCourseDraftToExistingCourse(draftId, payload);
return jsonContent(result);
}
);
server.tool(
'tag_courses',
'Assign tags directly to one or more live courses by name. Default mode is merge, so existing tags are preserved unless replace is requested.',
tagCoursesShape,
async (args) => {
const payload = ZAutomationCourseTagAssignment.parse(args);
const result = await apiClient.tagCourses(payload);
return jsonContent(result);
}
);
}
function jsonContent(data: unknown) {
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(data)
}
]
};
}