# --------------------------------------------------------------------------- # 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 `. # 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 `). 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 //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 # --------------------------------------------------------------------------- # Your own lesson notes (optional — what you wrote down in class) # --------------------------------------------------------------------------- # Directory of Markdown files holding your own notes. With it set, the server # offers list_notes, get_note and add_note, indexes the notes so `search` finds # them, and the German prompts consult them. Unset = none of that exists. # docker-compose.yml sets /data/notes and gives it a volume; bind-mount a synced # folder there instead to write notes from a phone. See docs/NOTES.md. # NOTES_DIR=/data/notes # Leave the files alone: list_notes and get_note still work, add_note and # POST /api/notes are refused. Right for a deployment whose notes are synced in # from somewhere else and should have exactly one writer. # NOTES_READONLY=false # Password for the web app at /app — the notes editor and the settings page # where the Schulcloud token is replaced. Unset = the app is not served at all. # # Unlike every other credential here this one is typed by a person on a phone, # so it is a passphrase rather than a random token: at least 12 characters, and # three or four words is the right shape. It is hashed with scrypt at startup # and the plain value is never stored, compared or logged. Changing it logs out # every session, because the session signing key is derived from it. # # The app is on the internet like the rest of the endpoint. Failed logins are # rate-limited per address, but the password is the thing protecting the notes # and the Schulcloud token — pick a long one. # WEB_PASSWORD= # --------------------------------------------------------------------------- # 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= # How far back to read the class register ("Unterrichtsinhalt", plus the notes # teachers leave on a period) into the search index, in days. Default 180; 0 # turns it off. Costs one timetable call per 90 days plus one per lesson series # on a full crawl — a few dozen requests for a school year. This is what makes # "what did we actually do before the test" searchable rather than something to # reconstruct one tool call at a time. # UNTIS_HISTORY_DAYS=180 # --------------------------------------------------------------------------- # 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