Build courses, manage enrollments and publish Peerfold Sites pages from Cursor through the Peerfold MCP server.
# Authoring on Peerfold
Apply these whenever you create or change course content through the Peerfold MCP
server or the REST API. They are the four contracts an integration gets wrong.
## Never guess a block shape
A lesson body is an ordered array of blocks. A block is `{ type, props }`, plus
`children` on the container types. Ask for the shape before you write one:
1. `list_block_types` lists every type the registry allows, with its label, category,
whether it nests children, and its prop names.
2. `get_block_schema` returns one type's JSON Schema, its default props, and a
pasteable example node.
3. `validate_blocks` is a free dry run that returns the editor's exact per-block
errors. Keep the `normalized` array it hands back so block ids stay stable.
4. `update_lesson_blocks` is the write.
The same registry drives the editor, the renderer, SCORM export and these tools,
so content written this way renders the same as content typed by hand. Clients
that surface MCP resources can read the same reference at
`peerfold://block-schema-reference.json`.
## Read before every whole-document write
Several tools replace an entire document. Read the current state first, or you
delete whatever you did not resend.
| Replaces the whole thing | Read it with |
| --- | --- |
| `update_lesson_blocks` | `get_lesson_blocks` |
| `set_exam_questions` | `get_exam` |
| `set_access_rules` | `get_course` |
| `set_course_ceus` | `get_course_ceus` |
| `set_homepage` | `get_homepage` (plus `get_homepage_schema`) |
| `set_portal_menu` | `get_portal_menu` |
| `set_portal_footer` | `get_portal_footer` |
| `set_course_landing` and `set_program_landing` (the `sections` document) | `get_course` or `get_program`, plus `get_program_landing_schema` |
`set_course_settings` is the exception. It is a merge patch, field by field. Send a
value to change it, omit it to keep it, send `null` to clear it. `set_course_landing`
is split: its `enabled` flag merges and its `sections` document replaces. Each
argument is optional, so neither can undo the other by accident.
Every tool's arguments are a strict object. A misspelled or unsupported key is
refused by name. Fix the name; do not retry with more fields.
## Exam rules and exam questions are two different calls
`set_course_settings { exam }` carries the rules: passing score, attempts, whether
results are shown. The questions are a separate document. Read them with `get_exam`
and write them with `set_exam_questions`, which replaces the whole list. Omit a
question's `id` on first write and a stable id is minted. Send an id you read from
`get_exam` to edit that question in place, because a learner's in-flight attempt
keys on it.
## Publishing is explicit, and it is a revision
Nothing publishes as a side effect. Run `course_health` first. `warnings` and
`issues` are advisory; `blockers` is the only list that predicts a failed publish.
Then call `publish_course` on purpose, and only after the user has said yes in this
conversation. If it finds structural issues it refuses once and names them. Tell the
user what they are before you re-call with `confirm_issues: true`. The call returns
the revision number, the live learner URL and the admin review URL. `preview_course`
gives you a single-use link into the published revision as the reserved preview
member.
Published courses are immutable revisions. Learners keep reading the previously
published revision until a new one lands, so a half-finished draft edit is never
visible to anyone.
Every write is audit-logged with `actorType: "mcp"` and syncs to the connected CRM
through the standard outbox. An API write and a UI edit look the same downstream.