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:
42
README.md
42
README.md
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user