Document WebUntis

docs/API.md gains the findings: the endpoint and its required version
parameter, the one-time code and the two error codes worth naming, the Z that
means local time, a substitution being two periods, the unused exam module
that puts announced tests in the period notes, and a day without lessons that
is not a holiday. Those cost an afternoon of probing to learn and nothing
upstream states them.

docs/AUTH.md sets the key against the Schulcloud token it sits beside: no
password, nothing to keep alive, revocable on its own, and not read-only in
itself — which is why the allowlist exists. PI.md and DEPLOYMENT.md add the
four values, the container recreate a changed key needs, and the clock
requirement; the Pi's troubleshooting table gains both failure messages.

README and CLAUDE.md say what the server now is: Schulcloud for the material,
WebUntis for the day. Smoke is 89 checks with the index and a key, 87
live-only, 9 fewer without one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-17 20:43:24 +02:00
parent eaf9c7aa38
commit ccbf3ad3e9
8 changed files with 245 additions and 25 deletions

View File

@@ -1,8 +1,9 @@
# schulcloud-mcp
Read-only access to a [Schulcloud](https://github.com/hpi-schul-cloud) account —
courses, boards, lessons, tasks and files — for **Claude**, via MCP, and for
**you**, via a CLI that mirrors your coursework to disk.
courses, boards, lessons, tasks and files — plus the timetable from
[WebUntis](https://www.untis.at/), for **Claude**, via MCP, and for **you**, via
a CLI that mirrors your coursework to disk.
Both are front ends over one core library and one live Schulcloud session, kept
alive on a Pi.
@@ -16,8 +17,9 @@ instance, not inferred from the upstream source.
> *"What do I have due this week?"*
> *"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?"*
Twenty-four tools, all read-only:
Twenty-seven tools, all read-only:
| | |
|---|---|
@@ -45,6 +47,9 @@ Twenty-four tools, all read-only:
| `list_classes` | classes ("Klassen") with their teachers, and group membership |
| `list_news` | school and course announcements |
| `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 |
| `untis_lesson_topics` | what previous lessons of a subject actually covered ("Unterrichtsinhalt") |
`download_file` extracts text from **PDF, DOCX, XLSX, PPTX and OpenDocument**
files and returns **images inline** for Claude to look at. Image-only PDFs —
@@ -56,17 +61,20 @@ MCP resource (`schulcloud://courses/<id>`, `schulcloud://rooms/<id>`) holding
the same overview `get_course` and `get_room` return. In Claude Code, type `@`
and part of the course name.
**Two ready-made prompts**, in German because the school is:
**Three ready-made prompts**, in German because the school is:
| | |
|---|---|
| `zusammenfassung` *kurs* [*fokus*] | summarise a course or room: topics, tasks, key material, each with its source |
| `pruefungsvorbereitung` *kurs* [*thema*] [*datum*] | prepare for an exam: scope, explanations, practice questions, a study plan |
| `tagesvorbereitung` [*tag*] | prepare a school day: the lessons from WebUntis, what is new in those courses, what is due |
In Claude Code they run as `/mcp__schulcloud__zusammenfassung Mathe_10b`.
In Claude Code they run as `/mcp__schulcloud__zusammenfassung Mathe_10b`, or
`/mcp__schulcloud__tagesvorbereitung morgen` the evening before.
Claude Code splits arguments on spaces and drops extra words, so join words
with `_` (`Lineare_Funktionen`) and skip an optional argument with `-`. *kurs*
is any unambiguous part of a course or room name, or its id.
is any unambiguous part of a course or room name, or its id; *tag* takes
`heute`, `morgen`, `übermorgen`, `21.09.2026` or `2026-09-21`.
## The CLI
@@ -116,6 +124,19 @@ copy is the browser's own session token**, so a Schulportal tab left open will
auto-logout after ~2 hours and revoke this server's token with it. Copy the
token in a private window and close it. See [docs/AUTH.md](docs/AUTH.md).
**The timetable comes from somewhere else.** Schulcloud holds the material for
a lesson but not the lesson: this school's course `times` are empty and it
publishes its schedule in **WebUntis**. So the server reads that too, through
the API the Untis Mobile app uses — which authenticates with a key from Profil →
Freigaben and a time-based code per request, meaning no password, no session to
hold open and nothing that expires monthly. The two systems answer different
halves of the same question, and the German `tagesvorbereitung` prompt is where
they meet: which lessons happen today, what changed in their courses, what is
due. That key *can* write (the app may report an absence), so this side is kept
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).
**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
@@ -155,7 +176,7 @@ bypass, "what's new since…" — are sketched with their trade-offs in
```
src/
core/ client, types, board assembly, crawler, extraction, paths
core/ client, types, board assembly, crawler, extraction, paths, WebUntis
store/ Postgres: crawl generations, diffs, full-text search
indexer/ crawl → persist → mirror bytes → extract text → index
mcp/ MCP server, tools, resources and prompts
@@ -169,8 +190,9 @@ vendor/ upstream clones, git-ignored, for reference only
```
`core/` knows nothing about MCP, HTTP or the CLI: it holds the Schulcloud client,
the traversal every feature needs, document extraction, and the path
sanitisation that both the server's mirror and the CLI's sync depend on.
the traversal every feature needs, document extraction, the WebUntis client with
its one-time codes, and the path sanitisation that both the server's mirror and
the CLI's sync depend on.
## Development
@@ -186,7 +208,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 — 77 checks (79 with the index)
Streamable HTTP and exercises every tool against the live account — 87 checks (89 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.