Automate with MCP
MCP tool reference
Every tool the MCP endpoint offers, grouped by scope, with its arguments - generated from the tool registry of this release.
CourseForge 5.3.0 offers 107 tools in 11 groups. This page is generated from the tool registry itself, so it lists exactly what tools/list returns to a connection with every scope. Each tool also carries protocol annotations - whether it reads or writes, whether it is destructive, whether it spends credit - which clients may use to warn before calling it.
reads changes nothing writes changes data destructive deletes something spends credit calls the account's own AI provider admin administrators only * required argument
The groups#
| Group | Tools | What it is for | Who | Spends |
|---|---|---|---|---|
courses |
6 | List, read, create, rename and delete courses. | everyone | no |
research |
3 | Take the research assignment for a course, and store what searching the web found. The client does the searching, so nothing here spends anything. | everyone | no |
structure |
6 | Read the outline brief, write an outline, and have CourseForge design one itself. | everyone | yes |
pages |
8 | List and read pages, take a writing brief, store a finished page, and generate one through the profile's model. Also retitles pages and chapters and rewrites a chapter description, which edits the stored outline, because chapters and pages are matched by title. | everyone | yes |
details |
5 | The thirteen switchable elements and seven values, at course, chapter or page level. | everyone | no |
tags |
8 | The tag library and what is tagged with what. | everyone | no |
runs |
8 | Start, watch and cancel background and batch runs. This is the group that spends money at scale. | everyone | yes |
profiles |
25 | AI accounts, models and BookStack instances. Keys are never readable, but they can be replaced. | everyone | no |
publish |
5 | Push a course into BookStack and resolve cross references. | everyone | no |
account |
9 | Who you are connected as, your own password and display name, and your own connections. Never anybody else's. | everyone | no |
admin |
24 | Accounts, settings, the cron token, diagnostics and updates. Only ever available to an administrator. | administrators | no |
How to connect, narrow a connection and choose between writing pages yourself and having CourseForge write them: Connect an AI client.
Courses#
List, read, create, rename and delete courses.
get_next_step#
Where a course has got to and the single next tool call that moves it forward, with the reason and the arguments. Call it with no course_id to be pointed at the right course, or with one to be told the next step for that course. Start here if you do not know the sequence, and come back to it whenever you lose track: the answer is worked out from the database every time, so it is correct even after a restart, and it never depends on remembering anything. The whole sequence runs without an AI account configured in CourseForge - you do the writing. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id |
integer | The course to report on. Omit to be pointed at one. |
list_courses#
Every course this account can see, with its progress: how many chapters and pages it has, how many are written, how many are published, and whether a generation run is open. An administrator sees every account's courses, each marked with its owner.
| Argument | Type | Description |
|---|---|---|
owner |
string | Administrators only: list one account's courses instead of all of them. |
get_course#
The full outline of one course: every chapter and page with its id, title and status, plus the book title, the topic, the resolved content details and the publishing state. Use it to see what still needs writing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
include_content |
boolean | Include the Markdown of the written pages. A finished course holds far more text than one answer can carry, so this fills up to about 300,000 characters of it and then stops, whole pages at a time; the outline always comes back complete either way. Leave it off unless you need the text. |
content_from_page_id |
integer | Where the text should start. Omit it for the beginning of the course; to read on past a previous answer, pass the page id that answer returned as content.next_page_id. Only meaningful together with include_content. |
create_course#
Creates an empty course from a one-line brief. It has no outline yet: follow this with generate_structure to have CourseForge design one, or with get_structure_brief and apply_structure to write one yourself. Pass web_research for a subject that moves - a framework, an API, a product, a standard - and every brief this course hands out afterwards asks for the current state of it to be established first, starting with the outline.
| Argument | Type | Description |
|---|---|---|
name * |
string | A short name for the course, for the course list. |
topic |
string | The brief the outline is designed from. One or two sentences saying what the course covers, for whom, and anything that must be assumed - tooling, prerequisites, depth. |
profile_id |
integer | The profile to generate with. Omit to use the only one, if there is only one. |
web_research |
boolean | Switches web research on for the whole course, rather than leaving it to the installation default. With it on, get_research_brief hands you the assignment for the subject, get_structure_brief asks for the outline to be designed against what you found, and every get_page_brief asks the page to be written against it and to cite sources. A client that can search the web - Claude Code, for instance - does that searching itself, so it costs nothing here; a course generated by CourseForge's own model instead uses that provider's search tool, which does cost. Leave it out to inherit the default. |
research_max_searches |
integer | Roughly how many searches one page may make while it is being written. 0 leaves it to whoever is doing the searching. Only meaningful with web_research on. |
update_course#
Renames a course, changes its brief, assigns a different profile, or sets the book title and description that are published to BookStack. Only the fields you give are changed.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
name |
string | A new name for the course. |
topic |
string | A new brief. This does not rewrite an existing outline by itself. |
profile_id |
integer | A different profile to generate with. |
book_title |
string | The title of the published book. |
book_description |
string | The description of the published book. |
bookstack_instance |
string | The id of the BookStack instance in the course's profile to publish into. list_profiles shows which a profile has. A course may publish into several at once: this replaces the first of them - naming a different instance means the course stops publishing to the one it names now - and leaves any others it has behind that first one alone. set_publish_targets writes the whole list, which is what to use to add a second destination rather than change the one there is. |
shelf_id |
integer | The BookStack shelf the book belongs on in that first instance. list_bookstack_shelves finds it. |
shelf_name |
string | The name of that shelf, for display. |
auto_tags |
boolean | Whether the outline generator may invent tags for the course, written as {{Tag}} markers in the outline. |
tag_pool |
string | A comma-separated list of tag names the generator should choose from. Leave empty to let it choose freely. |
tag_pool_strict |
boolean | Whether the generator must stay inside that pool. |
delete_course#
Removes a course and everything in it: chapters, pages, written content, tag links and run records. This cannot be undone, and it does not remove anything already published to BookStack. Requires the course name as confirmation.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
confirm_name * |
string | The exact name of the course, as a confirmation that the right one is being deleted. |
Research#
Take the research assignment for a course, and store what searching the web found. The client does the searching, so nothing here spends anything.
get_research_brief#
The research assignment for a whole course: what to establish about the subject before any of it is designed or written - the current stable version and its release date, what changed recently enough that a model would get it wrong, what has been deprecated and what replaced it, where the official documentation now lives, and what practitioners currently recommend. Search the web from this brief with your own tools, then send what you found to store_research. It is stored once on the course and then read by the outline and by every single page, so a course about something that moves - WordPress, a framework, an API, a standard - is researched one time rather than once per page. Costs nothing: no model is called and no search is run on this server.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
store_research#
Stores the findings from get_research_brief on the course and stamps them with today's date. From then on they travel into the outline brief and into every page brief, and the model is told when they were established so it can say which version a statement holds for. Send Markdown: short sections, dense bullets, and a closing "## Sources" list of what you actually read. Sending an empty string clears the findings. Replaces whatever was stored before - to add to a briefing rather than replace it, read the old one with get_research first and send both.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
findings * |
string | The research findings as Markdown. Anything past 12,000 characters is cut at a line boundary, because this text is carried in the context of every page of the course and is therefore paid for once per page. Send an empty string to clear. |
get_research#
The research findings stored on a course, with the date they were established and how old that makes them. Nothing here ever expires by itself - old findings still beat a guess - so this is what a client reads to decide whether the subject has moved far enough to be worth researching again. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
Outlines#
Read the outline brief, write an outline, and have CourseForge design one itself.
get_structure_brief#
The complete brief for designing a course outline, or for revising the one that already exists: the system instructions and user message CourseForge would send its own model, the course topic, the current outline, the requested changes, and the exact Markdown format that apply_structure parses. Pass feedback to be given the revision brief instead of the design brief - a revision must reproduce every unaffected title character for character, because pages are matched by title. Design the outline from this brief and send it to apply_structure. Costs nothing: no model is called.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
feedback |
string | The changes you want made to the existing outline. Omit to design one from scratch. |
apply_structure#
Replaces a course's outline with the Markdown you give it. Chapters and pages are matched by title: a page whose title is unchanged keeps its text and its details, a page with a new title starts empty, and a page that is no longer in the outline is deleted along with everything written on it. Always send the complete outline - anything left out is removed. Run preview_structure first when the Markdown did not come from get_structure_brief. If the outline would remove a page that has text on it, this refuses and names those pages unless confirm_removals is true, so an outline that only adds and renames applies in one call. Returns what was added, what was kept and what was removed. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
structure * |
string | The complete outline in CourseForge's Markdown format, as described by get_structure_brief. |
confirm_removals |
boolean | Set true only when you mean to delete written pages that this outline leaves out. Without it the call is refused and the pages at stake are named. |
generate_structure#
CourseForge calls the course's own model and designs a complete outline. Existing chapters and pages are matched by title, so a new outline over a course that has already been written would delete every page it does not name. When it would take a page that has text on it, the outline is handed back for you to look at instead of being applied, and applying it then costs nothing - so the model is never called twice over the same decision. Pass confirm_removals to have it applied whatever it removes. Pass feedback to revise the existing outline rather than design a new one. It blocks while the model works, which on a large course takes a minute or two. To design an outline without spending anything at all, use get_structure_brief and apply_structure instead.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
feedback |
string | The changes you want made to the existing outline. Omit to design one from scratch. |
confirm_removals |
boolean | Set true only when you mean to delete written pages the new outline leaves out. Without it the designed outline is returned unapplied and the pages at stake are named. |
plan_page_scopes#
Every page in an outline can carry a scope: one line, indented under its title, naming what that page and no other page teaches. Every page brief carries every page's scope, which is how a page knows what the others cover and leaves it to them. Outlines designed now come with scopes; this fills them in for pages that have none - an older outline, pages added by hand - in one request to the course's outline model, and writes them into the stored outline. Pass all to plan every page afresh. No page text is touched. To plan them without spending anything, add the scope lines to the outline yourself and store it with apply_structure.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
all |
boolean | Plan every page afresh instead of only the pages that have no scope. |
get_structure#
The stored outline of a course exactly as it is, plus what CourseForge reads out of it: the book title and description, how many chapters and pages there are, and every title. Read this before revising an outline, so that the titles you mean to keep stay byte-identical and their pages keep their text. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
preview_structure#
Parses outline Markdown and reports what CourseForge understood from it - the book title, the description, every chapter with its pages, and any {{Tag}} markers - without touching any course. It takes no course_id and changes nothing. Use it whenever an outline was assembled by hand, because apply_structure deletes every page the outline leaves out and there is no undo. Reports duplicate titles, chapters with no pages and anything the parser could not read. Costs nothing.
| Argument | Type | Description |
|---|---|---|
structure * |
string | The outline Markdown to check. |
Pages#
List and read pages, take a writing brief, store a finished page, and generate one through the profile's model. Also retitles pages and chapters and rewrites a chapter description, which edits the stored outline, because chapters and pages are matched by title.
get_page_brief#
The complete writing brief for one page: the system instructions and the user prompt CourseForge would send a model, the content details resolved for it, and the shared course context - the course map, in which every page carries the scope it alone teaches, plus the chapter it belongs to. Write the page from this brief and send the result to write_page. Omit page_id to be given the next page that has not been written yet. The shared context is the same for page after page: pass the ids you were given back as known_context, and while they still match that text is left out of the answer instead of being sent again. Costs nothing: no model is called.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
page_id |
integer | A specific page. Omit for the next unwritten one. |
feedback |
string | Revision instructions, when rewriting a page that already has text. |
known_context |
string | The shared_context_id and chapter_context_id you already hold from earlier briefs of this course, comma-separated. A segment whose id is listed is not repeated; one that has changed since - an edited outline, the next chapter - is sent again under a new id. |
write_page#
Stores the Markdown of one page. The text must follow the brief: GitHub-flavoured Markdown, no level-1 heading, no raw HTML. Returns how many pages are still unwritten, so a loop of brief - write - store knows when it is finished.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
page_id * |
integer | The page this text belongs to. |
content * |
string | The finished page, in Markdown. |
list_pages#
Every page of a course in reading order, with its chapter, status, word count and whether it has been published. Lighter than get_course when all you need is what to write next.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
only |
string | Which pages to return. One of all, written, unwritten, failed, unpublished. |
get_page#
One page in full: its title, its Markdown, its extra context, its status and where it sits in the course.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
page_id * |
integer | The page to read. |
update_page#
Changes a page's title, its scope, its Markdown, or the extra context that is added to its brief the next time it is written. Only the fields you give are changed. A new title or scope also rewrites the stored outline, because both are lines of it and chapters and pages are matched by title.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
page_id * |
integer | The page to change. |
title |
string | A new title. |
scope |
string | What this page and no other page teaches, as one line - concepts separated by semicolons. Every page brief shows every page's scope. An empty string removes it. |
content |
string | New Markdown for the page. |
extra_context |
string | Notes that are added to this page's brief - things this page in particular must cover, or must not. They are instructions to the writer, not text that appears on the page. |
update_chapter#
Changes a chapter's title or its description. Renaming a chapter rewrites the stored outline, because chapters are matched by title.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
chapter_id * |
integer | The chapter to change. |
title |
string | A new title. |
description |
string | A new description. This is part of what the pages are written from. |
generate_page#
CourseForge calls the course's own model and writes one page, start to finish, and stores it. This is the opposite of get_page_brief: the work happens on the server and is billed to the profile's AI account. It blocks until the page is finished, which on a long page can take minutes - to write a whole course, use start_run instead.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
page_id |
integer | The page to write. Omit for the next unwritten one. |
feedback |
string | Revision instructions, when rewriting a page that already has text. |
extra_context |
string | Notes to store on the page and add to its brief before writing it - things this page in particular must cover, or must not. Saved with the page, so a later rewrite carries them too. Omit to leave whatever the page already holds; update_page sets it without spending anything. |
fix_typography#
Sets the quotation marks, apostrophes, ellipses, dashes and spacing of a course, one chapter or one page the way the course language sets them - German gets โ and โ where the model wrote " and ", French gets its guillemets and its narrow spaces, an ellipsis becomes one character and a hyphen between two words becomes a dash. Code, links, formulas, HTML and anything the author escaped are never touched. Costs nothing: no model is called, the rules are all in code. page_id wins over chapter_id, which wins over the course, so course_id alone corrects the whole course. Send preview to be told what would change without changing it. Running it twice changes nothing the second time.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
chapter_id |
integer | One chapter and its pages. Omit for the whole course. |
page_id |
integer | One page. Omit for the whole chapter or course. |
language |
string | Set the text as this language instead of the course's own. |
preview |
boolean | Report what would change and write nothing. |
Content details#
The thirteen switchable elements and seven values, at course, chapter or page level.
get_detail_catalogue#
Every content detail this installation has, with what it does and its installation-wide default: the switchable features - summary, exercises, diagrams, formulas, flashcards, code examples, tables, callouts, emojis, auto links and the rest - and the values such as minimum and maximum length, diagram count, card count and audience. This is the only place the valid keys, the value types and the permitted ranges are listed, so read it before calling set_details. Takes no arguments and costs nothing.
No arguments.
get_details#
What the content details are at one level and where each of them comes from. Give course_id alone for the course, add chapter_id for one chapter, or page_id for one page - page_id wins over chapter_id, which wins over the course. Every detail is reported three ways: what this level stores of its own, what it would inherit if it stored nothing, and what actually applies to a page written here. The chain is installation default, then the course's profile (see set_profile_details), then course, chapter, page. Features are tri-state, so a stored 0 means inherited, never off. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
chapter_id |
integer | Read one chapter's level instead of the course's. |
page_id |
integer | Read one page's level. Takes precedence over chapter_id. |
list_detail_overrides#
Every chapter and page of one course that stores a detail of its own, and what it stores - the whole of what the Details tab calls "overrides below the course", in one call. get_details answers for one level and says nothing about the rest, so finding the three pages in a fifty-page course that were given exercises meant fifty calls. Levels that inherit everything are left out, because inheriting is the ordinary case and listing it would bury the answer. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
set_details#
Sets content detail overrides on the course, on one chapter or on one page. The ids choose the level: page_id wins over chapter_id, which wins over the course, so course_id alone changes the whole course. features is an object keyed by feature - 1 or true switches it on, -1 or false switches it off, 0 or null removes the override so that level inherits again. values is an object keyed by value - a number or a string sets it, null clears it back to inherited. Only the keys you send are touched; everything else stays as it was. Keys are checked against get_detail_catalogue and a wrong one is rejected with the list of valid ones. Returns the resolved details afterwards so you can see the effect. This changes what the next generation writes and never edits a page that already has text.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
chapter_id |
integer | Set the override on one chapter instead of the course. |
page_id |
integer | Set the override on one page. Takes precedence over chapter_id. |
features |
object | Feature keys mapped to 1 / true (on), -1 / false (off) or 0 / null (inherit). For example {"exercises": 1, "emojis": -1, "mermaid": 0}. |
values |
object | Value keys mapped to a number, a string, or null to inherit. For example {"min_length": 800, "diagram_max": 1, "audience": "experienced developers"}. |
reset_details#
Removes every content detail override at one level so it inherits everything again: a page falls back to its chapter, a chapter to its course, and the course to the installation defaults. The ids choose the level the same way as set_details - page_id wins over chapter_id, which wins over the course. Nothing else is touched: resetting a chapter leaves its pages' own overrides in place. The cleared overrides are returned, so the same set_details call can put them back if this was a mistake.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
chapter_id |
integer | Clear one chapter's overrides instead of the course's. |
page_id |
integer | Clear one page's overrides. Takes precedence over chapter_id. |
Tags#
The tag library and what is tagged with what.
list_tags#
One account's tag library: every tag with its name, its value, and how many courses, chapters and pages it is attached to. Tags belong to an account, not to a course, so the same tag is reused everywhere. An administrator sees every account's tags, each marked with its owner - a library stays your own unless you widen it. Costs nothing.
| Argument | Type | Description |
|---|---|---|
owner |
string | Administrators only: one other account's library. |
all |
boolean | Administrators only: widen to every account on the installation. Without it a listing is your own. |
create_tag#
Adds a tag to your library. It is attached to nothing yet, so this is only worth calling when you want the tag to exist before it is used - attach_tag creates a missing tag itself. A name already in the library is refused; change that one with update_tag instead. Costs nothing.
| Argument | Type | Description |
|---|---|---|
name * |
string | The tag name, as it will appear in BookStack. |
value |
string | The tag value. BookStack shows a tag as "name: value"; leave it out for a bare label. |
update_tag#
Changes a tag's name or value in the library, which changes it everywhere it is attached at once. Anything already in BookStack keeps the old tag until that course is pushed again, so follow this with publish_course for any course that is live. Only the fields you give are changed. Costs nothing.
| Argument | Type | Description |
|---|---|---|
tag_id * |
integer | The tag to change, as returned by list_tags. |
name |
string | A new name. |
value |
string | A new value. Send an empty string to clear it. |
delete_tag#
Removes a tag from the library and from every course, chapter and page it is attached to. This cannot be undone, and it does not remove the tag from anything already published - push those courses again afterwards. Requires the tag name as confirmation. To stop one tag being published from one place without losing it everywhere, use set_tag_link with enabled false instead. Costs nothing.
| Argument | Type | Description |
|---|---|---|
tag_id * |
integer | The tag to delete, as returned by list_tags. |
confirm_name * |
string | The exact name of the tag, as a confirmation that the right one is being deleted. |
get_course_tags#
Every tag in play anywhere in one course: what is attached at the course, at each chapter and at each page, which of those inherit downwards, which CourseForge created itself from {{Tag}} markers in the outline, and which are switched off. Each item also carries its effective tags - what would actually be published for it, its own plus everything inherited from above. Items with no tags at all are left out. Costs nothing: no model is called.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
attach_tag#
Attaches a tag to the course, to one chapter or to one page, adding it to the owner's library first if it is not there yet. Set inherit to have it apply to everything below as well - from the course to every chapter and page, from a chapter to its own pages - which is how a course-wide tag is done without one link per page. Attaching by hand also turns an automatic tag into a manual one and switches it back on. Call publish_course afterwards to push the change to BookStack. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
name * |
string | The tag name. A tag of this name is created if the library has none. |
value |
string | The tag value. This is the value in the library, so setting it here changes the tag everywhere it is used. Leave it out to keep whatever the tag already has. |
target |
string | What to attach it to: the whole course (the default), one chapter, or one page. One of course, project, chapter, page. |
target_id |
integer | The chapter id or page id, when target is chapter or page. |
inherit |
boolean | Also apply this tag to everything below the item. Ignored on a page, which has nothing below it. |
detach_tag#
Removes one link between a tag and a course, chapter or page. The tag stays in the library and stays attached to everything else. A tag CourseForge created from a {{Tag}} marker comes back the next time the outline is generated, so for one of those use set_tag_link with enabled false - that decision is kept. Name the tag with exactly one of tag_id and name, whichever you have to hand. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
tag_id |
integer | The tag to detach, as returned by get_course_tags. Give this or name, and not both. |
name |
string | The tag to detach, by name, looked up in the library of the account that owns the course - the same name attach_tag takes. Give this or tag_id, and not both. |
target |
string | Where to detach it from: the whole course (the default), one chapter, or one page. One of course, project, chapter, page. |
target_id |
integer | The chapter id or page id, when target is chapter or page. |
set_tag_link#
Changes an existing link without removing it: whether it inherits downwards, and whether it is switched on. A switched-off link is kept but ignored everywhere - it reaches no writing brief and no BookStack push - which is how an automatic tag is stopped from being published without deleting it, and unlike a detach it survives a regenerated outline. Give at least one of inherit and enabled. The tag has to be attached to that item already; use attach_tag if it is not. Name the tag with exactly one of tag_id and name, whichever you have to hand. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
tag_id |
integer | The tag whose link is being changed, as returned by get_course_tags. Give this or name, and not both. |
name |
string | The tag whose link is being changed, by name, looked up in the library of the account that owns the course. Give this or tag_id, and not both. |
target |
string | Which link: the one on the course (the default), on one chapter, or on one page. One of course, project, chapter, page. |
target_id |
integer | The chapter id or page id, when target is chapter or page. |
inherit |
boolean | Whether the tag also applies to everything below. Cannot be set on a page. |
enabled |
boolean | Whether the link counts at all. False keeps it but hides it from every brief and every push. |
Generation runs#
Start, watch and cancel background and batch runs. This is the group that spends money at scale.
start_run#
Starts writing a set of pages on the server, and returns at once. The run is recorded before any work begins, so it carries on after this client disconnects, after the session ends and across a restart of the server - start it, go away, and call poll_run or list_runs later to see where it got to. A live run is written by CourseForge's own scheduler in the background and therefore needs cron to be configured: when it is not, this tool starts nothing and says so, and the ways round it are to have an administrator set app.cron_token, to switch to a batch run, or to write pages one at a time with generate_page. A batch run hands the whole selection to the provider's own queue at roughly half the price and is answered within 24 hours, which is the right choice for a large course where time is not critical. mode auto reads the profile's page model: a model ending in ":batch" queues, anything else goes to the background worker. Call estimate_run first to see the size of it.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
pages |
any | Which pages to write. Omit it, or "unwritten", for every page that has no text yet - the usual case. "all" rewrites the whole course, pages that are already written included. "failed" retries the ones whose last attempt errored. Or give a list of page ids from list_pages. Pages already claimed by another run are left out of the keyword selections. |
chapter_id |
integer | Restrict the run to one chapter. It combines with pages: the selection is then made inside that chapter only. |
mode |
string | How the work is done. auto reads the profile's page model and is almost always right. live means CourseForge writes the pages itself from the scheduler; batch means the provider's queue, at about half the price, answered within a day. One of auto, live, batch. |
estimate_run#
What start_run would do with the same arguments, without doing it and without spending anything: how many pages, which model and provider, whether it would go to the background worker or to the batch queue, and roughly how many input and output tokens are involved. The input figure is measured from the real prompts - the same ones the run would send - sampled across the selection and scaled up, and the answer says how many pages were measured and at how many characters to the token. No price is given: CourseForge does not know what an account pays, and a confident number in the wrong currency is worse than a token count. Read the warnings: the usual one is that the provider has a batch queue nobody is using. The prompts it measures are built and thrown away and no model is ever called, so finding out what a run would cost is itself free. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
pages |
any | Which pages to write. Omit it, or "unwritten", for every page that has no text yet - the usual case. "all" rewrites the whole course, pages that are already written included. "failed" retries the ones whose last attempt errored. Or give a list of page ids from list_pages. Pages already claimed by another run are left out of the keyword selections. |
chapter_id |
integer | Restrict the estimate to one chapter. |
mode |
string | Which way to cost it. auto reads the profile's page model, as start_run would. One of auto, live, batch. |
list_runs#
Every run this account can see, newest first, with the course it belongs to, whether it is live or batch, its status, and how many of its pages are written, still pending, skipped or failed. This is the tool to call after reconnecting, to find out whether work ordered earlier is still going. only=open lists just the runs that have not ended; all and finished look at the 100 newest. Follow with get_run for the detail of one, or poll_run to make a batch check in now. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id |
integer | One course only. Omit for every course this account can see. |
only |
string | Which runs to return. open means anything that has not ended. One of all, open, finished. |
get_run#
One run in detail: its status, its mode, the model and provider behind it, the page counts, when it started, when it was last checked, and - for a batch - the provider's own id and the moment its results expire and stop being collectable. include_items adds every page in the run with its own status and error message, which is how you find out which three of five hundred pages failed and why. This reads what is stored and asks the provider nothing; poll_run is the one that goes out to the network. Costs nothing.
| Argument | Type | Description |
|---|---|---|
run_id * |
integer | The run, as returned by list_runs or start_run. |
include_items |
boolean | Include every page in the run with its own status and error. On a large run this is a long list. |
poll_run#
Asks the provider now whether a batch has finished, and writes home every page that has, instead of waiting for the next scheduler tick to do it. Use it when you want the answer in this conversation. On a live run there is no provider to ask: it reports where the background worker has got to and closes the run if the last page has landed. Safe to call as often as you like - the same page is never written twice - and it spends nothing beyond what the run has already committed. It reports how many pages arrived since the previous check. Costs nothing.
| Argument | Type | Description |
|---|---|---|
run_id * |
integer | The run to check. |
cancel_run#
Stops a run and releases the pages it was holding back to unwritten. At the provider it is best effort: a batch that has already finished cannot be cancelled, and is collected and written home instead of being thrown away. A page a live worker is writing at this very moment is left to finish, because it has already been paid for. Pages that were written before the cancel stay written - this stops the work, it does not undo it. Use delete_run afterwards to remove the record. Costs nothing.
| Argument | Type | Description |
|---|---|---|
run_id * |
integer | The run to stop. |
delete_run#
Removes the record of a run that has ended. This is bookkeeping only: every page the run wrote stays exactly as it is, and nothing is refunded or recalled. A run that is still open is refused - stop it with cancel_run first, then delete it. Costs nothing.
| Argument | Type | Description |
|---|---|---|
run_id * |
integer | The finished run to forget. |
scheduler_status#
Whether background work will actually be picked up on this installation: whether a cron token is configured, when the scheduler last called in, how many worker slots it has, and how many runs and pages are waiting for it. Ask before starting a live run if you want to be sure something will collect it, because a background run on an installation with no scheduler sits untouched for ever. A batch run can be submitted without the scheduler - the provider holds that work - but then its results have to be collected with poll_run. Costs nothing.
No arguments.
Profiles#
AI accounts, models and BookStack instances. Keys are never readable, but they can be replaced.
list_profiles#
The profiles this account can generate with: the id to pass to update_course, the AI accounts each one holds and what kind they are, whether each account has a key stored, the models chosen for the outline and the page slot, the BookStack instances, and the language. API keys are never included - only whether one is set. An administrator sees every account's profiles, each marked with its owner - a working set stays your own unless you widen it. Costs nothing.
| Argument | Type | Description |
|---|---|---|
owner |
string | Administrators only: one other account's profiles. |
all |
boolean | Administrators only: widen to every account on the installation. Without it a listing is your own. |
get_profile#
One profile in full: every AI account with its kind, base URL and whether a key is stored, every BookStack instance, both model slots with their temperature and token ceiling, the language, the concurrency, the courses using it, and the prompt slots this profile overrides - so you can see how it differs from the installation defaults. No credential is ever returned. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile id, as returned by list_profiles. |
include_prompt_text |
boolean | Include the full text of each prompt override rather than only which slots are overridden. These can be long. |
create_profile#
Builds a working profile in one call: one AI account, both model slots pointed at it, the language, and optionally a BookStack instance. Call list_providers first to see which account kinds this installation speaks and which of them need a key. The key is stored on the server and can never be read back - only replaced. The profile belongs to the account that creates it. Follow with check_profile to prove the credentials work and list_models to choose a model. Costs nothing.
| Argument | Type | Description |
|---|---|---|
name * |
string | A short name for the profile, for the picker. |
ai_kind |
string | The kind of AI account. Call list_providers for what each one is and whether it needs a key. One of openai, anthropic, gemini, openrouter, oai-compat. |
api_key |
string | The API key for the account. Required for every kind whose needs_key is true. Written only: no tool will ever hand it back. |
base_url |
string | The endpoint. Omit to use the default list_providers gives for this kind, which is right for everything but a self-hosted or proxied gateway. |
preset_key |
string | A row of the provider catalogue rather than a raw driver kind - "groq", "together", "deepseek", "llamacpp" and the rest, as list_providers reports them. Naming one picks the endpoint and the label with it, which is what the browser's provider picker does; ai_kind is then optional. Every preset runs on the OpenAI-compatible driver. |
organization |
string | The OpenAI organization id, for an account that belongs to one. |
site_url |
string | OpenRouter only: the site this traffic is attributed to. |
site_name |
string | OpenRouter only: the name shown beside that attribution. |
model |
string | The model that writes the pages - where the budget goes. Add ":batch" to route a whole course through the provider's queue at about half price. |
structure_model |
string | The model that designs the outline. One call per course, so it can be the expensive one. Omit to use the same model as the pages. |
language |
string | The language the course is written in. |
temperature |
number | Sampling temperature for both slots. 0.7 unless you have a reason. Ignored by reasoning models and by every Claude model. |
max_tokens |
integer | The token ceiling for one reply. 0 means the provider's own default, which is usually right. |
concurrency |
integer | How many pages a run writes at once. |
bookstack_url |
string | The BookStack instance this course publishes to, without a trailing slash. |
bookstack_token_id |
string | The BookStack API token id. Not a secret; it is readable back. |
bookstack_token_secret |
string | The BookStack API token secret. Written only: no tool will ever hand it back. |
update_profile#
Changes one profile. Only the fields you give are touched, and everything else - the other accounts, the prompt overrides - is left exactly as it was. An omitted or empty api_key keeps the stored key: passing "" does not clear it, and there is no way to blank a stored key at all, so an account whose key must go has to be replaced with a new one or the profile deleted and made again. Give ai_id when the profile holds more than one AI account, and bookstack_id when it holds more than one instance. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile to change. |
name |
string | A new name for the profile. |
ai_id |
string | Which AI account to change, when the profile holds more than one. get_profile lists the ids. |
ai_name |
string | A new name for that AI account, for the picker. |
ai_kind |
string | Change the account to another kind. The base URL follows the new kind's default unless you gave one yourself or had typed one. One of openai, anthropic, gemini, openrouter, oai-compat. |
api_key |
string | A new key, replacing the stored one. Empty or omitted keeps what is stored. |
base_url |
string | A new endpoint for the account. |
preset_key |
string | A row of the provider catalogue rather than a raw driver kind - "groq", "together", "deepseek", "llamacpp" and the rest, as list_providers reports them. Naming one picks the endpoint and the label with it, which is what the browser's provider picker does; ai_kind is then optional. Every preset runs on the OpenAI-compatible driver. |
organization |
string | The OpenAI organization id, for an account that belongs to one. |
site_url |
string | OpenRouter only: the site this traffic is attributed to. |
site_name |
string | OpenRouter only: the name shown beside that attribution. |
model |
string | The model that writes the pages. Add ":batch" to queue the run at about half price. |
structure_model |
string | The model that designs the outline. |
language |
string | The language the course is written in. |
temperature |
number | Sampling temperature. Applies to both model slots; ignored by reasoning models and by every Claude model. |
max_tokens |
integer | The token ceiling for one reply, for both slots. 0 is the provider default. |
concurrency |
integer | How many pages a run writes at once. |
bookstack_id |
string | Which BookStack instance to change, when the profile holds more than one. |
bookstack_name |
string | A new name for that BookStack instance, for the picker. |
bookstack_url |
string | The BookStack base URL. |
bookstack_token_id |
string | The BookStack API token id. |
bookstack_token_secret |
string | A new BookStack token secret. Empty or omitted keeps the stored one. |
bookstackdev_id |
integer | The BookStackDev look this instance wears, by id - list_bookstackdev_profiles has them. 0 takes the look off, back to plain BookStack. |
typography |
boolean | Whether a page generated with this profile has its punctuation set the way the course language sets it before it is stored. Omitted leaves the profile as it is; a profile that has never said follows the installation. This decides nothing about courses that are already written - fix_typography corrects one of those whatever this says. |
delete_profile#
Removes a profile and the credentials in it. The keys are gone and cannot be recovered, because nothing here can read them back to copy them elsewhere first. A profile a course still points at is refused, with the names of the courses - move them to another profile with update_course before deleting this one. Requires the profile name as confirmation. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile to delete. |
confirm_name * |
string | The exact name of the profile, as a confirmation that the right one is being deleted. |
add_ai_account#
Adds another AI account to a profile that already has one - an OpenAI key beside an Anthropic one, a self-hosted gateway beside both, the Claude subscription beside a metered key. update_profile changes an account a profile already holds; this is how it gets a second. Call list_providers first for the kinds this installation speaks. Nothing points at the new account until you say so: use set_model_slot to send the outline or the pages through it. The key is stored on the server and can never be read back. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile to add the account to. |
kind |
string | The kind of AI account. Call list_providers for what each one is and whether it needs a key. One of openai, anthropic, gemini, openrouter, oai-compat. |
name |
string | A name for this account, which is how the model slots refer to it. Defaults to the provider's own label. |
api_key |
string | The API key. Required for every kind whose needs_key is true. Written only: no tool will ever hand it back. |
base_url |
string | The endpoint. Omit to use the default list_providers gives for this kind. |
preset_key |
string | A row of the provider catalogue rather than a raw driver kind - "groq", "together", "deepseek", "llamacpp" and the rest, as list_providers reports them. Naming one picks the endpoint and the label with it, which is what the browser's provider picker does; ai_kind is then optional. Every preset runs on the OpenAI-compatible driver. |
organization |
string | The OpenAI organization id, for an account that belongs to one. |
site_url |
string | OpenRouter only: the site this traffic is attributed to. |
site_name |
string | OpenRouter only: the name shown beside that attribution. |
delete_ai_account#
Takes one AI account out of a profile. Its key goes with it and cannot be recovered, because nothing here can read a key back to copy it elsewhere first. A model slot pointing at the account is left pointing at nothing, which stops that slot generating until it is pointed somewhere with set_model_slot - so the answer says which slots those were. Removing the last account of a profile is refused: a profile with no account cannot write a word, and deleting the profile is the honest way to say that. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile the account belongs to. |
ai_id * |
string | The account to remove. get_profile lists the ids. |
add_bookstack_instance#
Adds another BookStack instance to a profile that already has one - a staging wiki beside a live one, the wikis of two departments, a customer's install beside your own. update_profile changes an instance a profile already holds; this is how it gets a second, which is what a course needs before set_publish_targets can send it to more than one wiki. The token secret is stored on the server and can never be read back. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile to add the instance to. |
url * |
string | The BookStack base URL, without a trailing slash. |
token_id * |
string | The BookStack API token id. Not a secret; it is readable back. |
token_secret * |
string | The BookStack API token secret. Written only: no tool will ever hand it back. |
name |
string | A name for this instance, which is how a course names its destination. |
bookstackdev_id |
integer | The BookStackDev look this instance should wear, by id. Omit for plain BookStack. |
delete_bookstack_instance#
Takes one BookStack instance out of a profile, with its token. A course that publishes to that instance loses the destination and the record of the book it made there, so publishing to the same wiki later would create a second book beside the first - the call is refused with the names of those courses unless you send confirm. Nothing is deleted inside BookStack itself, here or ever. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile the instance belongs to. |
bookstack_id * |
string | The instance to remove. get_profile lists the ids. |
confirm |
boolean | Go through with it even though courses publish to this instance. Their destinations and the record of what was published there are forgotten. |
set_model_slot#
Sets the outline slot or the page slot on its own: which AI account it runs through, which model, and that slot's own temperature and token ceiling. update_profile sets both slots together, which is right when a profile has one account and wrong as soon as it has two - the outline is one call and can afford the expensive model, while the pages are hundreds and may want a cheaper one, a different provider, or ":batch" on the end to go through the provider's queue at about half price. Call list_models for the ids an account will accept. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile to change. |
slot * |
string | Which slot: "outline" designs the course structure, "page" writes the pages. One of outline, page. |
model |
string | The model id, as list_models reports it. Add ":batch" to route this slot through the provider's queue. |
ai_id |
string | The AI account this slot runs through. Omit to leave it where it points, or when the profile holds only one account. |
temperature |
number | Sampling temperature for this slot alone. 0.7 unless you have a reason. |
max_tokens |
integer | The token ceiling for one reply from this slot. 0 means the provider's own default. |
list_prompt_slots#
Every prompt slot this installation has: its key, the group it belongs to, what it is for, the placeholders it may use, and the wording currently in force for the installation - which is what a profile that overrides nothing uses. This is the read a profile needs before set_profile_prompts can change one, and unlike get_prompts it needs no administrator, because overriding a slot for one profile is not the same power as rewriting it for everybody. Costs nothing.
| Argument | Type | Description |
|---|---|---|
group |
string | One group only, as the group field of a slot reports it. |
include_text |
boolean | Include the full wording of each slot rather than only what it is for. These are long. |
set_profile_prompts#
Replaces the wording this profile uses for one or more prompt slots, leaving the installation's own prompts alone for every other profile. This is the per-profile half of get_prompts and set_prompts, which are the installation-wide pair and need an administrator: a profile override needs only the profile. Call get_prompts for the slot names and what each one is for, and get_profile with include_prompt_text to read back what this profile currently overrides. A slot named in reset goes back to the installation's wording; an empty string is a deliberate override that sends nothing for that slot, which is not the same thing. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile to change. |
prompts |
object | Slot name to the text this profile should use instead. Unknown slot names are refused rather than silently stored. |
reset |
array | Slot names to stop overriding, so the installation's wording applies again. |
set_profile_details#
Decides content details for every course written with this profile - which elements a page gets and the values that size them - one level above the course. The chain is: installation default, then this profile, then course, chapter, page, the closest level winning. A course inherits whatever the profile decides until it overrides it, so this is where a house style lives: "every course on this profile opens with learning objectives and aims at 1,500 words". Call get_detail_catalogue for the keys, the types and the ranges. features maps a key to 1 (on), -1 (off) or 0 (stop deciding, so the installation default applies again); values maps a key to a number or a piece of text, or null to stop deciding. reset_all drops everything this profile decided. Nothing already written changes - the next page generated on any course of this profile follows the new answer. Costs nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile to change. |
features |
object | Feature key to 1 (on), -1 (off) or 0 (inherit the installation default). Unknown keys are refused rather than silently dropped. |
values |
object | Value key to the number or text this profile decides, or null to stop deciding it. Numbers are clamped to the range get_detail_catalogue reports. |
reset_all |
boolean | Drop every content detail this profile decided, so its courses follow the installation defaults again. Can be combined with features and values, which are applied afterwards. |
list_models#
Asks the provider which models this account may actually call, and marks which of them its batch queue accepts. Writing a model as "claude-opus-5:batch" in a profile routes the whole course through that queue at about half the price, at the cost of waiting - up to a day for the answer, which is why the suffix belongs on the page slot and not on the outline. An empty batch list means the provider would not say, not that nothing can be queued: supports_batch is the answer then. This is a live call to the provider and can take a few seconds, but no provider charges for a model listing. Pass the chosen id back to update_profile as model or structure_model. Reaches the provider but buys nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile the account belongs to. |
ai_id |
string | Which AI account to ask, when the profile holds more than one. |
refresh |
boolean | Accepted and ignored: the list is fetched from the provider on every call, so it is never stale. |
check_profile#
Answers "does this account work" before a whole course depends on it, by making one live call and reporting what came back. For a key-based account that call is a model listing, which is the cheapest thing that proves the base URL and the key are both right, and no provider bills for it; for the Claude subscription account, which has no model listing to fetch, it reports whether the CLI is installed, whether it is signed in, and whether an API key in the server environment has quietly taken over the billing. Use list_models when the question is which models to choose rather than whether the credentials are good. Reaches the provider but buys nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile the account belongs to. |
ai_id |
string | Which AI account to check, when the profile holds more than one. |
list_providers#
Every kind of AI account this installation can speak: its key for ai_kind, its label, the base URL it defaults to, whether it needs an API key, whether it has a batch queue, and a line of guidance on what belongs there. Call this before create_profile - the catalogue is what create_profile validates ai_kind against, and it grows. Read from this installation, so nothing is contacted. Costs nothing.
No arguments.
list_bookstack_shelves#
The shelves the profile's BookStack instance offers, with their ids, so a course can be filed onto one when it is published. This is a network call to the BookStack server and also proves the token works. A course carries the shelf on its own publishing settings, not on the profile: pass the id you choose to update_course as shelf_id, with the name as shelf_name. Reaches the BookStack server but buys nothing.
| Argument | Type | Description |
|---|---|---|
profile_id * |
integer | The profile the instance belongs to. |
bookstack_id |
string | Which BookStack instance to ask, when the profile holds more than one. get_profile lists the ids. |
list_bookstackdev_profiles#
The looks this account has made for its BookStack instances: name, which instances wear it, which other addresses the link is allowed on, and the embed line. A look is a BookStackDev configuration - Shiki code highlighting, Mermaid, MathJax, link embeds, an audio player, external-link marks, a light/dark button, page styling - served to a wiki by one script tag. Costs nothing.
| Argument | Type | Description |
|---|---|---|
owner |
string | Administrators only: the account whose looks to list. |
all |
boolean | Administrators only: every account's looks. |
list_bookstackdev_options#
Every option a BookStackDev look has, grouped by feature, with its type, its default and its range - the keys create_bookstackdev_profile and update_bookstackdev_profile take in settings. Also the Shiki themes the code highlighter knows. Costs nothing.
No arguments.
get_bookstackdev_profile#
One look in full: every setting, the instances wearing it, every address the link works on, the embed line for a wiki's custom head, and the conventions check - whether the prompts of the profiles publishing into it agree with what it renders. Costs nothing.
| Argument | Type | Description |
|---|---|---|
bookstackdev_id * |
integer | The look, as returned by list_bookstackdev_profiles. |
create_bookstackdev_profile#
Makes a look with every feature switched on the way the shipped BookStackDev is, changed by whatever settings you give. Name the BookStack instances that should wear it (get_profile lists their ids) and any other address the link should work on - a wiki CourseForge holds no credentials for. The answer carries the embed line. Costs nothing.
| Argument | Type | Description |
|---|---|---|
name * |
string | What to call it, for the picker. |
settings |
object | Settings to change from the defaults, keyed by group then field - see list_bookstackdev_options. {"codeBlocks": {"themeDark": "github-dark"}} |
instance_ids |
array | BookStack instance ids, from get_profile, that wear this look. |
origins |
array | Other addresses the link may be loaded on, as origins: https://wiki.example.com. The instances above are allowed without being listed here. |
update_bookstackdev_profile#
Changes one look. Only what you give is touched: a settings object changes the fields it names and no other, instance_ids replaces the list of instances wearing it, origins replaces the list of extra addresses. Costs nothing.
| Argument | Type | Description |
|---|---|---|
bookstackdev_id * |
integer | The look to change. |
name |
string | A new name. |
settings |
object | Fields to change, keyed by group then field - see list_bookstackdev_options. |
instance_ids |
array | The complete list of instance ids that wear this look afterwards. |
origins |
array | The complete list of extra origins afterwards. |
delete_bookstackdev_profile#
Removes a look. Its link stops answering at once, and every instance wearing it goes back to plain BookStack. Requires the name as confirmation. Costs nothing.
| Argument | Type | Description |
|---|---|---|
bookstackdev_id * |
integer | The look to delete. |
confirm_name * |
string | The exact name of the look, as a confirmation that the right one is being deleted. |
rotate_bookstackdev_link#
Gives a look a new key, and so a new embed line. The old line is refused from this moment wherever it was pasted, which is how a link that got copied somewhere it should not have been is taken back. Every wiki using it has to paste the new line. Costs nothing.
| Argument | Type | Description |
|---|---|---|
bookstackdev_id * |
integer | The look whose link to regenerate. |
check_bookstackdev_conventions#
Whether the prompts of every profile publishing into an instance wearing this look agree with what the look renders: formulas written for delimiters it does not typeset, diagrams or formulas asked for while the feature is off. A finding about a prompt carries the recommended wording, which set_profile_prompts can write. Costs nothing.
| Argument | Type | Description |
|---|---|---|
bookstackdev_id * |
integer | The look to check. |
Publishing#
Push a course into BookStack and resolve cross references.
get_publish_status#
The publishing state of one course, worked out from CourseForge's own records without contacting BookStack: every BookStack instance the course publishes to with its shelf, its book and how much of the course is in each of them, how many chapters and pages are in sync, how many have changed since the last push, how many have never been pushed, how many pages have no text to push yet, and how many cross-reference markers would still fail to become links. A course may publish to several instances at once; the counts are folded across the ones that are switched on, so "in sync" means in sync everywhere and "changed" means at least one of them would be written to. Read this before publish_course to see what a push would do. It is counts and a summary rather than a list, so it stays small whatever the size of the course, and it changes nothing. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
publish_course#
Pushes the course into every BookStack instance the course points at. For each of them it creates the book if it is not there yet, puts it on that instance's chosen shelf, then creates or updates every chapter and every page, carrying the effective tags with them. Items that have not changed since the last push to that instance are skipped and an item that was deleted there is recreated, so re-publishing never duplicates anything. Pages with no text yet are skipped. Each instance holds its own book with its own ids, and a page's cross references point inside the wiki it is in, so the same page is written slightly differently to each. An instance that cannot be reached does not stop the others: its failure is reported and the push carries on. This is a long sequence of API calls against somebody else's live wiki, times the number of instances, and can take several minutes on a large course; existing chapters and pages are overwritten in place, and none of it can be undone from CourseForge. part "all" publishes the whole course and resolves cross references afterwards; part "chapter" or "page" publishes one item and needs target_id, creates the book and the parent chapter around it, and leaves cross references alone - call resolve_links after those. force re-sends items whose fingerprint says they are unchanged, which also overwrites edits made in BookStack by hand. The course has to say where it publishes first: set_publish_targets sets the whole list, and update_course still sets a single one as bookstack_instance. No model is called - this reaches the BookStack server but buys nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
part |
string | How much of the course to publish. "all" is the whole course and is the usual answer; "book" creates or updates the book itself and its shelf without touching a single chapter or page, which is how a renamed course or a moved shelf is pushed without rewriting five hundred pages; "chapter" is one chapter with its pages; "page" is a single page. The last two need target_id. One of all, book, chapter, page. |
target_id |
integer | The chapter id or page id to publish, required when part is "chapter" or "page" and not allowed otherwise. Ids come from get_course or list_pages. |
instances |
array | Publish to these BookStack instances only, named by their instance id. Leave it out to publish to every instance the course has switched on, which is the usual answer; name one when a single wiki was behind or failed and the others are already up to date. get_publish_status lists the ids. |
force |
boolean | Re-send every item even when its fingerprint says it is unchanged. Use this to repair a book somebody has edited in BookStack; their edits are replaced. |
resolve_links#
Rewrites the ( ๐ Title ) markers the writer left in the text into real BookStack links, using the URLs of what has already been published, and re-sends only the pages whose text actually changed. It calls no model and can be run as often as you like - the stored page always keeps the raw marker, so resolving is repeatable and never corrupts the source text. A reference whose target is not published yet keeps its marker and resolves on a later run; a reference that matches no chapter or page in the course goes out as plain text, and list_unresolved_links says which ones those are. A full publish_course already does this, so run it after publishing a single chapter or page, or after a page was rewritten. Each rewritten page is written back to the live wiki - this reaches the BookStack server but buys nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
instances |
array | Resolve into these BookStack instances only, named by their instance id. Leave it out for every instance the course has switched on. Each wiki gets links pointing inside itself. |
force |
boolean | Re-send every page that holds a marker, even one whose text has not changed. |
set_publish_targets#
Sets the whole list of BookStack instances a course publishes to, in one call. A course may have several: the same book written into a staging wiki and a live one, or into the wikis of two departments. Each entry names an instance the course's profile defines - list_profiles shows them - and may carry a shelf and a switch. The first entry is the one the course reports as its own book and shelf everywhere a single answer is wanted. An entry left off the list is forgotten together with the record of what was published into it, so the next push there creates a second book rather than updating the first: to stop publishing to a wiki without losing that record, send it with enabled false instead. Nothing here reaches BookStack and nothing already published is changed or deleted - this only says where the next push goes. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
targets * |
array | Every instance this course publishes to, in order. An empty list means the course publishes nowhere, which clears its book and shelf. |
list_unresolved_links#
Every cross-reference marker that does not turn into a link, and the page it sits on. Two kinds: markers whose title matches no chapter or page in this course, each with the closest existing title as a suggestion - those go out as plain text and are the ones people never notice - and markers whose target does exist but has not been published yet, which resolve by themselves once it is. This is what to check after a publish. It lists one row per affected page, so on a large course with many cross references the answer is long. Reads the stored text only: no model, no BookStack, nothing changed. Costs nothing.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
This account#
Who you are connected as, your own password and display name, and your own connections. Never anybody else's.
whoami#
The account this connection belongs to, its role, and what this installation of CourseForge can currently do - whether the scheduler is running, so background runs are possible, and how many courses the account can see.
No arguments.
get_my_account#
Everything this installation records about the account this connection belongs to - its user name, display name, role, when it was made and when it last signed in - together with every MCP connection it owns and the tool groups each of those carries. Passwords are stored only as hashes and tokens only as digests, so neither appears here. Costs nothing.
No arguments.
set_my_display_name#
Changes the name shown for this account in the interface and against its courses. The user name is the key everything is filed under and cannot be changed; this is only the friendly name beside it. An empty value falls back to the user name. Costs nothing.
| Argument | Type | Description |
|---|---|---|
display_name * |
string | The name to show for this account. |
change_my_password#
Sets a new password for this account. The current one has to be given and has to be right, so a connection that has been handed round cannot lock the owner out of their own installation. A password needs at least 10 characters and has to differ from the current one. This does not sign anything out and does not revoke any connection: open browser sessions and every MCP connection, this one included, carry on working. Costs nothing.
| Argument | Type | Description |
|---|---|---|
current_password * |
string | The password this account has now. Ask the person for it rather than guessing - a wrong value changes nothing and is refused. |
new_password * |
string | The password to set, at least 10 characters. Leading and trailing spaces are kept, because they are part of a password. |
list_my_connections#
The MCP connections this account owns: the name each was given, the tool groups it is limited to, when it was made, when it was last used, how often, and whether it still works. Two things stop one working without removing it - passing its expiry date, and being older than the last password an administrator set for this account. Only connections belonging to this account are ever listed, whatever its role. Tokens are stored as hashes and can never be shown again. Costs nothing.
No arguments.
revoke_my_connection#
Deletes the token of one connection belonging to this account, so the next request it makes is refused. Revoking the connection you are talking through ends this conversation immediately and no further tool call will work - and this tool cannot tell you whether that is the one, because a handler is told which account is calling and never which connection. Call list_my_connections first and match the name against the client you are running in. This cannot be undone: a replacement has to be made from the Connect screen in the browser. Requires the connection name as confirmation.
| Argument | Type | Description |
|---|---|---|
connection_id * |
integer | The connection to revoke, as returned by list_my_connections. |
confirm_name * |
string | The exact name of that connection, as a confirmation that the right one is being cut off. |
list_scopes#
Every tool group this installation has: its key, what it covers, whether it needs an administrator and whether anything in it spends money on the AI account. It also says which groups this account may hold at all, and which the connection you are talking through holds right now - which is the ceiling on what create_my_connection will issue. Costs nothing.
No arguments.
create_my_connection#
Mints a token for another Claude client to connect with, in this account's name - the same thing the Connect screen does, for setting a second machine up without opening a browser. The token is returned exactly once and is never recoverable: the database keeps only a hash. A new connection may hold no tool group that the connection asking for it does not already hold, so a narrowed token can copy itself or make something smaller and never something larger; naming a group this connection lacks is refused rather than quietly dropped. Leaving scopes out means the same groups this connection has. Call list_scopes for the keys. Costs nothing.
| Argument | Type | Description |
|---|---|---|
name * |
string | What to call the connection, so it is recognisable in the list. |
scopes |
array | Tool groups the new connection may use, by key. Omit for the same groups this connection holds. |
ttl_days |
integer | Days until it expires by itself. 0 means it never does, which is what the browser offers by default. |
note |
string | A line about which machine it is for, shown beside it in the list. |
rename_my_connection#
Changes the name or the note on a connection belonging to this account. Only those two: the tool groups and the expiry are what the token was issued to do, and widening them afterwards would make the record of what was issued a lie - to change either, revoke it and issue another. An omitted field keeps what is stored. Costs nothing.
| Argument | Type | Description |
|---|---|---|
connection_id * |
integer | The connection to rename, as returned by list_my_connections. |
name |
string | A new name. |
note |
string | A new note. |
Administration#
Accounts, settings, the cron token, diagnostics and updates. Only ever available to an administrator. Administrators only.
transfer_course#
Administrators only. Moves a course to another account. Its profile belongs to the old owner and will not resolve for the new one, so the assignment is cleared and has to be made again.
| Argument | Type | Description |
|---|---|---|
course_id * |
integer | The course id, as returned by list_courses. |
to * |
string | The user name of the account that should own the course. |
list_users#
Every account on this installation: user name, role, whether it is disabled, when it last signed in, and how much it owns - courses, pages, profiles, tags and connections. Read this before changing or deleting an account, because those content counts are what a deletion is actually about. Costs nothing.
No arguments.
create_user#
Creates an account. Leave password out and CourseForge generates one and returns it in this answer exactly once - only a hash is stored, so it cannot be read again afterwards. The account is asked to choose its own password at the first sign-in unless must_change_password is false. To let somebody create their own account instead of being handed a password, use issue_invite. Costs nothing.
| Argument | Type | Description |
|---|---|---|
username * |
string | The user name to sign in with. Letters, digits, spaces and . _ @ + - are allowed. |
role |
string | user owns its own courses and nothing else. admin can also manage accounts, settings and updates. Defaults to user. One of user, admin. |
password |
string | A password of at least 10 characters. Omit it to have one generated and returned here. |
display_name |
string | The name shown in the interface. Defaults to the user name. |
must_change_password |
boolean | Ask this account to choose a new password at the first sign-in. Defaults to true. |
update_user#
Changes one account: its role, its display name, whether it is disabled, or its password. Only the fields you give are changed. Setting a password here replaces the old one without knowing it, and asks the account to choose a new one at the next sign-in. The last enabled administrator cannot be demoted or disabled, and this connection's own account cannot demote or disable itself. Costs nothing.
| Argument | Type | Description |
|---|---|---|
username * |
string | The account to change, as returned by list_users. |
role |
string | A new role. One of user, admin. |
display_name |
string | A new display name. |
disabled |
boolean | true refuses every sign-in and every connection token this account owns, without deleting anything. false lets it back in. |
password |
string | A new password, of at least 10 characters. Setting one cuts off every MCP connection the account made before now, because a reset is how somebody who has held this password is cut off. |
delete_user#
Removes an account. content says what happens to everything it owns: "delete" destroys its courses, pages, profiles, tags and connections, and "transfer" hands them to transfer_to, or to you if that is left out. Deleted content cannot be recovered, so confirm_username has to match the account name exactly. Call list_users first to see how much is at stake. Costs nothing.
| Argument | Type | Description |
|---|---|---|
username * |
string | The account to delete. |
content * |
string | What happens to the courses, profiles, tags and connections this account owns. One of delete, transfer. |
transfer_to |
string | The account that inherits the content when content is "transfer". Defaults to the account this connection belongs to. |
confirm_username * |
string | The exact user name again, as a confirmation that the right account is being deleted. |
issue_invite#
Returns a fresh invite code exactly once, so somebody can create their own account from the sign-in screen rather than being handed a password over a chat. The database keeps only a hash, so the code cannot be read back later - pass it on now. Several invites may be open at once, each with its own label, role, expiry and number of places; issuing one leaves the others alone. It is good for a single account unless max_uses says otherwise. Costs nothing.
| Argument | Type | Description |
|---|---|---|
role |
string | The role the account created with this code gets. Defaults to user. One of user, admin. |
label |
string | Who this invite is for - "marketing team", "the contractor" - so it can be told from the others in list_users and revoked by name. Up to 80 characters. |
ttl_hours |
integer | How long the code stays valid. Defaults to 48 hours. |
max_uses |
integer | How many accounts this one code may create. Defaults to 1. Anything above that is a code worth several accounts to whoever finds the file it is written in, so raise it only for a group you are expecting. |
revoke_invite#
Cancels an open invite, so the code stops working and nobody else can redeem it. list_users shows the open invites with their ids and labels; name one with invite_id, or leave it out to revoke the newest. Accounts already created from it keep working - revoking closes the door, it does not undo who came through. Refused when there is no open invite. Costs nothing.
| Argument | Type | Description |
|---|---|---|
invite_id |
integer | The invite to revoke, from list_users. Omit for the newest open one. |
get_cron_url#
The finished URL a scheduler calls to run background and batch work, built from the cron token this installation already has. generate_cron_token returns the same URL but makes a new token first, which stops every scheduler still calling the old one - so this is the tool for "what should I paste into cron", and that one is for "the secret has leaked". Refused when no token has been generated yet. Costs nothing.
No arguments.
list_settings#
Every setting this installation has: key, label, description, type, the current value, the value the release ships, and whether this installation has overridden it. The value of a secret - the cron token, the GitHub token - is never returned; you are told only whether one is stored. Use the keys from here with set_settings. Costs nothing.
| Argument | Type | Description |
|---|---|---|
group |
string | One group only: general, content, scheduler, batch, updates, mcp, security, timeouts. Omit for all of them. |
set_settings#
Writes one or more settings. values is an object of setting key to new value, and every value is validated against the catalogue before anything is written, so one bad value changes nothing at all. An unknown key is refused and the valid ones are listed. An empty string for a secret leaves the stored secret alone rather than clearing it - reset_setting clears one. Call list_settings first for the keys, their types and their ranges. Costs nothing.
| Argument | Type | Description |
|---|---|---|
values * |
object | Setting key to new value, for instance {"app.name": "Team CourseForge", "app.cron_workers": 4}. |
reset_setting#
Drops this installation's override for one setting, so the value the release ships applies again. Use it to undo a change without having to know what the default was. Resetting app.cron_token clears the cron secret and stops background runs until generate_cron_token makes a new one. Costs nothing.
| Argument | Type | Description |
|---|---|---|
key * |
string | The setting to reset, as returned by list_settings. |
generate_cron_token#
Makes a new cron secret, stores it, and returns the finished URL to paste into a hosting control panel as a job that runs once a minute. This URL is what makes background and batch runs possible at all: with no token, cron.php answers 404 to everybody and long runs are not offered. The token is returned here once and cannot be read back afterwards, and any scheduler still calling the old URL stops working the moment this returns - so only generate a new one when you are able to update the scheduler as well. Costs nothing.
No arguments.
set_up_php#
Reads the PHP configuration this host gives CourseForge, works out which limits are below what it needs, and writes a .user.ini raising exactly those. Every number is a floor and every run only ever raises: a directive already set is never removed or lowered, and a limit the host is generous about is left alone. Reports what it could not change, because some directives are fixed by the host and a line for them would be ignored in silence. Idempotent - running it twice writes nothing the second time. Pass dry_run to see the plan without touching anything at all. Pass release to take CourseForge's settings back out and let the host's own values return, which is what moving to different hosting wants. Costs nothing.
| Argument | Type | Description |
|---|---|---|
dry_run |
boolean | Report what would happen, writing and storing nothing. |
release |
boolean | Remove CourseForge's block from .user.ini and let this host's own values return. They come back once PHP re-reads the file, which takes up to user_ini.cache_ttl seconds. |
get_diagnostics#
The installation check: the PHP runtime and its extensions, directory permissions, the database, the scheduler, updates, the MCP endpoint and the AI providers, each as a row with a status and a hint about what to do. This is the first thing to call when something is not working. It reads only: nothing here is changed. Costs nothing.
No arguments.
get_audit_log#
Recent administrative actions - accounts created, roles changed, settings saved, connections revoked, updates installed - newest first, with who did it, when, from where, and whether it came through the web interface or through MCP. action matches the start of the action name, so "user" covers user.create, user.role and user.delete. The values of settings are never recorded here, only their keys. Costs nothing.
| Argument | Type | Description |
|---|---|---|
action |
string | Keep only actions starting with this, for instance "user", "settings" or "update". |
limit |
integer | How many entries to return. Defaults to 100. |
list_connections#
Every MCP connection on this installation: its name, the account it belongs to, the tool groups it is limited to, when it was made, when it was last used and how often. Tokens are stored only as hashes and can never be shown again. Use the connection_id from here with revoke_connection. Your own unless you name another account or ask for all of them. Costs nothing.
| Argument | Type | Description |
|---|---|---|
owner |
string | One other account's connections. |
all |
boolean | Administrators only: widen to every account on the installation. Without it a listing is your own. |
revoke_connection#
Deletes one connection's token, so the next request it makes is refused. Revoking the connection you are talking through ends this conversation immediately, and no further tool call will work - call list_connections first and be sure which one you have, and check the name against the one this conversation is using. This cannot be undone: a new connection has to be made from the Connect screen. Costs nothing.
| Argument | Type | Description |
|---|---|---|
connection_id * |
integer | The connection to revoke, as returned by list_connections. |
confirm_name * |
string | The exact name of that connection, as a confirmation that the right one is being cut off. |
check_for_update#
Asks GitHub whether a newer release exists and reports what is installed, what is available and whether the two differ. force ignores the cached answer and asks again, which is worth doing straight after a release and not much otherwise, because GitHub allows anonymous callers only sixty requests an hour per address. Nothing is downloaded or replaced here - install_update does that. Costs nothing.
| Argument | Type | Description |
|---|---|---|
force |
boolean | Ignore the cached answer and ask GitHub again. |
get_update_status#
What the updater knows: the installed version, when it last checked, whether an update is waiting, whether automatic updates are switched on, and which backups are on disk to roll back to. It answers from the cached check where it can and asks GitHub only when that cache has gone stale, so it is the cheap way to ask; check_for_update is the one that always goes out. Costs nothing.
No arguments.
install_update#
Downloads the latest release and replaces the application's own files with it. A backup of the current version is taken first, so a version that fails to start can be put back with rollback_update. This takes minutes and rewrites the code that is answering you, so do not run it while a generation run is writing pages, and expect this call to be slow. Call check_for_update first to see what would be installed, because confirm_version has to name it exactly. Settings and data are never touched: they live outside the release directory. Costs nothing.
| Argument | Type | Description |
|---|---|---|
confirm_version * |
string | The version that would be installed, exactly as check_for_update or get_update_status reports it under latest.version. The install is refused unless the two agree. |
rollback_update#
Puts the previously installed version back from the backup the last update took. Use it when a new version does not start or breaks something. With no backup on disk there is nothing to restore and the call reports that rather than doing anything. get_update_status lists what backups exist. confirm has to be true, because this replaces the running application with an older one and anything the newer version wrote to the database stays as it is. Costs nothing.
| Argument | Type | Description |
|---|---|---|
confirm * |
boolean | Must be true. Without it nothing is restored and the call reports what it would have done. |
get_update_history#
Every update and rollback this installation has attempted, newest first: which version it came from and went to, who asked for it, whether it was a manual or an automatic attempt, how it ended, the log it wrote and the error if it failed. This is where to look when a version is not the one somebody expected. Alongside it: the backups on disk and whether an update is running right now. Costs nothing.
No arguments.
get_prompts#
The prompt library: every instruction CourseForge sends a model, as a named slot you can read and rewrite. A slot is one piece of the eventual prompt - the persona the model writes as, the format contract an outline has to obey, the audience block, the tagging rules, the language instruction - and they are joined together for each job. Each slot comes back with its group, its label, what it is for, the placeholders it may use, the text this installation currently sends, the text the release ships, and whether the two differ. These are the installation-wide base layer: a profile may still override any slot for its own courses, and get_profile shows what a profile has changed. Costs nothing.
| Argument | Type | Description |
|---|---|---|
group |
string | One group of slots only, as returned in groups. Omit for the whole library. |
set_prompts#
Rewrites one or more prompt slots for the whole installation, which is how the house style of everything CourseForge writes gets tuned. prompts is an object of slot key to new text, and only the slots you name are touched. An empty string is not an empty prompt: it drops this installation's override and puts the shipped default back. An unknown slot key is refused and nothing is written. Keep the placeholders the slot declares - they are filled in at generation time and a slot that loses one silently loses the thing it named. Read get_prompts first for the keys, the current text and the defaults. This is the base layer only: a profile can still override any of these for its own courses. Costs nothing.
| Argument | Type | Description |
|---|---|---|
prompts * |
object | Slot key to new text, for instance {"global_system": "You are a patient technical writer."}. An empty string resets that slot to the shipped default. |