Schulcloud holds the material for a lesson but not the lesson: this school's course `times` are empty and it publishes the schedule in WebUntis. So "what do I have today, and has anything been cancelled" was unanswerable, and the timetable cannot be typed into a prompt either — it changes daily. core/untis.ts talks to the API the Untis Mobile app uses, and three tools sit on it: untis_timetable (a day or a range, Entfall, Vertretung, room changes, the notes on each period, inline homework, the period id), untis_homework (the class register's list, which is not Schulcloud's tasks) and untis_lesson_topics (what earlier lessons of a series actually covered, which is what says where a subject got to). Read-only, but not by the Schulcloud client's rule: this API is JSON-RPC, so every call is a POST, reads included. READ_METHODS is the guarantee instead, enforced at the single choke point and asserted by a test. It matters because the key can do what the app can — the live account holds W_OWN_ABSENCE, so the same key could report the user absent. What the live instance taught us, all recorded in docs/API.md: - `startDateTime` ends in Z and is local time. The 08:00 lesson reports 08:00Z, so new Date() would move every lesson by an hour or two. - A substitution is two periods, the original CANCELLED and the replacement IRREGULAR beside it, not one period with a changed teacher. - Announced tests live in the period's info text. The exam module is unused here, so getExams2017 is always empty and that field carries the tests. - A day with no lessons is not a holiday: the weeks this account spends in the company simply have no periods. - `?v=i3.2` is required, or the call fails with a Java NPE reported as -8998. Errors arrive with HTTP 200 and an error member. -8504 is a rejected key and -8524 a drifting clock; the tools name both, because no retry fixes either. Configuration is all four UNTIS_* values or none — three are identifiers and the fourth is a credential, so a half-filled block is a paste that went wrong. Without them the tools are not registered at all, since a tool that can only fail is worse than a missing one. whoami reports the WebUntis identity and survives a dead Schulcloud session, so "is the server reachable" no longer gets a misleadingly total no. mcp-env.sh switches WebUntis off for a fixture run: that key belongs to the real school. 224 tests. All 10 WebUntis smoke checks pass, with a key and without one; the Schulcloud checks in those runs answer 401 because this machine's session is logged out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
127 lines
6.0 KiB
Plaintext
127 lines
6.0 KiB
Plaintext
# ---------------------------------------------------------------------------
|
|
# Schulcloud instance
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Base URL of the instance, no trailing slash.
|
|
TSC_URL=https://schulcloud-thueringen.de
|
|
|
|
# The value of the `jwt` cookie from a logged-in browser session.
|
|
# The 30-day `exp` is only a ceiling; the real limit is a 2-hour sliding session
|
|
# TTL that the built-in keepalive holds open. IMPORTANT: close the Schulportal
|
|
# window after copying this — an open tab shares the session and its auto-logout
|
|
# will revoke this token ~2h after login. See docs/AUTH.md.
|
|
# Needed for the first start. Later tokens go in with `schulcloud token set` or
|
|
# the server's /token page, without a restart; a saved newer one wins over this.
|
|
TSC_JWT_COOKIE=
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# This MCP server
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Shared secret callers must present as `Authorization: Bearer <token>`.
|
|
# REQUIRED for the public deployment — without it the endpoint is open to
|
|
# anyone who finds the hostname. Generate one with:
|
|
# openssl rand -hex 32
|
|
MCP_AUTH_TOKEN=
|
|
|
|
# claude.ai: the token its connector sends as a request header
|
|
# (`authorization: Bearer <token>`). Accepted on /mcp only, never on /api — which
|
|
# can replace the Schulcloud token — because claude.ai stores it. At least 32
|
|
# characters, different from MCP_AUTH_TOKEN; unset = off. Generate one with:
|
|
# openssl rand -hex 32
|
|
# MCP_CONNECTOR_TOKEN=
|
|
|
|
# Clients that cannot send a header: serve MCP at /<this value>/mcp with no
|
|
# token at all. The URL becomes the credential — see "Connecting Claude" in
|
|
# docs/DEPLOYMENT.md before using it. At least 32 URL-safe characters; unset =
|
|
# off. Generate one with:
|
|
# openssl rand -hex 32
|
|
# MCP_PATH_SECRET=
|
|
|
|
# Where a Schulcloud token replaced at runtime (`schulcloud token set`, /token)
|
|
# is saved, so a restart keeps it. docker-compose.yml sets /data/state; unset =
|
|
# replacements last until the next restart.
|
|
# STATE_DIR=/data/state
|
|
|
|
# Listen address inside the container. Leave as-is when running behind Caddy.
|
|
PORT=8080
|
|
BIND_HOST=0.0.0.0
|
|
|
|
# Local Docker only: the loopback port docker-compose.override.yml publishes the
|
|
# container on. Default 8080; change it when that port is already taken.
|
|
# MCP_HOST_PORT=8080
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Index and file mirror (optional — without these the server runs live-only:
|
|
# search crawls on every call, and the CLI's /api surface is unavailable)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Postgres for crawl generations, full-text search and the file mirror index.
|
|
# On the Pi, point this at the existing instance with its own database and user.
|
|
DATABASE_URL=postgresql://schulcloud:schulcloud@postgres:5432/schulcloud
|
|
|
|
# Where mirrored file bytes are stored. Needs to be writable by the container.
|
|
# MIRROR_DIR=/data/mirror
|
|
|
|
# Files larger than this are indexed as metadata but not mirrored; they are
|
|
# still downloadable, proxied live. Default 64 MiB.
|
|
# MIRROR_MAX_BYTES=67108864
|
|
|
|
# Also index personal files ("Meine Dateien") and submitted / returned work,
|
|
# including teacher grade comments. This is what makes "what did the teacher
|
|
# say about X" searchable and lets what_changed report a re-grade. Costs roughly
|
|
# three extra requests per task on a full crawl, so it is off by default.
|
|
# INDEX_PERSONAL_FILES=false
|
|
|
|
# Also walk the file manager ("Dateien") when crawling: Kurs-Dateien for every
|
|
# course, plus Persönliche, Team- and Geteilte Dateien on a full crawl. Many
|
|
# teachers keep their material only there, so this is on by default. One page
|
|
# load per folder — about 160 on a 26-course account.
|
|
# INDEX_FILE_MANAGER=true
|
|
|
|
# How often to re-crawl on a timer, in ms. Default 21600000 (6h). 0 = on demand
|
|
# only. A re-crawl of unchanged content downloads nothing, because Schulcloud
|
|
# file records are immutable.
|
|
# CRAWL_INTERVAL_MS=21600000
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# WebUntis (optional — the timetable, which Schulcloud does not hold)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Where the school publishes its timetable. With these set, the server offers
|
|
# untis_timetable, untis_homework and untis_lesson_topics, plus the German
|
|
# "tagesvorbereitung" prompt; without them none of that exists. All four values
|
|
# are in one dialog: WebUntis → Profil → Freigaben → Untis Mobile → QR-Code.
|
|
#
|
|
# UNTIS_SECRET is the "Schlüssel" field and is a credential: it authenticates
|
|
# every request as this user, needs no password, works with an SSO login, and
|
|
# stays valid until you generate a new key in that dialog. It can do whatever
|
|
# the Untis Mobile app can — this server only ever calls read methods, by
|
|
# allowlist (src/core/untis.ts). See docs/AUTH.md.
|
|
#
|
|
# Authentication is a time-based code, so the host's clock must be in sync;
|
|
# WebUntis answers -8524 ("invalid client time") when it is not.
|
|
# UNTIS_SERVER=yourschool.webuntis.com
|
|
# UNTIS_SCHOOL=yourschool
|
|
# UNTIS_USER=your.username
|
|
# UNTIS_SECRET=
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Limits (optional — sensible defaults are built in)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Largest file download_file will pull, in bytes. Default 25 MiB.
|
|
# Videos in Schulcloud routinely exceed this; they are not extractable anyway.
|
|
# MAX_DOWNLOAD_BYTES=26214400
|
|
|
|
# Characters of extracted text returned before truncation. Default 120000.
|
|
# MAX_EXTRACTED_CHARS=120000
|
|
|
|
# Per-request timeout against the Schulcloud API, in ms. Default 30000.
|
|
# REQUEST_TIMEOUT_MS=30000
|
|
|
|
# How often to call refresh-session to hold the session open, in ms. Default
|
|
# 1800000 (30 min). Must stay well under the instance's JWT_TIMEOUT_SECONDS —
|
|
# 7200s here, readable from GET /api/v3/config/public. Set to 0 to disable.
|
|
# KEEPALIVE_INTERVAL_MS=1800000
|