Add a German prompt that prepares a school day

Tagesvorbereitung is where the two systems meet. It attaches the day's
timetable and then asks for the rest: match each lesson to its Schulcloud
course, read what is new there, look at what the previous lesson covered,
check both lists of due work, and read the notes on the periods, where the
announced tests are. Written for the morning before school, so it asks for
something short.

The argument takes heute, morgen, übermorgen or a date in either notation,
because evening preparation is the normal case and a German keyboard writes
21.09.2026. Registered only with WebUntis configured: a Tagesvorbereitung
that has to ask which lessons exist is not one.

readTimetable is shared with untis_timetable, the way readCourse is shared
with get_course, so an attached day reads exactly like a fetched 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 74b97bc4dc
commit eaf9c7aa38
3 changed files with 151 additions and 2 deletions

View File

@@ -2,12 +2,13 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ErrorCode, type GetPromptResult } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import type { ServerContext } from '../context.ts';
import { germanDate } from '../core/dates.ts';
import { addDays, germanDate, germanDay, germanWeekday, isCalendarDate, schoolToday } from '../core/dates.ts';
import { fold, joinSections, matchesAll, tokenize } from '../core/text.ts';
import { courseUri, roomUri } from './resources.ts';
import { readCourse } from './tools/content.ts';
import { readRoom } from './tools/rooms.ts';
import { ProtocolError, toProtocolError } from './tools/result.ts';
import { readTimetable } from './tools/untis.ts';
/**
* Prompts: ready-made requests a person picks from a menu, written in German
@@ -81,6 +82,64 @@ export function registerPrompts(server: McpServer, context: ServerContext): void
},
);
// Only with WebUntis: without the timetable there is no way to know which
// lessons a day holds, and a "Tagesvorbereitung" that has to ask is not one.
const untis = context.untis;
if (untis) {
server.registerPrompt(
'tagesvorbereitung',
{
title: 'Tagesvorbereitung',
description:
'Bereitet einen Schultag vor: die Stunden aus WebUntis samt Entfall und Vertretung, dazu das Neue ' +
'aus den passenden Kursen der Schulcloud, Fälligkeiten und angekündigte Tests.',
argsSchema: {
tag: z
.string()
.optional()
.describe('Optional: heute (Standard), morgen, übermorgen oder ein Datum wie 2026-09-21.'),
},
},
async ({ tag }) => {
const date = resolveDay(argumentText(tag));
let timetable: string;
try {
timetable = await readTimetable(untis, { from: date, to: date });
} catch (error) {
throw toProtocolError(error, `read the WebUntis timetable for ${date}`);
}
return {
description: `Tagesvorbereitung: ${germanWeekday(date)}, ${germanDay(date)}`,
messages: [
{ role: 'user', content: { type: 'text', text: timetable } },
{ role: 'user', content: { type: 'text', text: dayPrompt(date) } },
],
};
},
);
}
}
/**
* The day a person meant: `heute`, `morgen`, `übermorgen`, `21.09.2026` or
* `2026-09-21`.
*
* Evening preparation is the normal case for "morgen", and a German date is
* what a German keyboard produces — refusing either would make the prompt
* something to look up rather than to type.
*/
export function resolveDay(value: string | undefined, today: string = schoolToday()): string {
const raw = value?.trim().toLowerCase();
if (!raw || raw === 'heute') return today;
if (raw === 'morgen') return addDays(today, 1);
if (raw === 'übermorgen' || raw === 'uebermorgen') return addDays(today, 2);
const german = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(raw);
if (german) return `${german[3]}-${german[2]!.padStart(2, '0')}-${german[1]!.padStart(2, '0')}`;
if (isCalendarDate(raw)) return raw;
throw new ProtocolError(
ErrorCode.InvalidParams,
`${value}“ ist kein Tag. Nimm heute, morgen, übermorgen oder ein Datum wie 2026-09-21.`,
);
}
// --- arguments -----------------------------------------------------------
@@ -283,6 +342,53 @@ export function examPrompt(target: Target, options: { topic?: string; date?: str
]);
}
/**
* The morning briefing.
*
* Built around the one thing WebUntis knows and Schulcloud does not — which
* lessons actually happen — and the one thing Schulcloud knows and WebUntis
* does not: the material for them. The two lists of things to hand in stay
* separate on purpose, because a teacher uses one or the other and a merged
* list quietly drops half.
*/
export function dayPrompt(date: string): string {
return joinSections([
`Bereite mich auf den Schultag am ${germanWeekday(date)}, ${germanDay(date)} vor. Der Stundenplan aus ` +
'WebUntis steht oben.',
`So gehst du vor:\n${numbered([
'Geh die Stunden des Tages durch. Entfallene Stunden lässt du weg, bei Vertretungen zählt, was stattfindet.',
'Ordne jeder Stunde den passenden Kurs in der Schulcloud zu (list_courses). Die Fächerkürzel und die ' +
'Kursnamen sehen unterschiedlich aus, also geh über das Fach, nicht über eine ID.',
'Sieh dir zu jedem dieser Kurse an, was neu ist: what_changed seit dem letzten Schultag, dazu get_course ' +
'und das, was daran hängt (get_board, get_lesson), sowie die Kurs-Dateien (fs_tree, fs_read).',
'Mit untis_lesson_topics und der periodId einer Stunde siehst du, was im Unterricht zuletzt behandelt ' +
'wurde. Daran erkennst du, was als Nächstes dran ist.',
'Prüfe, was fällig ist: list_tasks für die Schulcloud-Aufgaben und untis_homework für die Hausaufgaben ' +
'aus dem Klassenbuch. Das sind zwei getrennte Listen.',
'Lies die Notizen an den Stunden im Stundenplan. Angekündigte Tests und Leistungskontrollen stehen ' +
'meistens dort und nirgends sonst.',
])}`,
`Die Vorbereitung enthält:\n${bulleted([
'**Der Tag:** je Stunde Zeit, Fach, Raum und Lehrkraft, bei Änderungen mit einem Wort dazu.',
'**Je Fach:** worum es zuletzt ging, was voraussichtlich dran ist, und was ich mir dafür ansehen sollte — ' +
'jeweils mit Quelle, damit ich es wiederfinde.',
'**Vorbereiten und mitbringen:** konkrete Punkte aus den Notizen, Hausaufgaben und Aufgaben.',
'**Fällig:** Aufgaben und Hausaufgaben mit Datum, das von heute und morgen zuerst.',
'**Angekündigt:** Tests, Leistungskontrollen und Prüfungen, mit Datum und Fach.',
'**Sonstiges:** Neuigkeiten (list_news), die den Tag betreffen.',
])}`,
`Wichtig:\n${bulleted([
'Wenn an dem Tag kein Unterricht ist, sag das in einem Satz, nenne den nächsten Schultag und bereite ' +
'stattdessen kurz diesen vor.',
'Stütze dich auf Schulcloud und WebUntis und nenne die Quelle. Was du aus eigenem Wissen ergänzt, ' +
'kennzeichnest du.',
UNREADABLE,
'Halte es knapp: ich lese das morgens vor der Schule.',
'Antworte auf Deutsch.',
])}`,
]);
}
function numbered(items: (string | false | undefined)[]): string {
return items
.filter((item): item is string => Boolean(item))