Files
Schulcloud-MCP/local-instance/scripts/mcp-env.sh
MechaCat02 74b97bc4dc Read the timetable from WebUntis
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>
2026-09-17 20:43:24 +02:00

49 lines
2.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Mint a session on the local instance and print the environment the MCP server
# and CLI expect, so they can be pointed at it instead of the live Schulcloud.
#
# eval "$(./scripts/mcp-env.sh)" # as the demo student
# eval "$(./scripts/mcp-env.sh klara.fall@schul-cloud.org Schulcloud1\!)"
#
# Nothing is written to the repo: the output contains a live session token, and
# a throwaway instance is still no reason to start committing those.
set -euo pipefail
# `localhost`, not `127.0.0.1`: it must match SC_DOMAIN, because urls the server
# hands back (Etherpad pads, for one) are built from it, and the client refuses
# to follow a url onto a different host rather than leak a session cookie there.
URL=${LOCAL_SC_URL:-http://localhost:4400}
USER=${1:-demo-schueler@schul-cloud.org}
PASS=${2:-schulcloud}
token=$(curl -fsS -X POST "$URL/api/v3/authentication/local" \
-H 'Content-Type: application/json' \
-d "$(printf '{"username":%s,"password":%s}' \
"$(printf '%s' "$USER" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')" \
"$(printf '%s' "$PASS" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')")" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["accessToken"])')
# The index and mirror are pinned too, not just the instance. The repo's .env
# normally points at the live account, and process env beats --env-file, so
# without these a local smoke run would crawl this throwaway instance straight
# into the live index — and a per-course refresh carries everything outside its
# scope forward, so the fixtures would outlive the run. Fixtures in real data
# is exactly the accident the store tests' "test" guard exists to prevent.
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
cat <<ENV
export TSC_URL=$URL
export TSC_JWT_COOKIE=$token
export MCP_AUTH_TOKEN=local-instance-token
export DATABASE_URL=postgresql://schulcloud:schulcloud@127.0.0.1:55432/schulcloud_local
export MIRROR_DIR=$ROOT/tmp/mirror-local
export INDEX_PERSONAL_FILES=true
# WebUntis off for a local run: this instance has no timetable, and the key in
# the repo's .env belongs to the real school — a fixture run has no business
# talking to it, even read-only.
export UNTIS_SERVER=
export UNTIS_SCHOOL=
export UNTIS_USER=
export UNTIS_SECRET=
ENV