Document the H5P findings
docs/API.md gains the endpoint and what it returns, why `play` is the bigger and lesser of the two, the element-by-id route found while probing, and the per-library shapes with the traps in them — the string "true", the inline cloze markers, the correct-option-first convention, and the UI subtrees that drown an exercise in button labels. CLAUDE.md's "no quiz of its own" note said a contentId was the only handle onto the content. That was the reason nobody looked further, so it now says where to look instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
25
CLAUDE.md
25
CLAUDE.md
@@ -47,8 +47,8 @@ index.
|
||||
read-only with respect to Schulcloud. Run `smoke` after touching `src/core/`,
|
||||
`src/mcp/` or `src/http/` — the unit tests cover only pure functions.
|
||||
|
||||
Run smoke **both ways**: with `DATABASE_URL` set (89 checks, index-backed) and
|
||||
without (87 checks, live-only); without a WebUntis key both drop by 9, and the
|
||||
Run smoke **both ways**: with `DATABASE_URL` set (93 checks, index-backed) and
|
||||
without (91 checks, live-only); without a WebUntis key both drop by 9, and the
|
||||
run then asserts the `untis_*` tools are *not* offered. The degradation paths are
|
||||
supported modes, not fallbacks nobody exercises. Every Schulcloud check fails with 401 when the live
|
||||
session has lapsed — check the container's keepalive log before suspecting code.
|
||||
@@ -94,6 +94,9 @@ bin/cli.ts ──HTTP──────────┘ cli/{config,client,sync
|
||||
from Profil → Freigaben, so there is no session and no keepalive on this
|
||||
side. Resolves the payload's element ids to names and returns every day in
|
||||
a range, empty ones included. See Invariants for why the allowlist is there.
|
||||
- `h5p.ts` — the quizzes on a board. One GET per element, parsed into
|
||||
questions and answers; board assembly attaches it like a pad, the crawl
|
||||
indexes its text, and `get_h5p` prints it.
|
||||
- `dates.ts` — school days as `YYYY-MM-DD` in Europe/Berlin. The container
|
||||
runs UTC, so `schoolToday()` is not `new Date()`: at 00:30 in Erfurt the
|
||||
process clock still says yesterday, and a nightly briefing would prepare
|
||||
@@ -286,10 +289,20 @@ These cost real time to discover; `docs/API.md` has the full list with evidence.
|
||||
- **A room's `allowedOperations` is an object, not a list.** Every operation is
|
||||
present with a boolean; `false` means denied. Typing it as `string[]`
|
||||
type-checks and throws `.some is not a function` the moment anything reads it.
|
||||
- **Schulcloud has no quiz of its own.** There is no quiz module or endpoint
|
||||
upstream: interactive exercises are H5P elements, whose `contentId` is the
|
||||
only handle onto the content, or external (LTI) tools behind
|
||||
`contextExternalToolId`. Say that rather than looking for a quiz API.
|
||||
- **Schulcloud has no quiz of its own — a quiz is H5P.** There is no quiz module
|
||||
or endpoint upstream: interactive exercises are `h5p` elements carrying a
|
||||
`contentId`, or external (LTI) tools behind `contextExternalToolId`. Don't
|
||||
look for a quiz API; look for the H5P one.
|
||||
- **One request holds a whole quiz**: `GET /api/v3/h5p-editor/params/{contentId}`
|
||||
returns the JSON the player is fed — every question, every option and which
|
||||
are correct — even though the player shows one question at a time. Nothing to
|
||||
step through, no page to scrape, and `play/{id}` is the same content plus 24 KB
|
||||
of scripts, so `params` is both cheaper and complete. The H5P service is not
|
||||
in `docs-json` and has no document of its own; `core/h5p.ts` interprets the
|
||||
payload, whose shape belongs to the H5P library the teacher used. Model a new
|
||||
library there rather than in a tool, and keep the generic harvest as the
|
||||
fallback — an exercise arriving as "0 questions" is the bug that path exists
|
||||
to prevent.
|
||||
- **The file manager is a third store, reachable only as HTML.** Persönliche,
|
||||
Kurs-, Team- and Geteilte Dateien live in the legacy `files` collection, not in
|
||||
files-storage: `list_files` answers 0 for a course holding dozens of
|
||||
|
||||
14
README.md
14
README.md
@@ -18,8 +18,9 @@ instance, not inferred from the upstream source.
|
||||
> *"Find the material about Verschlüsselung and explain the Caesar cipher worksheet."*
|
||||
> *"Summarise the routing lesson from the LF10 course."*
|
||||
> *"What do I have tomorrow, and has anything been cancelled?"*
|
||||
> *"Quiz me on the DIN 5008 exercise from the DK room."*
|
||||
|
||||
Twenty-seven tools, all read-only:
|
||||
Twenty-eight tools, all read-only:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -46,6 +47,7 @@ Twenty-seven tools, all read-only:
|
||||
| `get_room` | one room: its boards, members and what you may do there |
|
||||
| `list_classes` | classes ("Klassen") with their teachers, and group membership |
|
||||
| `list_news` | school and course announcements |
|
||||
| `get_h5p` | an H5P exercise in full: every question, option and correct answer |
|
||||
| `api_get` | GET-only escape hatch for uncovered API surface |
|
||||
| `untis_timetable` | the school day from **WebUntis**: lessons, Entfall, Vertretung, room changes, period notes |
|
||||
| `untis_homework` | homework from WebUntis' class register — a separate list from Schulcloud's tasks |
|
||||
@@ -137,6 +139,14 @@ read-only by an allowlist of five read methods rather than by "GET only". Unset
|
||||
`UNTIS_*` and none of it exists — the tools are not even offered. See
|
||||
[docs/AUTH.md](docs/AUTH.md).
|
||||
|
||||
**A quiz is one request, not a wizard.** Schulcloud has no quiz of its own, so
|
||||
an exercise is an H5P element and the board hands over nothing but a content
|
||||
id. The player then shows one question at a time, which makes a quiz look like
|
||||
something to step through or scrape — it isn't: the endpoint the player loads
|
||||
returns the whole exercise, every option and every solution. So `get_h5p`
|
||||
prints all of it, `get_board` names it with its question count, and `search`
|
||||
reaches the question text like any other material.
|
||||
|
||||
**claude.ai gets a token of its own.** Its connector stores a request header,
|
||||
so `MCP_CONNECTOR_TOKEN` opens `/mcp` and nothing else — it is refused on
|
||||
`/api`, which can replace the Schulcloud token — and rotates without touching
|
||||
@@ -208,7 +218,7 @@ npm run typecheck
|
||||
```
|
||||
|
||||
`npm run smoke` starts the HTTP server, connects a real MCP client over
|
||||
Streamable HTTP and exercises every tool against the live account — 87 checks (89 with the index, 9 fewer without a WebUntis key)
|
||||
Streamable HTTP and exercises every tool against the live account — 91 checks (93 with the index, 9 fewer without a WebUntis key)
|
||||
covering the auth gate, the connector token and the secret path, the protocol handshake, every content chain, file
|
||||
extraction, resources and prompts, token replacement, `api_get`'s guard rails and error handling.
|
||||
|
||||
|
||||
47
docs/API.md
47
docs/API.md
@@ -225,6 +225,53 @@ hops and without Etherpad's API key; `core/etherpad.ts` does this. The url is
|
||||
built from the server's `ETHERPAD__PAD_URI`, so it must be checked against the
|
||||
instance host before the session cookie is sent to it.
|
||||
|
||||
### H5P elements are the quizzes, and one request holds a whole one
|
||||
|
||||
An `h5p` element carries nothing but `content: { contentId }`. The content
|
||||
itself comes from the H5P service, whose API lives under `/api/v3/h5p-editor/`
|
||||
— not in `docs-json`, and with no document of its own
|
||||
(`/api/v3/h5p-editor/docs-json` is a 404). The deployment's ingress table routes
|
||||
`/h5p/player` and `/h5p/editor`, which are the front-end apps, not this API.
|
||||
|
||||
- **`GET /api/v3/h5p-editor/params/{contentId}`** returns the JSON the player is
|
||||
fed: `{ h5p: <metadata>, library, params: { metadata, params } }`. The inner
|
||||
`params` is the exercise — **every question, every option and which are
|
||||
correct** — so the player showing one question at a time is a display detail,
|
||||
not a limit on what can be read. Bearer auth, same as everything else.
|
||||
- `GET /api/v3/h5p-editor/play/{contentId}` is the same content wrapped in the
|
||||
player's integration object: 74 KB against 51 KB for the live quiz below,
|
||||
because it carries script and style lists. `params` is both smaller and
|
||||
complete, so nothing needs `play`.
|
||||
- `GET /api/v3/elements/{elementId}` returns a single board element with its
|
||||
content, which is how an element's `contentId` can be re-read without its
|
||||
board. (`/api/v3/board/element/{id}` does not exist.)
|
||||
|
||||
The **shape inside `params` belongs to the H5P library** the teacher used, which
|
||||
is where the work is. Verified against the live quiz "Quiz zur formalen
|
||||
Gestaltung einer Projektdoku" (`H5P.QuestionSet`, 20 questions):
|
||||
|
||||
- A `QuestionSet` holds `questions: [{ library, params, subContentId }]`, each
|
||||
sub-content naming its own library — `H5P.MultiChoice 1.16` here. Any other
|
||||
main library *is* a single question, with the same `params` shape.
|
||||
- `H5P.MultiChoice`: `question` and `answers[].text` are HTML,
|
||||
`answers[].correct` is the solution, `answers[].tipsAndFeedback.tip` a hint,
|
||||
and `behaviour.singleAnswer` is what makes the player draw radio buttons —
|
||||
the only honest source for "tick exactly one".
|
||||
- `H5P.TrueFalse` stores `correct` as the **string** `"true"`/`"false"`, with
|
||||
the button labels in `l10n`.
|
||||
- Cloze libraries (`H5P.Blanks`, `H5P.DragText`, `H5P.MarkTheWords`) mark the
|
||||
solutions inside the text as `*answer:tip*`, alternatives separated by `/`.
|
||||
- `H5P.SingleChoiceSet` and `H5P.Summary` put the **correct option first** and
|
||||
let the player shuffle; nothing else marks it.
|
||||
- Every payload also carries `UI`, `l10n`, `behaviour` and `overallFeedback`
|
||||
subtrees of button labels and display settings. Anything that harvests text
|
||||
generically has to skip them, or the exercise reads as "Überprüfen,
|
||||
Wiederholen, Absenden".
|
||||
|
||||
`core/h5p.ts` models the libraries above and harvests the text of anything else
|
||||
under a label saying so — a teacher's exercise reported as "0 questions" would
|
||||
be worse than a clumsy rendering of it.
|
||||
|
||||
## The file manager ("Dateien") is a third store
|
||||
|
||||
Persönliche Dateien, Kurs-Dateien, Team-Dateien and Geteilte Dateien are the
|
||||
|
||||
@@ -107,9 +107,9 @@ node dist/bin/cli.js sync
|
||||
## Run the test suites
|
||||
|
||||
```bash
|
||||
npm test # 224 offline tests
|
||||
npm test # 239 offline tests
|
||||
npm run smoke # end-to-end against the live instance, live-only mode
|
||||
DATABASE_URL=… npm run smoke # end-to-end with the index (89 checks with a WebUntis key)
|
||||
DATABASE_URL=… npm run smoke # end-to-end with the index (93 checks with a WebUntis key)
|
||||
```
|
||||
|
||||
Store tests need a database and skip without one:
|
||||
|
||||
@@ -217,7 +217,7 @@ be pointed at the instance in between:
|
||||
|
||||
```bash
|
||||
eval "$(./scripts/mcp-env.sh)" # as the demo student
|
||||
cd .. && npm run smoke # 80 checks against the local instance
|
||||
cd .. && npm run smoke # 82 checks against the local instance
|
||||
```
|
||||
|
||||
`mcp-env.sh` points the index at its own database, `schulcloud_local`, and the
|
||||
|
||||
Reference in New Issue
Block a user