2026-04-27 11:40:38 +01:00
|
|
|
import type {
|
|
|
|
|
TAutomationCourseTagAssignment,
|
|
|
|
|
TAutomationDraftTagAssignment,
|
|
|
|
|
TAutomationDraftTagParam
|
|
|
|
|
} from '@cio/utils/validation/tag';
|
|
|
|
|
import type { TCourseContentReorder, TCourseLandingPageUpdate, TCourseUpdateParam } from '@cio/utils/validation/course';
|
2026-03-13 18:01:46 +01:00
|
|
|
import type {
|
2026-03-18 10:19:37 +01:00
|
|
|
TCourseImportCourseParam,
|
2026-03-13 18:01:46 +01:00
|
|
|
TCourseImportDraftCreate,
|
2026-03-18 10:19:37 +01:00
|
|
|
TCourseImportDraftCreateFromCourse,
|
2026-03-13 18:01:46 +01:00
|
|
|
TCourseImportDraftPublish,
|
2026-03-18 10:19:37 +01:00
|
|
|
TCourseImportDraftPublishToCourse,
|
2026-03-13 18:01:46 +01:00
|
|
|
TCourseImportDraftUpdate
|
|
|
|
|
} from '@cio/utils/validation/course-import';
|
2026-03-18 10:19:37 +01:00
|
|
|
import type {
|
|
|
|
|
TExerciseCreate,
|
|
|
|
|
TExerciseFromTemplate,
|
|
|
|
|
TExerciseGetParam,
|
|
|
|
|
TExerciseListQuery,
|
|
|
|
|
TExerciseUpdate
|
|
|
|
|
} from '@cio/utils/validation/exercise';
|
2026-03-13 18:01:46 +01:00
|
|
|
|
2026-04-27 11:40:38 +01:00
|
|
|
import type { McpServerConfig } from './config';
|
|
|
|
|
import type { TGetOrganizationCoursesQuery } from '@cio/utils/validation/organization';
|
2026-03-13 18:01:46 +01:00
|
|
|
|
|
|
|
|
type ApiSuccess<T> = {
|
|
|
|
|
success: true;
|
|
|
|
|
data: T;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ApiFailure = {
|
|
|
|
|
success?: false;
|
|
|
|
|
error?: string;
|
|
|
|
|
message?: string;
|
|
|
|
|
code?: string;
|
|
|
|
|
field?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class ClassroomIoApiError extends Error {
|
|
|
|
|
constructor(
|
|
|
|
|
message: string,
|
|
|
|
|
public status: number,
|
|
|
|
|
public code?: string,
|
|
|
|
|
public field?: string
|
|
|
|
|
) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = 'ClassroomIoApiError';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ClassroomIoApiClient {
|
|
|
|
|
constructor(private readonly config: McpServerConfig) {}
|
|
|
|
|
|
|
|
|
|
async createCourseDraft(payload: TCourseImportDraftCreate) {
|
|
|
|
|
return this.request('/organization/course-import/drafts', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:19:37 +01:00
|
|
|
async createCourseDraftFromCourse(payload: TCourseImportDraftCreateFromCourse) {
|
|
|
|
|
return this.request('/organization/course-import/drafts/from-course', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getCourseStructure(courseId: TCourseImportCourseParam['courseId']) {
|
|
|
|
|
return this.request(`/organization/course-import/courses/${courseId}/structure`, {
|
|
|
|
|
method: 'GET'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateCourseLandingPage(courseId: TCourseImportCourseParam['courseId'], payload: TCourseLandingPageUpdate) {
|
|
|
|
|
return this.request(`/course/${courseId}/landing-page`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 23:39:12 +01:00
|
|
|
async reorderCourseContent(courseId: TCourseUpdateParam['courseId'], payload: TCourseContentReorder) {
|
|
|
|
|
return this.request(`/course/${courseId}/content/reorder`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 18:01:46 +01:00
|
|
|
async getCourseDraft(draftId: string) {
|
|
|
|
|
return this.request(`/organization/course-import/drafts/${draftId}`, {
|
|
|
|
|
method: 'GET'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 08:58:41 +01:00
|
|
|
async listOrganizationCourses(query: Partial<TGetOrganizationCoursesQuery> = {}) {
|
2026-03-18 10:19:37 +01:00
|
|
|
const searchParams = new URLSearchParams();
|
|
|
|
|
if (query.tags) searchParams.set('tags', query.tags);
|
|
|
|
|
|
|
|
|
|
const querySuffix = searchParams.toString() ? `?${searchParams.toString()}` : '';
|
|
|
|
|
return this.request(`/organization/courses${querySuffix}`, {
|
|
|
|
|
method: 'GET'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listCourseExercises(courseId: string, query: TExerciseListQuery = {}) {
|
|
|
|
|
const searchParams = new URLSearchParams();
|
|
|
|
|
if (query.lessonId) searchParams.set('lessonId', query.lessonId);
|
|
|
|
|
if (query.sectionId) searchParams.set('sectionId', query.sectionId);
|
|
|
|
|
|
|
|
|
|
const querySuffix = searchParams.toString() ? `?${searchParams.toString()}` : '';
|
|
|
|
|
return this.request(`/course/${courseId}/exercise${querySuffix}`, {
|
|
|
|
|
method: 'GET'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getCourseExercise(courseId: string, exerciseId: TExerciseGetParam['exerciseId']) {
|
|
|
|
|
return this.request(`/course/${courseId}/exercise/${exerciseId}`, {
|
|
|
|
|
method: 'GET'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createCourseExercise(courseId: string, payload: Omit<TExerciseCreate, 'courseId'>) {
|
|
|
|
|
return this.request(`/course/${courseId}/exercise`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: {
|
|
|
|
|
...payload,
|
|
|
|
|
courseId
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createCourseExerciseFromTemplate(courseId: string, payload: TExerciseFromTemplate) {
|
|
|
|
|
return this.request(`/course/${courseId}/exercise/from-template`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateCourseExercise(courseId: string, exerciseId: TExerciseGetParam['exerciseId'], payload: TExerciseUpdate) {
|
|
|
|
|
return this.request(`/course/${courseId}/exercise/${exerciseId}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 18:01:46 +01:00
|
|
|
async updateCourseDraft(draftId: string, payload: TCourseImportDraftUpdate) {
|
|
|
|
|
return this.request(`/organization/course-import/drafts/${draftId}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:19:37 +01:00
|
|
|
async tagCourseDraft(draftId: TAutomationDraftTagParam['draftId'], payload: TAutomationDraftTagAssignment) {
|
|
|
|
|
return this.request(`/organization/course-import/drafts/${draftId}/tags`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 18:01:46 +01:00
|
|
|
async publishCourseDraft(draftId: string, payload: TCourseImportDraftPublish) {
|
|
|
|
|
return this.request(`/organization/course-import/drafts/${draftId}/publish`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:19:37 +01:00
|
|
|
async publishCourseDraftToExistingCourse(draftId: string, payload: TCourseImportDraftPublishToCourse) {
|
|
|
|
|
return this.request(`/organization/course-import/drafts/${draftId}/publish-existing-course`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async tagCourses(payload: TAutomationCourseTagAssignment) {
|
|
|
|
|
return this.request<
|
|
|
|
|
Array<{ courseId: string; tags: Array<{ id: string; name: string; slug: string; color: string }> }>
|
|
|
|
|
>('/organization/tags/courses/assign', {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: payload
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 18:01:46 +01:00
|
|
|
private async request<TResponse>(
|
|
|
|
|
path: string,
|
|
|
|
|
options: {
|
|
|
|
|
method: 'GET' | 'POST' | 'PUT';
|
|
|
|
|
body?: unknown;
|
|
|
|
|
}
|
|
|
|
|
): Promise<TResponse> {
|
|
|
|
|
const response = await fetch(new URL(path, this.config.CLASSROOMIO_API_URL), {
|
|
|
|
|
method: options.method,
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${this.config.CLASSROOMIO_API_KEY}`,
|
|
|
|
|
'content-type': 'application/json',
|
|
|
|
|
'user-agent': this.config.CLASSROOMIO_USER_AGENT
|
|
|
|
|
},
|
|
|
|
|
body: options.body ? JSON.stringify(options.body) : undefined
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const json = (await response.json().catch(() => null)) as ApiSuccess<TResponse> | ApiFailure | null;
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorPayload = json as ApiFailure | null;
|
|
|
|
|
throw new ClassroomIoApiError(
|
|
|
|
|
errorPayload?.error ?? errorPayload?.message ?? `ClassroomIO request failed with status ${response.status}`,
|
|
|
|
|
response.status,
|
|
|
|
|
errorPayload?.code,
|
|
|
|
|
errorPayload?.field
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!json || typeof json !== 'object' || !('success' in json) || !json.success) {
|
|
|
|
|
throw new ClassroomIoApiError('ClassroomIO returned an invalid response payload', response.status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return json.data;
|
|
|
|
|
}
|
|
|
|
|
}
|