An independent, open-source Agent Plugin for anonymous campus and course discovery plus opt-in, read-only University of Tuebingen Alma, ILIAS, Moodle, Mail, and PPI workflows in compatible ChatGPT, Codex, and MCP clients.
Important
This community project is not an official service of the University of Tuebingen. University and service names identify interoperable systems; they do not imply endorsement.
The current implementation is a hosted-ready, single-container MCP and OAuth service. The public
deployment is not live until an authorized operator chooses a host and publishes the final MCP
URL. End users will not install Python, clone a wrapper, provide credentials in chat, create a
separate plugin account, or create an .env file.
The implementation provides:
- thirty-five anonymous public reads covering modules, lectures, events, talks, canteens, buildings, rooms, occupancy, sports, directory entries, TIMMS, and Praxisportal projects, including source-labelled course discovery;
- ten OAuth-scoped Alma tools for connection status, profile, timetable, progress, exams, enrolments, planning, catalogue access, report listing, and explicit PDFs;
- all eight read methods exposed by the pinned ILIAS client and all eight exposed by the pinned Moodle client, each under an independent service scope;
- three independently scoped Mail reads for folders, message summaries, and selected plain-text messages, with an intentionally memory-only upstream session;
- three independently scoped PPI reads for the protocol catalogue, existing borrowed entitlements, and explicitly selected ZIP archives;
- a project-owned OAuth 2.1 authorization-code service with PKCE, dynamic client registration, refresh-token rotation, and revocation;
- adaptive inline MCP Apps components on every tool, with evidence tables, status badges, recursive records, event timelines, occupancy gauges, source timestamps, canteen menus, buildings, study rooms, course sources, research-talk cards, Mail, PPI, and availability;
- isolated eight-hour upstream cookie sessions with optional encrypted SQLite persistence and no password persistence; Mail sessions are never persisted;
- a Streamable HTTP MCP endpoint, plugin manifest, workflow skills, container, generic Caddy/Compose deployment, optional Fly.io example, and CI.
- eight focused workflow skills, including a source-labelled daily brief that never reads Mail, official documents, or PPI archives without the corresponding explicit request.
Read the design specification for the trust and product boundaries.
The agent never receives university credentials. During OAuth authorization, the host opens the plugin's small service login page in its in-app browser. That page sends credentials over TLS directly to the same hosted service; values never become MCP tool arguments or model-visible output. The service authenticates only the requested connector and discards the submitted password. Alma, ILIAS, Moodle, and Mail use University credentials; PPI uses the same ZDV username with its separate PPI password. Each service has a separate OAuth scope and session record.
With persistence configured, Alma, ILIAS, Moodle, and PPI cookie jars plus OAuth metadata
are encrypted with AES-GCM before SQLite storage. Mail remains memory-only even then. Opaque
authorization codes and tokens are indexed only by keyed hashes and are never stored in raw
form. Restart and deployment restore still-valid persisted sessions; disconnect, upstream
expiry, the eight-hour limit, corruption, or key rotation fails closed. Without both
persistence variables, the deliberately lean in-memory behavior remains. There is no local
.env discovery, credential fallback, password storage, user-profile database, shared
private-data cache, or shared private course index.
- Alma public catalogue: module search, filters and safe-origin detail; current lectures.
- Campus: events, canteens, buildings, seats, gym/KUF occupancy and fitness offers.
- Rooms: resource discovery, scheduling rules, start/end intervals, date availability, periods and calendars. These remain read-only and never create a booking.
- Discovery: source-labelled public and connected course search, the public directory, research talks, TIMMS search/tree/streams, and Praxisportal filters, projects and subscription-type metadata.
alma:read: connection, profile, timetable, evidence-backed progress, exams, enrolments, course catalogue, study planner and explicitly selected reports.ilias:read: root navigation, memberships, tasks, deadlines, selected content, forums, exercises and course assignments.moodle:read: dashboard, calendar, courses, safe course detail, grades, notifications, categories and messages.mail:read: folders, bounded message summaries, and explicitly selected plain-text messages. HTML, tracking content, attachment bytes, and message mutations are excluded.ppi:read: protocol catalogue, existing borrowed entitlements, and explicitly selected ZIP archives. Signup, borrowing, and token requests are excluded.
The plugin is intentionally a thin but security-enforcing adapter. It preserves safe structured wrapper records with source and retrieval time, while validating outbound targets and projecting models that include hidden form or mutation state onto explicit allowlists. It never exposes authenticated HTML, cookies, CSRF/session fields, signed report URLs, or untrusted markup. Small derived views remain evidence-bound.
alma_academic_progress counts only numeric credits whose ALMA exam record has an explicit
passed status. If no such rows are present, earned_credits is null,
verified_passed_credits is the sum of the explicit evidence (possibly zero), and the original
records remain available. Aggregate PV account rows are preserved but are not relabelled as
passed courses.
End users do not perform these steps. Contributors and deployment operators can run:
uv sync --python 3.13 --extra dev
uv run pytest
uv run tuebingen-pluginCopy the variable names from .env.example into the process environment; the application deliberately does not load dotenv files. PUBLIC_BASE_URL is both the HTTPS OAuth issuer and the base of the MCP resource URL. Configure both SERVICE_STATE_DATABASE_PATH and SERVICE_STATE_ENCRYPTION_KEY, or neither; partial or malformed persistence configuration stops startup. The previous ALMA_SESSION_* variable names remain accepted for v0.2 deployment compatibility.
The production endpoints are:
POST /mcp— Streamable HTTP MCPGET /.well-known/oauth-protected-resource/mcp— OAuth resource metadataGET /.well-known/oauth-authorization-server— authorization-server metadataGET /.well-known/openai-apps-challenge— optional plain-text publisher-domain challengeGET|POST /authorize,POST /token,POST /register,POST /revoke— OAuth endpointsGET|POST /oauth/university— scope-selected University or PPI service loginGET|POST /oauth/alma— compatibility alias for existing clientsGET|POST /connect/alma— one-time Alma connection flowGET|POST /disconnect/alma— one-time disconnect flowGET /healthz— dependency-free readiness
The MCP endpoint and OAuth service run in one small OCI container with one HTTP port and no background worker. Any host is suitable when it provides HTTPS, deployment secrets, outbound access to University services, and roughly 512 MB memory. Encrypted restart-safe state additionally needs a persistent writable POSIX volume. SQLite is intentionally single-instance for this release; do not run multiple replicas against copied or network-shared database files.
Cloudflare can front the service for DNS, WAF, and edge rate limiting. This Python/SQLite release is not a Cloudflare Workers application. The generic production path is one Linux host running the included app container and Caddy reverse proxy:
cp deploy/.env.production.example deploy/.env.production
# Edit the untracked file with the real hostname and a newly generated encryption key.
docker compose --env-file deploy/.env.production -f deploy/compose.yaml up -d --buildCaddy obtains and renews TLS certificates. The application port remains private to the Compose
network, while its encrypted SQLite state and Caddy state use named volumes. Before distributing
the plugin, write the live endpoint into .mcp.json and every bundled skill dependency:
uv run python scripts/configure_plugin_endpoint.py https://studyos.example.edu/mcpThe committed plugin.example endpoint is intentionally non-routable so a stale development
deployment cannot be mistaken for production.
The included fly.toml targets one shared CPU in Frankfurt with 512 MB. Before the first deploy:
fly apps create tuebingen-agent-plugin
fly volumes create plugin_state --region fra --size 1
fly secrets set \
SERVICE_STATE_ENCRYPTION_KEY=REPLACE_WITH_URLSAFE_BASE64_32_BYTE_KEY
fly deployMount the volume at /data before using the persistent example. Compatible clients register
through the built-in dynamic registration endpoint and request only the service scopes they
need; no external identity tenant, API audience, or user-profile service is required. Store
FLY_API_TOKEN as a GitHub Actions secret to use the manual deployment workflow.
The repository includes production listing metadata, original logo assets, support/privacy/terms documents, the OpenAI domain-challenge route, and reviewer cases. See the publication dossier and reviewer tests.
Current OpenAI policy does not approve unofficial third-party connectors and treats passwords as restricted authentication data. A public-directory submission therefore still requires University authorization and an acceptable University-operated or explicitly review-approved authentication arrangement. A private or local pilot can be tested independently, but must not be described as an approved public plugin.
uv run ruff format --check .
uv run ruff check .
uv run mypy src
uv run pytest
uv run python scripts/check_line_count.py
for skill in skills/*; do uv run python scripts/validate_skill.py "$skill"; done
uv build
uv run pip-audit
docker build -t tuebingen-agent-plugin:test .
python /path/to/plugin-creator/scripts/validate_plugin.py .The automated suite uses synthetic clients at the upstream boundary. A release still requires dedicated test accounts to manually validate live Alma, ILIAS, Moodle, Mail, PPI, expiry, reconnect, report, archive, and disconnect behavior.
The current release is read-only. Its in-chat UI renders public campus logistics, course discovery, research talks, and the linked Alma, ILIAS, Moodle, Mail, and PPI data surfaces. Public room availability is not a booking. A future booking flow must use a short-lived opaque intent and a widget-only commit action after explicit human confirmation; it will not be added until the upstream submission contract is verified. The plugin does not include professor roster or grading tools, registrations, submissions, outbound messages, Mail mutations, or PPI signup, borrowing, and token requests. Those higher-risk surfaces require separate scopes and confirmation policies rather than extending the current read scopes. See the design specification for the original boundaries and secure state notes for the backend.
See CONTRIBUTING.md, SECURITY.md, PRIVACY.md, SUPPORT.md, and TERMS.md before contributing or operating the service.
Apache-2.0. See LICENSE.
