chore: update MCP package version to 0.0.7, add formatting script, and enhance README with architecture and principles

This commit is contained in:
Rotimi Best
2026-03-18 10:19:37 +01:00
parent 58264a2b1e
commit 0d65b5a273
4 changed files with 686 additions and 132 deletions
+329 -110
View File
@@ -1,77 +1,106 @@
# ClassroomIO MCP Server
# @classroomio/mcp
Thin stdio MCP server for ClassroomIO course-draft authoring.
Thin stdio MCP server for ClassroomIO course authoring.
The package does not parse PDFs or generate course structures. The agent does that work. `@classroomio/mcp` only exposes tools, validates tool input, and forwards requests to the ClassroomIO API.
## Architecture
```text
Teacher
Teacher / Admin
|
v
Claude Desktop / Claude Code / other MCP client
Claude Code / Codex / Cursor / other MCP client
|
| 1. Reads prompt or PDF content
| 2. Produces normalized course JSON
| 1. Reads prompt, PDF, or existing course goal
| 2. Produces or edits normalized course JSON
v
ClassroomIO MCP Server (stdio)
@classroomio/mcp (stdio)
|
| 3. Forwards validated tool calls
| - Authorization: Bearer <cio_mcp_...>
| 3. Sends authenticated tool calls
| Authorization: Bearer <cio_mcp_...>
v
ClassroomIO API
|
| 4. Validates payload
| 5. Resolves org from the key
| 6. Stores draft or publishes course
| 4. Validates payloads
| 5. Resolves the organization from the key
| 6. Creates drafts or applies changes to a course
v
ClassroomIO DB
```
## How It Comes Together
## Principles
1. The agent owns reasoning and extraction.
2. The MCP server does not parse PDFs or generate course structure.
3. The MCP server only exposes tools and forwards structured payloads into ClassroomIO.
4. ClassroomIO API remains the trust boundary for auth, validation, and persistence.
5. Draft creation and publish are explicit separate steps.
1. The agent owns reasoning.
2. The MCP package owns transport and input validation.
3. ClassroomIO API remains the trust boundary for auth, validation, and persistence.
4. Draft creation and publish are separate operations.
5. Updating an existing course is done through a seeded draft, not blind writes to the live course.
## Tool Surface
Current tools:
- `list_org_courses`
- `get_course_structure`
- `update_course_landing_page`
- `create_course_draft`
- `create_course_draft_from_course`
- `get_course_draft`
- `list_course_exercises`
- `get_course_exercise`
- `create_course_exercise`
- `create_course_exercise_from_template`
- `update_course_exercise`
- `update_course_draft`
- `tag_course_draft`
- `tag_courses`
- `publish_course_draft`
## Required Environment Variables
- `CLASSROOMIO_API_URL`
- `CLASSROOMIO_API_KEY`
- `publish_course_draft_to_existing_course`
## Auth Model
Current implementation uses an org-scoped automation key created in ClassroomIO under `Automation -> MCP`.
The package expects an org-scoped ClassroomIO automation key generated from `Automation -> MCP` in the ClassroomIO dashboard.
The MCP server sends that key as a bearer token on every request.
The MCP process sends the key as a bearer token on every request.
ClassroomIO API:
1. hashes and verifies the key
2. resolves the owning organization
3. checks the key scopes
4. persists the draft or publish request
4. executes the requested action
The MCP server does not decide permissions. ClassroomIO API remains the source of truth.
The MCP package never decides permissions.
## Run
## Required Environment Variables
- `CLASSROOMIO_API_URL`
- `CLASSROOMIO_API_KEY`
- `CLASSROOMIO_USER_AGENT` optional
## Run Locally
From the repo root:
```bash
pnpm --filter @classroomio/mcp build
pnpm build --filter=@classroomio/mcp
CLASSROOMIO_API_URL=http://localhost:3081 \
CLASSROOMIO_API_KEY=<cio_mcp_key> \
node packages/mcp/dist/index.js
```
## Claude Code Example
Published package:
```bash
CLASSROOMIO_API_URL=https://api.classroomio.com \
CLASSROOMIO_API_KEY=<cio_mcp_key> \
npx -y @classroomio/mcp
```
## Client Setup
### Claude Code
```bash
claude mcp add-json classroomio '{
@@ -84,7 +113,7 @@ claude mcp add-json classroomio '{
}'
```
## Codex Example
### Codex
```bash
codex mcp add classroomio \
@@ -93,7 +122,7 @@ codex mcp add classroomio \
-- npx -y @classroomio/mcp
```
## Cursor Example
### Cursor
```json
{
@@ -110,89 +139,16 @@ codex mcp add classroomio \
}
```
## User Flow Examples
## Draft Model
This package uses the same org-scoped automation key system that will later power the public API.
A draft is a saved, structured course proposal. It is not the live course.
Different integrations get different keys and scope presets. The infrastructure is shared, the secrets should not be.
### Flow 1: Prompt to draft to publish
User says:
```text
Create a complete course on exponential functions for high school students in ClassroomIO.
```
Expected agent behavior:
1. Claude plans the course structure.
2. Claude calls `create_course_draft` with:
- course title and description
- sections
- lessons
- lesson language content
3. Claude reviews the returned draft ID and summary.
4. User requests changes.
5. Claude calls `update_course_draft`.
6. When the user approves, Claude calls `publish_course_draft`.
Result:
- a real ClassroomIO course
- sections created
- lessons created
- lesson language content inserted
### Flow 2: PDF to draft
User says:
```text
I have my course in a PDF. Extract it and turn it into a structured ClassroomIO course draft.
```
Expected agent behavior:
1. Claude reads the PDF itself.
2. Claude extracts and restructures the material into normalized course JSON.
3. Claude calls `create_course_draft`.
4. Claude returns a summary of:
- detected sections
- detected lessons
- warnings or ambiguous splits
5. User asks for revisions.
6. Claude calls `update_course_draft`.
7. User approves publish.
8. Claude calls `publish_course_draft`.
Important:
- the PDF never needs to be uploaded to the MCP server
- the MCP server only receives structured JSON
### Flow 3: Human-in-the-loop revision
User says:
```text
Move logarithmic review before growth models and shorten lesson 2.
```
Expected agent behavior:
1. Claude calls `get_course_draft`.
2. Claude computes the revised structure.
3. Claude calls `update_course_draft` with the modified payload.
4. Claude summarizes what changed before publish.
## Normalized Payload Shape
The core payload sent through the MCP server is:
Draft payload shape:
```text
draft
course
tags[]
sections[]
lessons[]
lessonLanguages[]
@@ -201,4 +157,267 @@ draft
exercises[] optional
```
This keeps prompt-based creation and PDF-based extraction on the same ingestion path.
Drafts support:
- review before publish
- iterative edits from the agent
- safer updates to existing courses
- auditability of AI-generated structure
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`
- ClassroomIO already renders the lesson title in the course UI
## After Publish
Once a draft has been published, the live course becomes the safer source of truth.
Recommended agent rule:
- if the draft is still `DRAFT`, continue editing that draft
- if the draft is `PUBLISHED`, do not keep reusing it for major follow-up edits
- instead, read the live course and create a fresh draft from that course
Recommended post-publish sequence:
1. `get_course_structure`
2. `create_course_draft_from_course`
3. `update_course_draft`
4. `publish_course_draft_to_existing_course`
Why:
- teachers may edit the live course in the UI after publish
- another agent session may have changed the course
- reusing an old published draft can overwrite newer live changes
For exercises on an already-published course, prefer the direct exercise tools instead of creating a whole new draft when the user only wants to add or revise exercises.
## User Flows
### Flow 1: Create a new course from a prompt
User says:
```text
Create a complete course on exponential functions for high school students.
```
Expected tool sequence:
1. Agent plans the course.
2. Agent calls `create_course_draft`.
3. User asks for revisions.
4. Agent calls `update_course_draft`.
5. User approves.
6. Agent calls `publish_course_draft`.
Result:
- a new course is created
- sections and lessons are created
- localized lesson content is written
- publish returns the live `courseId` and public `courseUrl`
- publish can optionally set a random Unsplash-derived course cover
### Flow 2: Update a landing page with AI-generated copy and media
User says:
```text
Rewrite this course landing page to feel more premium, generate a new cover image, and add three believable reviews.
```
Expected tool sequence:
1. Agent calls `list_org_courses` if it needs to resolve the course ID by name first.
2. Agent calls `update_course_landing_page`.
What the landing-page tool can update:
- title
- short description
- overview
- requirements
- goals
- pricing fields
- instructor section
- reviews
- landing page image
The tool can either:
- use an explicit `imageUrl`
- or generate a random Unsplash-based image with `generateImage` and `imageQuery`
### Flow 3: Create a draft from a PDF
User says:
```text
I have my course in a PDF. Extract it and turn it into a ClassroomIO course draft.
```
Expected tool sequence:
1. Agent reads the PDF itself.
2. Agent restructures the content into normalized JSON.
3. Agent calls `create_course_draft`.
4. User reviews the draft.
5. Agent calls `update_course_draft` if needed.
6. User approves.
7. Agent calls `publish_course_draft`.
Important:
- the PDF does not need to be uploaded to MCP
- the MCP package only receives structured JSON
### Flow 4: Tag a draft before publish
User says:
```text
Add tags for algebra, beginner, and exponential growth to this draft.
```
Expected tool sequence:
1. Agent calls `tag_course_draft`.
2. User reviews the updated draft tags.
3. Agent later calls `publish_course_draft`.
Result:
- the draft stores tag names
- publish ensures the tags exist in ClassroomIO
- publish assigns those tags to the live course
### Flow 5: Update an existing course safely
User says:
```text
Take my existing Algebra course, reorganize it, and rewrite the lesson content for a beginner audience.
```
Expected tool sequence:
1. Agent calls `list_org_courses` if it needs to resolve the course ID from the course name first.
2. Agent calls `get_course_structure` with the existing `courseId`.
3. Agent optionally calls `create_course_draft_from_course` to persist a seeded draft.
4. Agent updates the draft with the new structure and content using `update_course_draft`.
5. User reviews the changes.
6. Agent calls `publish_course_draft_to_existing_course`.
What this publish does today:
- updates the existing course title, description, type, and metadata
- can set a generated course cover from Unsplash
- returns the public `courseUrl`
- updates existing sections when the draft keeps their IDs
- updates existing lessons when the draft keeps their IDs
- creates new sections and lessons for draft items without matching live IDs
- upserts lesson language content
- merges draft tags into the existing course
### Flow 6: Add exercises to a live course
User says:
```text
Add a 5-question multiple-choice exercise to lesson 3.
```
Expected tool sequence:
1. Agent calls `list_course_exercises` if it needs current context.
2. Agent calls `create_course_exercise` for a brand new exercise.
3. Agent calls `update_course_exercise` for later revisions.
Use this direct path when the course is already live and the change is exercise-specific.
What it does not do today:
- automatically delete sections or lessons that are absent from the draft
- automatically delete lesson locales that are absent from the draft
That behavior is intentional. The current update path is non-destructive by default.
## How Existing-Course Updates Work
When a draft is seeded from a live course:
- section `externalId` values are the real section IDs
- lesson `externalId` values are the real lesson IDs
As long as the agent preserves those IDs while editing the draft, publish-to-existing-course can map draft items back to the live records and update them safely.
If the agent adds a brand new section or lesson, it should assign a new synthetic `externalId`. ClassroomIO will create that record on publish.
## Input Contract Notes
For large updates, the agent should:
1. call `get_course_structure`
2. keep existing `externalId` values for records it wants to update
3. only generate new `externalId` values for new records
4. avoid deleting records from the draft unless the user explicitly wants omitted content to remain untouched
Publish input also supports:
- `bannerImageUrl`: use this exact course cover
- `bannerImageQuery`: search Unsplash with this query
- `generateBannerImage`: fetch a random Unsplash image using the course title or query
Tagging tools use:
- `tagNames`: human-readable tag names, not tag IDs
- `mode: "merge" | "replace"`
- `groupName` optional, defaults to `Automation`
The API creates missing tags automatically when needed.
Exercise tools use the live course directly:
- `list_course_exercises`
- `get_course_exercise`
- `create_course_exercise`
- `create_course_exercise_from_template`
- `update_course_exercise`
Supported `questionTypeId` values for exercise payloads:
- `1` `RADIO` - Single answer
- `2` `CHECKBOX` - Multiple answers
- `3` `TEXTAREA` - Paragraph
- `4` `TRUE_FALSE` - True/False
- `5` `SHORT_ANSWER` - Short answer
- `6` `NUMERIC` - Numeric answer
- `7` `FILL_BLANK` - Fill in the blank
- `8` `FILE_UPLOAD` - File upload
- `9` `MATCHING` - Matching
- `10` `ORDERING` - Ordering
- `11` `HOTSPOT` - Hotspot
- `12` `LINK` - Links
## Operational Notes
- `create_course_draft_from_course` may add warnings if a live course uses legacy lesson notes instead of localized lesson content
- if a course has ungrouped lessons, the seeded draft may add a synthetic `Ungrouped` section so the structure stays valid
- published drafts are treated as finalized snapshots
## Recommended Agent Behavior
For small edits, an agent can still read the course structure and modify only the affected items in the draft.
For broad revisions, prefer this sequence:
1. `get_course_structure`
2. `create_course_draft_from_course`
3. `update_course_draft`
4. `publish_course_draft_to_existing_course`
That keeps major AI-assisted edits reviewable and explicit.
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@classroomio/mcp",
"version": "0.0.2",
"version": "0.0.7",
"private": false,
"type": "module",
"main": "./dist/index.js",
@@ -16,7 +16,8 @@
"scripts": {
"build": "tsc --noEmit && rm -rf dist tsconfig.tsbuildinfo && node ./scripts/build.mjs",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
"start": "node dist/index.js",
"format": "prettier --write \"src/**/*.ts\""
},
"repository": {
"type": "git",
+111
View File
@@ -1,8 +1,25 @@
import type {
TCourseImportCourseParam,
TCourseImportDraftCreate,
TCourseImportDraftCreateFromCourse,
TCourseImportDraftPublish,
TCourseImportDraftPublishToCourse,
TCourseImportDraftUpdate
} from '@cio/utils/validation/course-import';
import type { TCourseLandingPageUpdate } from '@cio/utils/validation/course';
import type {
TExerciseCreate,
TExerciseFromTemplate,
TExerciseGetParam,
TExerciseListQuery,
TExerciseUpdate
} from '@cio/utils/validation/exercise';
import type { TGetOrganizationCoursesQuery } from '@cio/utils/validation/organization';
import type {
TAutomationCourseTagAssignment,
TAutomationDraftTagAssignment,
TAutomationDraftTagParam
} from '@cio/utils/validation/tag';
import type { McpServerConfig } from './config.js';
@@ -41,12 +58,83 @@ export class ClassroomIoApiClient {
});
}
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
});
}
async getCourseDraft(draftId: string) {
return this.request(`/organization/course-import/drafts/${draftId}`, {
method: 'GET'
});
}
async listOrganizationCourses(query: TGetOrganizationCoursesQuery = {}) {
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
});
}
async updateCourseDraft(draftId: string, payload: TCourseImportDraftUpdate) {
return this.request(`/organization/course-import/drafts/${draftId}`, {
method: 'PUT',
@@ -54,6 +142,13 @@ export class ClassroomIoApiClient {
});
}
async tagCourseDraft(draftId: TAutomationDraftTagParam['draftId'], payload: TAutomationDraftTagAssignment) {
return this.request(`/organization/course-import/drafts/${draftId}/tags`, {
method: 'PUT',
body: payload
});
}
async publishCourseDraft(draftId: string, payload: TCourseImportDraftPublish) {
return this.request(`/organization/course-import/drafts/${draftId}/publish`, {
method: 'POST',
@@ -61,6 +156,22 @@ export class ClassroomIoApiClient {
});
}
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
});
}
private async request<TResponse>(
path: string,
options: {
+243 -20
View File
@@ -1,9 +1,26 @@
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 { 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';
@@ -13,39 +30,245 @@ 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 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 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 or a top-level heading that repeats lessons[].title because the ClassroomIO UI already renders the lesson title.';
export function registerCourseDraftTools(server: McpServer, apiClient: ClassroomIoApiClient) {
server.tool('create_course_draft', createCourseDraftShape, async (args) => {
const payload = ZCourseImportDraftCreate.parse(args);
const result = await apiClient.createCourseDraft(payload);
return jsonContent(result);
});
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_draft', getCourseDraftShape, async (args) => {
const { draftId } = ZCourseImportDraftGetParam.parse(args);
const result = await apiClient.getCourseDraft(draftId);
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_draft', updateCourseDraftShape, async (args) => {
const { draftId, ...payload } = ZUpdateCourseDraftToolInput.parse(args);
const result = await apiClient.updateCourseDraft(draftId, payload);
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('publish_course_draft', publishCourseDraftShape, async (args) => {
const { draftId, ...payload } = ZPublishCourseDraftToolInput.parse(args);
const result = await apiClient.publishCourseDraft(draftId, 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) {