Read the user's own lesson notes, and the class register behind them

Schulcloud says what was uploaded and WebUntis says what was scheduled.
Neither says what was *taught* — which point the teacher laboured, which
example landed, what "will definitely come up". That lives in two places
this server could not reach: the notes the user takes in the lesson, and
WebUntis' class register.

Notes are a directory of Markdown files (NOTES_DIR), not a table. They
have to be writable from a phone in a classroom, readable when Postgres
is down, and outlive this project, and files are the only shape that is
all three — so the files are the truth and the index is a view of them,
the same split as file_texts and the mirror. list_notes and get_note read
disk, so they answer before the first crawl; search, what_changed and all
three German prompts read them alongside the Schulcloud material.

add_note writes one, and is the only thing in this server that writes
anything. That is not a hole in the read-only invariant but a different
store: it is bounded to NOTES_DIR by the same safeComponent/resolveWithin
pair that stops a hostile Schulcloud filename escaping the mirror, so a
note titled ../../.ssh/authorized_keys becomes a filename. Schulcloud and
WebUntis stay GET-only and allowlisted respectively. NOTES_READONLY
refuses writes outright.

Appending targets the *lesson*, not the title: "halt das auch noch fest"
mid-lesson carries a new title, and deriving the path from it would start
a second note every time, which is the one thing append exists to prevent.

Notes.app has no export — its bodies are compressed protobuf and the
iCloud copy is encrypted — so scripting the app is not the clumsy route
to the notes but the only one. scripts/export-apple-notes.js reads them
through AppleScript into one JSON object per line, and `schulcloud note
import` converts the HTML to Markdown, takes the Notes folder as the
subject and the *creation* date as the lesson's date. Attachments cannot
come across; a note that was a photo of the board imports as a line
saying so, because importing it empty would hide the loss.

The class register needed one API property to become cheap:
getLessonTopic2017 answers per *series*, not per period, so a term is
reconstructed by asking about the latest period of each lesson series and
merging back by id — a few dozen calls for a school year rather than one
per lesson. untis_lesson_topics now takes a subject as well as a period
id, and UNTIS_HISTORY_DAYS of register goes into the index under a kind
of its own, so "what did we actually do before the test" is searchable.

Sharing the snapshot rather than duplicating it caught one thing on the
way: the search tool's live path had to learn notes too, or fresh=true
would have quietly disagreed with the index.

305 tests; 88/89 smoke against the local instance, the one failure being
the H5P service that instance does not run. The live smoke could not be
retaken: that session has lapsed and needs a fresh jwt cookie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-18 21:45:39 +02:00
parent ad8ba28313
commit af4464decb
34 changed files with 3078 additions and 61 deletions

View File

@@ -11,6 +11,7 @@ import { registerH5pTools } from './tools/h5p.ts';
import { registerOverviewTools } from './tools/overview.ts';
import { registerRawTool } from './tools/raw.ts';
import { registerIndexTools } from './tools/index-tools.ts';
import { registerNoteTools } from './tools/notes.ts';
import { registerSearchTool } from './tools/search.ts';
import { registerRoomTools } from './tools/rooms.ts';
import { registerSubmissionTools } from './tools/submissions.ts';
@@ -48,13 +49,22 @@ How the content is organised, and the usual path through it:
graded submission, say it was not found rather than that none was given. On a teacher account these
tools report other people's submissions too.
**The user's own notes are a third source, and often the best one.** When the note tools are listed, the user
keeps notes from their lessons as Markdown files: list_notes and get_note read them, search finds them by
content, and add_note writes one. They record what a teacher said and stressed, which no upload does — so
consult them whenever the question is what was covered in class, what a topic means "the way we did it", or
what to revise for a test, and say when a note disagrees with the material. Notes are the user's own words:
quote them, do not silently correct them.
**The timetable is not in Schulcloud.** When the untis_* tools are listed, the school's schedule lives in
WebUntis and they are the only way to it: untis_timetable says which lessons a day actually holds, what was
cancelled ("Entfall"), what is a substitution ("Vertretung") and what a teacher noted on a period — announced
tests are usually in those notes. Schulcloud holds the material for those lessons, so the two go together:
take the subject from untis_timetable, then find its course with list_courses. untis_homework is the class
register's homework, which is a different list from Schulcloud's tasks; check both. untis_lesson_topics says
what previous lessons of a subject actually covered.
what previous lessons of a subject actually covered — pass it a subject to read back over a whole term, which
is the fastest way to reconstruct what a course has done. Those class-register entries are in the index too,
so search finds them beside the Schulcloud material.
When the user names a topic rather than a course, use search — the API has no search endpoint, so it walks the
courses and matches client-side, which takes a few seconds but covers board text and file names.
@@ -62,7 +72,9 @@ courses and matches client-side, which takes a few seconds but covers board text
The user can also attach a course or room directly (resources schulcloud://courses/<id> and schulcloud://rooms/<id>).
An attached one is exactly what get_course or get_room returns, so do not fetch it again — continue from its ids.
Everything here is read-only; nothing in this server can modify the account.`;
Everything that touches Schulcloud and WebUntis is read-only: no tool here can change the school account,
hand anything in, or mark anything done. The one exception writes nowhere near them — add_note, when it is
listed, saves a file in the user's own notes directory.`;
export function createServer(config: Config, services?: Services): { server: McpServer; context: ServerContext } {
const context = new ServerContext(config, services);
@@ -82,6 +94,8 @@ export function createServer(config: Config, services?: Services): { server: Mcp
registerIndexTools(server, context);
// Only when a key is configured: the tools are not offered at all otherwise.
registerUntisTools(server, context);
// Same rule, for NOTES_DIR.
registerNoteTools(server, context);
registerRawTool(server, context);
registerResources(server, context);
registerPrompts(server, context);