Skip to content

Repository files navigation

Uni Tuebingen StudyOS

Uni Tuebingen StudyOS logo

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.

Status

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.

Privacy model

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.

Tool surface

  • 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.

Local operator development

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-plugin

Copy 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 MCP
  • GET /.well-known/oauth-protected-resource/mcp — OAuth resource metadata
  • GET /.well-known/oauth-authorization-server — authorization-server metadata
  • GET /.well-known/openai-apps-challenge — optional plain-text publisher-domain challenge
  • GET|POST /authorize, POST /token, POST /register, POST /revoke — OAuth endpoints
  • GET|POST /oauth/university — scope-selected University or PPI service login
  • GET|POST /oauth/alma — compatibility alias for existing clients
  • GET|POST /connect/alma — one-time Alma connection flow
  • GET|POST /disconnect/alma — one-time disconnect flow
  • GET /healthz — dependency-free readiness

Hosting requirements

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 --build

Caddy 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/mcp

The committed plugin.example endpoint is intentionally non-routable so a stale development deployment cannot be mistaken for production.

Fly.io example

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 deploy

Mount 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.

Public-directory readiness

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.

Verification

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.

Scope and roadmap

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.

License

Apache-2.0. See LICENSE.

About

Open-source Agent Plugin for University of Tuebingen workflows

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages