Drive the Carrco Academy connectors to build and edit courses, sections, lessons, quizzes, programs, audience, compliance, and certificates via the course-builder (/course/mcp) and admin (/admin/mcp) tools. Use whenever building or editing a Carrco Academy course structure (the update_course_structure / "structure import" call that 400s), authoring lessons or quizzes, setting up COMPLIANCE courses, or any create_course → update_course_structure → publish flow. Encodes the exact draft shape, the locale/externalId/HTML rules, a copy-paste known-good payload, the destructive-tool confirm gate, and how to read the Zod 400 error instead of guessing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/carrco-academy-authoring:carrco-course-authoringThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are operating the **Carrco Academy** LMS through two claude.ai connectors:
You are operating the Carrco Academy LMS through two claude.ai connectors:
/course/mcp) — 9 course tools (list_courses, create_course, get_course, update_course, delete_course, list_course_students, export_course, get_course_structure, update_course_structure)./admin/mcp) — 30 tools: audience/enrollment, programs, compliance, news, org settings, certificates.All tools proxy to the ClassroomIO public API at /public-api/v1/*. The org is derived from the API key — never pass organizationId/orgId in any payload.
Authoritative contracts (read these when a field is in doubt — do NOT guess a shape):
reference/classroomio-public-api-contract.md(Zod schemas, every endpoint)reference/mcp-tool-catalog.md(per-tool params)
Course structure is built in three ordered steps. Skipping or reordering them is the #1 cause of failure.
create_course → creates the course shell, returns a courseId (uuid). Required: title, description, type (LIVE_CLASS|SELF_PACED|COMPLIANCE|PUBLIC). If type === 'COMPLIANCE', you MUST also pass compliance (with retakeIntervalMonths).update_course_structure → the heavy call. Takes the courseId from step 1 plus a draft describing all sections, lessons, lesson content, and quizzes. This is the operation that 400s — get the draft shape exactly right (see below).update_course passing isPublished: true. (There is no separate "publish" tool; publication is a course field.)Do not try to add sections/lessons with separate calls — there is only the one update_course_structure batch call. To edit later, re-send update_course_structure (use mode: 'merge' to add/patch, mode: 'replace' to overwrite the whole structure).
Recommended: call get_course_structure (or export_course) before a merge edit so you know the current externalIds you're merging against.
The MCP tool takes courseId as its own argument — it becomes the URL path param (/courses/:courseId/structure), NOT a field in the validated body. The validated request body is { mode?, idempotencyKey?, summary?, sourceArtifacts?, draft } (ZPublicApiUpdateCourseStructure); the whole draft is validated by Zod (ZCourseImportDraftPayload). Every rule below is enforced; violating any one returns 400 with a real Zod message.
draft.course — REQUIRED objecttitle — REQUIRED, non-empty string.description — REQUIRED, non-empty string.type — REQUIRED, enum LIVE_CLASS | SELF_PACED | COMPLIANCE. Note: the draft course type has no PUBLIC (unlike top-level create_course, which does).locale — ZSupportedLocale, defaults 'en'. Must be one of en, hi, fr, pt, de, vi, ru, es, pl, da. NEVER 'en-US' — region-tagged locales are rejected. Use bare 'en'.compliance — REQUIRED only if type === 'COMPLIANCE' (see compliance section). Optional otherwise.metadata — optional.draft.sections[] — REQUIRED array, .min(1) (cannot be empty)Each: externalId (REQUIRED non-empty string), title (REQUIRED non-empty), order (REQUIRED int ≥ 0).
draft.lessons[] — REQUIRED array, .min(1)Each: externalId (REQUIRED), sectionExternalId (REQUIRED — must match an existing sections[].externalId), title (REQUIRED), order (REQUIRED int ≥ 0), isUnlocked?, public?.
draft.lessonLanguages[] — REQUIRED array, .min(1)This is where lesson content lives (separated from the lesson row so a lesson can have multiple locales). Each:
lessonExternalId — REQUIRED — must match an existing lessons[].externalId.locale — REQUIRED, ZSupportedLocale enum (no default here — you must supply it; use 'en', never 'en-US').content — REQUIRED non-empty HTML string. LOAD-BEARING HTML RULE: lesson body HTML only. Do NOT include the lesson title. Do NOT use <h1> or <h2> anywhere. Start headings at <h3> (h3 is the highest heading level allowed in lesson content). Lists, <p>, <strong>, etc. are fine.Every course needs at minimum: 1 section + 1 lesson + 1 lessonLanguage. Omitting any of these three (or sending
[]) is the most common 400.
externalId, sectionExternalId, lessonExternalId are local linking keys you invent — e.g. "sec-1", "lesson-intro", "quiz-fundamentals". They are not uuids and not server ids. Do not generate uuids for them; do not reuse a server id. They must be unique within their kind, and every reference (lessons[].sectionExternalId, lessonLanguages[].lessonExternalId, exercises[].lessonExternalId/sectionExternalId) must point at an externalId that exists in the draft. The only uuid in this whole call is the top-level courseId.
draft.exercises[] (quizzes) — optionalEach: externalId (REQUIRED unique string), title (REQUIRED), optional lessonExternalId/sectionExternalId (must reference existing items if set), order?, description?, dueBy?, questions?.
questions[]: question (REQUIRED), questionTypeId (numeric 1–15, NOT a string — e.g. 1=RADIO, 2=CHECKBOX, 4=TRUE_FALSE, 5=SHORT_ANSWER), points?, order?, settings?, options?.options[]: label (REQUIRED), isCorrect (REQUIRED bool), settings?. When authoring options, mark at least one correct and give choice-type questions ≥2 options — the service layer applies per-type rules on publish even though the draft import accepts looser shapes.draft.tags[] — optional, default []Trimmed non-empty strings, unique (case-insensitive), max 100 tags, and each tag ≤ 80 chars.
sections[].externalId unique. 2. lessons[].externalId unique. 3. exercises[].externalId unique. 4. lessons[].sectionExternalId resolves to a section. 5. lessonLanguages[].lessonExternalId resolves to a lesson. 6. exercises[].lessonExternalId/sectionExternalId (if set) resolve. 7. tags unique.If draft.course.type === 'COMPLIANCE' (or you create/update a course to COMPLIANCE), compliance is required — omitting it 400s with error path compliance. ZComplianceSettings:
retakeIntervalMonths — REQUIRED, int 1–120.gracePeriodDays — int 0–365, default 0.reminderDaysBefore — int[] (each 1–365, max 10 entries), default [30,7,1].isMandatory — bool, default true.framework — HIPAA|OSHA|SOX|GDPR|PCI_DSS|FERPA|ISO|CUSTOM, nullable/optional.maxRetakeAttempts — int ≥1, nullable/optional.passingScore — int 0–100, default 80.update_course_structure with courseId = the uuid from create_course. This validates: required course strings present; type=COMPLIANCE so compliance supplied with retakeIntervalMonths; sections/lessons/lessonLanguages each non-empty; all externalIds arbitrary unique strings that resolve; lesson content starts at <h3> with no <h1>/<h2> and no repeated title; quiz uses questionTypeId: 4 (TRUE_FALSE) with 2 options, one correct.
{
"courseId": "<COURSE_UUID_FROM_create_course>",
"mode": "merge",
"idempotencyKey": "carrco-import-2026-06-17-001",
"draft": {
"course": {
"title": "Forklift Safety Certification",
"description": "OSHA-aligned forklift operation and safety course for warehouse crew.",
"type": "COMPLIANCE",
"locale": "en",
"compliance": {
"retakeIntervalMonths": 12,
"gracePeriodDays": 30,
"reminderDaysBefore": [30, 7, 1],
"isMandatory": true,
"framework": "OSHA",
"passingScore": 80
}
},
"tags": ["safety", "osha", "warehouse"],
"sections": [
{ "externalId": "sec-fundamentals", "title": "Fundamentals", "order": 0 },
{ "externalId": "sec-operation", "title": "Safe Operation", "order": 1 }
],
"lessons": [
{
"externalId": "lesson-intro",
"sectionExternalId": "sec-fundamentals",
"title": "Why Forklift Safety Matters",
"order": 0,
"isUnlocked": true
},
{
"externalId": "lesson-controls",
"sectionExternalId": "sec-operation",
"title": "Controls and Pre-Operation Inspection",
"order": 0
}
],
"lessonLanguages": [
{
"lessonExternalId": "lesson-intro",
"locale": "en",
"content": "<h3>The Cost of Carelessness</h3><p>Forklifts cause thousands of workplace injuries every year. This lesson covers why disciplined operation protects you and your crew.</p><h3>What You Will Learn</h3><ul><li>Common hazard categories</li><li>Your responsibilities as an operator</li></ul>"
},
{
"lessonExternalId": "lesson-controls",
"locale": "en",
"content": "<h3>Knowing Your Machine</h3><p>Before every shift, complete the pre-operation inspection.</p><h3>Pre-Operation Checklist</h3><ol><li>Check tires and forks</li><li>Test horn and brakes</li><li>Verify load capacity plate</li></ol>"
}
],
"exercises": [
{
"externalId": "quiz-fundamentals",
"sectionExternalId": "sec-fundamentals",
"lessonExternalId": "lesson-intro",
"title": "Fundamentals Knowledge Check",
"order": 0,
"questions": [
{
"question": "Forklift operators must complete a pre-operation inspection before each shift.",
"questionTypeId": 4,
"points": 1,
"order": 0,
"options": [
{ "label": "True", "isCorrect": true },
{ "label": "False", "isCorrect": false }
]
}
]
}
],
"warnings": []
}
}
For a non-compliance course, set course.type to SELF_PACED (or LIVE_CLASS) and drop the compliance block entirely — it is only required for COMPLIANCE.
The client.ts fix now surfaces the real Zod validation message from the API in the tool error. When update_course_structure (or any create/update) returns 400:
draft.lessonLanguages.0.locale: Invalid enum value, draft.sections: Array must contain at least 1 element(s), draft.course.compliance: Required, or draft.lessons.1.sectionExternalId: ... must reference an existing section.'en-US' instead of 'en'; an empty sections/lessons/lessonLanguages array; a lessonExternalId that doesn't match any lesson; a COMPLIANCE course missing compliance; <h1>/<h2> in lesson content; a uuid used where an arbitrary externalId belongs.idempotencyKey to keep the retry safe).Other status codes: 401 = missing/malformed Authorization: Bearer key. 403 = key lacks public_api:* scope (or a plan-gated feature like a non-minimal landing theme). 404 = bad courseId. 409 = certificate already issued for that completion record.
Five tools are flagged destructive and auto-inject a confirm: boolean param. They refuse unless confirm: true is passed:
delete_course, remove_member, delete_program, delete_newsfeed, revoke_certificate.
These are irreversible. Do not pass confirm: true on your own initiative — confirm the specific target with the operator first (name the course/member/program/certificate being destroyed), then pass confirm: true only after explicit go-ahead. The non-destructive update_course_structure with mode: 'replace' also overwrites structure — treat a replace as semi-destructive and confirm before overwriting existing content.
create_course → then update_course_structure → then update_course { isPublished: true }.get_course_structure (read current externalIds) → update_course_structure with mode: 'merge'.update_course_structure with mode: 'replace' (confirm first).invite_member / assign_to_courses (admin). Group into a program → create_program + add_program_members (roleId 2=tutor, 3=student).set_certificate_design / issue_certificate (admin).Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub branded-mayhem-collective-llc/carrco-academy-authoring --plugin carrco-academy-authoring