Skip to content

CourseForge 4.9.0

Choose a tag to compare

@ZcarecroW ZcarecroW released this 01 Sep 17:53
· 13 commits to main since this release
v4.9.0
44210d3

A word count said a finished page was empty

list_pages reported seven finished pages of a course as 194, 283, 954, 1177, 2036, 2586 and 2905 words long. They were 3,893, 4,677 and more. Nothing had been lost — the count was wrong, and wrong in the direction that destroys work: a model reading that list sees pages that look like failed generations and rewrites them, spending money to replace text that was already there.

The count ran the Markdown through strip_tags first. Markdown is not HTML: List<String>, Map<String, Object> and a written-out <div class="card"> are prose about code, and strip_tags removes each of them along with everything up to the next >. Worse, a single a<b with no later > anywhere on the page is an unterminated tag, so the rest of the document is discarded — a 2,408-word page came back as two words.

It now counts the page as it is stored, which is what get_page and the browser's own tree have always counted. Three surfaces answering "how long is this page" differently is worse than any one of them being wrong, because the difference reads as information.

Everything the browser can do, a connected client can do

That was the claim this project makes about its MCP surface. It was audited screen by screen against the tool registry, and it was untrue in eighteen places. This release closes them: thirteen new tools, and arguments added to five that existed.

Profiles

update_profile names the BookStack instance of a profile that has one, and would add one only when there was none — so a profile could be given its first instance and never a second, and the same for AI accounts. 4.7 made publishing a course into several wikis at once a headline feature, and a client setting an installation up over MCP could configure exactly one of them.

  • add_bookstack_instance, delete_bookstack_instance, add_ai_account, delete_ai_account — the Add and Remove buttons the browser has always had beside those two lists. Adding is its own verb rather than a special case of update, because an argument that edits when an id is given and creates when it is not turns a mistyped ai_id into a second account holding the key that was meant to replace the first. Removing an instance a course publishes to is refused with the names of those courses unless you confirm; removing the last AI account of a profile is refused outright.
  • preset_key on create_profile, add_ai_account and update_profile. The provider picker offers two dozen rows — Groq, Together, Fireworks, DeepSeek, a local llama.cpp — and most are the same OpenAI-compatible driver at a different endpoint. The tools took only the six raw driver kinds underneath them. organization, cli_path, site_url and site_name are now settable too.
  • set_model_slot sets one slot alone: its account, its model, its temperature, its token ceiling. update_profile writes both together, which is right for a profile with one account and wrong the moment it has two — the outline is one call and can afford the expensive model, while the pages are hundreds.
  • set_profile_prompts writes a profile's prompt overrides, which were readable and not writable, and list_prompt_slots reads the library naming them without needing an administrator. Overriding a slot for one profile is not the same power as rewriting it for everybody.
  • update_profile also finally takes typography, ai_name and bookstack_name.

Administration and content

  • revoke_invite, because an invite could be issued and not withdrawn.
  • get_cron_url, because the scheduler URL could only be read by rotating the secret in it first — which stops every scheduler already calling it.
  • list_detail_overrides answers "what does this course override, anywhere" in one call instead of one per page.
  • publish_course with part: "book" — the book and its shelf without the five hundred pages under it, which is what a renamed course needs.
  • generate_page with extra_context, which the browser writes in the same click that generates.

Connections

AccountTools said in its own docblock that no tool would ever create a connection, because a token able to mint another could mint a wider one and every narrowing on the Connect screen would become a formality. That argument is about escalation, not about creation — so the rule is now written down instead of the absence.

create_my_connection may issue nothing the calling connection does not already hold. An omitted scope list means "the same as mine" rather than "everything the account allows" — which is the trap, since that is what an empty list means by the time it is stored — and a group the caller lacks is refused by name rather than quietly dropped, because a token that silently does less than was asked for is worse than one that refuses. A connection can copy itself or make something smaller, never something larger.

The browser has no such limit, because a person signed in with a password is the account rather than a delegation of it. rename_my_connection and list_scopes go with it.

What is deliberately still absent

Signing in, redeeming an invite and first-run setup all create or resume the very credential a tool call needs before it can happen at all. revoke_my_connection is the nearest thing to signing out, and it is permanent rather than resumable. This is now written in docs.md beside the parity claim, so the next audit does not rediscover them as gaps.

Upgrading

Nothing to do, and no schema change. Existing connections keep the scopes they were issued with.


365 tests pass, 21 of them new. tests/mcp-parity.test.php carries the audit as a map of capability to tool name, so a tool renamed or dropped fails the suite rather than decaying quietly; the word-count test asserts the three surfaces agree rather than that any one of them is right, and fails against the old code.

Verified over real HTTP against a scratch database, through api/mcp.php with a minted token: 97 tools listed, list_pages and get_page agreeing at 494 words on the page shape that used to report two, a second BookStack instance and a Groq preset account added to a profile with no secret in either answer, the page slot pointed at the new account while the outline slot kept its own temperature, and a connection narrowed to two groups copying itself, refusing to widen itself, and not being shown the admin tools at all.