Uniboh
FreeNot checkedMCP server for University of Bologna services, enabling access to Virtuale, timetables, and exam plans through natural language.
About
MCP server for University of Bologna services, enabling access to Virtuale, timetables, and exam plans through natural language.
README
A Model Context Protocol server (stdio transport, TypeScript) that exposes University of Bologna services to MCP clients. Today it covers:
- virtuale.unibo.it — the Unibo Moodle instance, via its
service.phpAJAX API (enrolled courses, course state, file/resource download, Panopto content). - corsi.unibo.it — public course timetables, normalized to events and exported as an ICS calendar.
- almaesami.unibo.it — student exam plan, history, messages, and upcoming appelli, read-only.
- rps.unibo.it — student attendance ("Presenze studenti"): records and register, read-only.
- studenti.unibo.it — Studenti Online (SOL): career summary and service catalogue, read-only.
The longer-term goal is to wrap any Unibo service behind one MCP server — see CLAUDE.md.
Install
npm install
npm run build
Run
Stdio transport, for use by an MCP client.
No credentials (health check + calendar tools only):
npm run dev
With a preloaded session (enables the authenticated Virtuale tools):
VIRTUALE_BASE_URL="https://virtuale.unibo.it" \
VIRTUALE_SESSKEY="your_sesskey" \
VIRTUALE_COOKIES="MoodleSession=...; other_cookie=..." \
npm run dev
npm run start runs the compiled dist/server.js instead of tsx.
Environment variables
| Variable | Required | Purpose |
|---|---|---|
VIRTUALE_BASE_URL |
no (defaults to https://virtuale.unibo.it) |
Moodle base URL. |
VIRTUALE_SESSKEY |
no | Moodle sesskey; with VIRTUALE_COOKIES, enables authenticated tools without a login call. |
VIRTUALE_COOKIES |
no | Cookie header (e.g. MoodleSession=...). |
ALMAESAMI_BASE_URL |
no (defaults to https://almaesami.unibo.it) |
AlmaEsami base URL. |
ALMAESAMI_COOKIES |
no | Cookie header with an authenticated JSESSIONID; fallback for the almaesami_* tools. |
RPS_BASE_URL |
no (defaults to https://rps.unibo.it) |
RPS base URL. |
RPS_COOKIES |
no | Cookie header with an authenticated PHPSESSID; fallback for the rps_* tools. |
SOL_BASE_URL |
no (defaults to https://studenti.unibo.it) |
Studenti Online base URL. |
SOL_COOKIES |
no | Cookie header with an authenticated JSESSIONID; fallback for the sol_* tools. |
EMAIL |
no | Unibo SSO email; with PASSWORD, enables unibo_browser_login (headless-Chromium ADFS login for Virtuale + AlmaEsami + RPS + Studenti Online) and transparent auto re-login on session expiry. Shared across services since they all federate to the same idp.unibo.it SSO. |
PASSWORD |
no | Unibo SSO password. Only works for accounts without interactive MFA. |
Authentication
All three authenticated services (Virtuale, AlmaEsami, RPS) federate to the same
idp.unibo.it ADFS SSO, so a single unified in-memory session store holds per-service
credentials: one session_id can carry a Virtuale sesskey+cookies, an AlmaEsami
JSESSIONID, and an RPS PHPSESSID at once. Authenticated Virtuale tools need both a
sesskey and a cookie header; AlmaEsami/RPS need only their host cookie. Ways to provide them:
- Env vars —
VIRTUALE_SESSKEY+VIRTUALE_COOKIES,ALMAESAMI_COOKIES,RPS_COOKIES, set once at startup. EMAIL+PASSWORD, viaunibo_browser_login— drives a real headless Chromium through Unibo's ADFS SSO flow (Home Realm Discovery → AD login), then reuses the same shared IdP session to complete the AlmaEsami and RPS SAML handshakes too, capturing every host's cookies automatically. Best-effort per service (the result reports per-service success). Only works for accounts without interactive MFA. See scripts/test-browser-login.mjs to verify your account works before wiring it into an MCP client. (virtuale_browser_loginremains as a deprecated alias.)*_bootstrap_session— paste credentials grabbed from a logged-in browser; returns asession_id. The reliable path for accounts with MFA.virtuale_bootstrap_sessiontakes asesskey+ cookies;almaesami_bootstrap_session/rps_bootstrap_sessiontake a cookie header (the cookie is never echoed back).virtuale_login_with_password— best-effort direct form login (no browser). This will fail for accounts on federated SSO (most Unibo accounts); prefer options 2 or 3.
Auto re-login. When EMAIL + PASSWORD are set and a call fails because the session
expired, the server transparently re-runs the headless browser login once, updates the stored
credentials, and retries the call — but only for env-backed or unibo_browser_login
sessions (credentials that are ours to refresh), never for pasted *_bootstrap_session
credentials. Concurrent expiries share a single in-flight re-login so an expiry storm triggers
at most one browser login.
Session data is kept in server memory only and is never written to disk. Treat sesskey + cookies as account-bound secrets.
Keeping secrets out of the model's context
If credentials are set via env vars (VIRTUALE_SESSKEY/VIRTUALE_COOKIES, ALMAESAMI_COOKIES, RPS_COOKIES), every tool already falls back to them silently when a call omits cookies/session_id — the model never has to see or pass the secret at all.
If you'd rather the model work with an explicit handle instead of an invisible fallback, call the corresponding env-session tool first — virtuale_get_env_session, almaesami_get_env_session, rps_get_env_session, or (if you'd rather store an SSO password than pre-captured cookies) unibo_browser_login — each takes no input (browser login takes only an optional force_relogin), reads its env var(s) server-side, and returns only an opaque session_id (idempotent: repeat calls return the same id). Pass that session_id to the other tools. unibo_browser_login's single session_id works across the virtuale_*, almaesami_*, and rps_* tools. The underlying secret is never included in any of these responses.
The *_bootstrap_session and virtuale_login_with_password tools still take credentials as tool input (by design, since you're supplying them inline), so those do pass through the model's context. The *_bootstrap_session tools never echo the pasted cookie back.
Tools
Session management
unibo_browser_login— mint/reuse onesession_id(usable acrossvirtuale_*,almaesami_*,rps_*) by driving a headless Chromium through ADFS SSO withEMAIL/PASSWORDand completing every service's SAML handshake off the shared IdP session; never returns the password/sesskey/cookies, and reports per-service success. No interactive-MFA support. Optionalforce_relogin.virtuale_browser_login— deprecated alias forunibo_browser_login(same handler).virtuale_bootstrap_session— build a session from an existing Virtualesesskey+ cookies.almaesami_bootstrap_session/rps_bootstrap_session— build a session from a pasted AlmaEsamiJSESSIONID/ RPSPHPSESSIDcookie header; the cookie is never echoed back.virtuale_get_env_session/almaesami_get_env_session/rps_get_env_session— mint/reuse asession_idfrom that service's env cookie(s) without ever returning the secret. See Keeping secrets out of the model's context.virtuale_login_with_password— best-effort form login → stores session, returnssession_id(fails on federated SSO).virtuale_get_session_info— stored session metadata (origin, which services it carries, optionally the cookie headers).virtuale_logout_session— drop one session from memory.virtuale_health_check— no-logincore_get_stringprobe for connectivity.
Virtuale (authenticated)
virtuale_get_enrolled_courses— wrapslocal_uniboapi_get_enrolled_courses_unibo.virtuale_get_course_state— wrapscore_courseformat_get_state, parses the state JSON.virtuale_list_course_files— slimscore_courseformat_get_statedown to the downloadable files/resources, grouped by section (cmid,name,modname,url) — a token-friendly view; feed acmidtovirtuale_get_resource.virtuale_get_panopto_content— wrapsblock_panopto_get_content.
Each accepts an optional session_id; if omitted, the env-var session is used.
virtuale_get_resource— fetches a course file/resource bycmid(builds/mod/resource/view.php?id=<cmid>) or an expliciturl, following the redirect to the protectedpluginfile.phpcontent. Always returns metadata (final URL, content-type, size, filename); withsave_to(an absolute path) it streams the file to disk, otherwise it returns text inline for text-like files and extracts + returns the text of PDFs (long text is truncated — passsave_tofor the full file). Acceptssession_id,cookies(a header with an authenticatedMoodleSession), or falls back toVIRTUALE_COOKIES. Read-only.
Virtuale quizzes (authenticated, read-only)
mod_quiz_* isn't on the AJAX service allowlist, so these scrape the same HTML a browser
sees (course page → quiz page → attempt review page). Each accepts session_id,
cookies (a cookie header with an authenticated MoodleSession), or falls back to
VIRTUALE_COOKIES.
virtuale_quiz_list_course_quizzes— quiz activities on a course page, with thecmidneeded by the other quiz tools.virtuale_quiz_list_attempts— a quiz's attempt summaries (status, dates, marks, grade, review URL/attempt id) for finished attempts.virtuale_quiz_get_attempt_review— one finished attempt's questions, answer options, the student's selection, correctness, and feedback.virtuale_quiz_sync_bank— diffs a local quiz-bank JSON file (bank_path) against Moodle attempts byattempt_id, fetches only the new attempt reviews, and appends them to the file in the bank's schema (backing up to<bank_path>.bakfirst).cmidsdefaults to the bank's ownquizzeslist;dry_runreports what would be fetched without writing. Writes to disk; returns only a count summary, never question content.
Scoped to reviewing attempts already finished and reviewable under the quiz's own review settings — it does not start, resume, or answer a live/in-progress attempt.
Timetable / calendar (public, no auth)
unibo_calendar_resolve_timetable_url— find the corsi.unibo.it timetable URL from a course page.unibo_calendar_list_curricula— list curricula from@@available_curricula.unibo_calendar_list_teachings— extract teaching IDs from the timetable page.unibo_calendar_get_events— fetch@@orario_reale_jsonevents, optional teaching-code filter.unibo_calendar_get_ics— same, returned as an ICS calendar string.
AlmaEsami (authenticated)
All read-only. Each accepts session_id (from almaesami_get_env_session,
almaesami_bootstrap_session, or unibo_browser_login) or cookies (a cookie header with an
authenticated JSESSIONID), or falls back to ALMAESAMI_COOKIES.
almaesami_bootstrap_session— mint asession_idfrom a pastedJSESSIONIDcookie header (never echoed back).almaesami_get_env_session— mint/reuse asession_idfromALMAESAMI_COOKIESwithout ever returning the cookie.almaesami_get_exam_plan— the exam plan (activities, CFU, status, bookable flag).almaesami_get_exam_history— the exam history / cronologia (appello date, examiner, type/mode, status).almaesami_get_messages— student messages (subject, sender, date, related appello).almaesami_list_appelli— upcoming exam sessions (appelli): date/time, activity, examiner, type/mode, enrollment window, and abookableflag — to answer "when can I sit exam X". The endpoint/grid is UNVERIFIED (it lives behind SSO and could not be confirmed against a live session): the result carriesunverified: true, the parser reads fields by content so it tolerates layout changes, and the endpointpathis overridable. If it returns nothing, fall back to thebookableflags onalmaesami_get_exam_plan.
These never mutate state: booking an exam ("prenota") and deleting messages ("Cancella") are intentionally not automated.
AlmaEsami is behind ADFS SSO with no JSON API. If EMAIL/PASSWORD are set (non-MFA
account), unibo_browser_login captures the JSESSIONID automatically. Otherwise
authenticate the bootstrap way: log in via a browser, copy the JSESSIONID cookie for
almaesami.unibo.it (it expires after a short idle period), and hand it to
almaesami_bootstrap_session. See almaesami-rps-api-notes.md.
RPS — attendance (authenticated)
All read-only. Each accepts session_id (from rps_get_env_session,
rps_bootstrap_session, or unibo_browser_login) or cookies (a cookie header with an
authenticated PHPSESSID), or falls back to RPS_COOKIES.
rps_bootstrap_session— mint asession_idfrom a pastedPHPSESSIDcookie header (never echoed back).rps_get_env_session— mint/reuse asession_idfromRPS_COOKIESwithout ever returning the cookie.rps_get_attendance_records— recorded presences (date, subject, lecturer, duration).rps_get_register— per-subject hours attended and attendance percentage.
Confirming attendance by entering a "codice rilevazione" is a write action and is
intentionally not automated. If EMAIL/PASSWORD are set (non-MFA account),
unibo_browser_login captures the PHPSESSID automatically. Otherwise authenticate the
bootstrap way: log fully into rps.unibo.it, confirm you see the app (not the SSO Sign In
page), copy the PHPSESSID cookie, and hand it to rps_bootstrap_session.
Studenti Online — SOL (authenticated)
All read-only. Each accepts session_id (from sol_get_env_session, sol_bootstrap_session,
or unibo_browser_login) or cookies (a cookie header with an authenticated JSESSIONID),
or falls back to SOL_COOKIES.
sol_bootstrap_session— mint asession_idfrom a pastedJSESSIONIDcookie header (never echoed back).sol_get_env_session— mint/reuse asession_idfromSOL_COOKIESwithout ever returning the cookie.sol_get_career— the student's home/career summary: greeting/identity, enrolled course of study, and in-progress requests.sol_get_services— the catalogue of Studenti Online service tiles (name, link, description) — fees, certificates, enrolments, etc. — so a caller can discover what the portal exposes.
First integration, partly unverified: the sol_* readers were derived from a single
logged-in home-page capture, so selectors are best-effort and individual fields degrade to
empty rather than failing. Consequential pages behind the service tiles (paying fees, new
enrolment/career requests, degree applications) mutate real state and are intentionally not
automated. See sol-api-notes.md.
Calendar flow example
unibo_calendar_resolve_timetable_urlwith the unibo.it course URL.unibo_calendar_list_curriculawith the resolved timetable URL.- (optional)
unibo_calendar_list_teachingsto get teaching IDs to filter on. unibo_calendar_get_eventsorunibo_calendar_get_icswithyear,curriculum, and optional teaching codes.
Development
npm run typecheck # tsc --noEmit
npm test # node built-in test runner via tsx
npm run build # emit dist/
Tests live next to sources as src/*.test.ts and are excluded from the build.
Reference notes
- virtuale.unibo.it-api-notes.md — reverse-engineered Moodle AJAX endpoints.
- unibo-timetable-calendar-api.md — corsi.unibo.it timetable API.
- Timetable logic is informed by FrancescoBonzi/UniboCalendar.
License
ISC — see LICENSE.
Install Uniboh in Claude Desktop, Claude Code & Cursor
unyly install unibohInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add uniboh -- npx -y github:gattaa/unibohFAQ
Is Uniboh MCP free?
Yes, Uniboh MCP is free — one-click install via Unyly at no cost.
Does Uniboh need an API key?
No, Uniboh runs without API keys or environment variables.
Is Uniboh hosted or self-hosted?
A hosted option is available: Unyly runs the server in the cloud, no local setup required.
How do I install Uniboh in Claude Desktop, Claude Code or Cursor?
Open Uniboh on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectCompare Uniboh with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
