Skip to main content
Glama
oliverhruby

EduPage MCP Server

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
MCP_HOSTNoBind host for HTTP transport (default localhost)
MCP_PORTNoBind port for HTTP transport (default 8000)
MCP_API_KEYNoOptional bearer token for HTTP transport auth
MCP_TRANSPORTNoTransport mode for the MCP serverstdio
EDUPAGE_PASSWORDYesEduPage password
EDUPAGE_USERNAMEYesEduPage username or login
EDUPAGE_SUBDOMAINSNoComma-separated list of school subdomains for multi-school auto-login (optional)

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
loginA

Log in to Edupage for a school. Writes: establishes (or replaces) the server-side session for that subdomain.

Args: username: EduPage account login. Falls back to EDUPAGE_USERNAME. password: Account password. Falls back to EDUPAGE_PASSWORD. subdomain: School subdomain (e.g. 'school'). Falls back to the first value of EDUPAGE_SUBDOMAINS; for method='auto' it is optional and tags the detected school's session. method: How to authenticate: - 'credentials' (default) — username/password for a known subdomain. - 'auto' — portal auto-detect of the school (formerly login_auto). - 'session' — build a session from an existing PHPSESSID cookie (formerly login_from_session); pass it in session_id. session_id: PHPSESSID cookie value, required when method='session'.

Returns: dict with logged_in status, subdomain, user_id, role and whether 2FA is pending. When two_factor_required is true, finish with two_factor_finish.

Notes: - Each login call adds/replaces that subdomain's session; call login_all to log into several schools in one call. - When EDUPAGE_SUBDOMAINS is set it is a strict allowlist: login into a school outside it is refused. - Prefer setting EDUPAGE_USERNAME / EDUPAGE_PASSWORD (and EDUPAGE_SUBDOMAINS for multi-school) — the server then logs in automatically at startup.

login_allA

Log in to one or more schools in a single call. Writes: establishes (or replaces) the server-side session for each subdomain.

Args: subdomains: Comma-separated school subdomains, e.g. 'school1,school2'. Falls back to EDUPAGE_SUBDOMAINS. usernames: Comma-separated usernames, one per school (or a single one). Falls back to EDUPAGE_USERNAME. passwords: Comma-separated passwords, one per school (or a single one). Falls back to EDUPAGE_PASSWORD.

Returns: dict: {'results': [{subdomain, ok, user_id, role, two_factor_required}], 'active_subdomain': ...}. A failed school is reported per-entry with its error.

Notes: - Add schools one at a time with login. - When EDUPAGE_SUBDOMAINS is set it is a strict allowlist: schools outside it are refused per-entry without creating a session. - Finish any pending 2FA with two_factor_finish.

two_factor_finishA

Finish a pending 2FA login. Writes: completes the pending auth flow. Only needed after a login that returned two_factor_required: True.

Args: code: Optional email/app verification code. When given it is used directly; otherwise the device-confirmation flow is polled. subdomain: School subdomain whose pending login to complete (defaults to the active subdomain). poll_seconds: How long (seconds) to wait for approval on a device when code is not given. Defaults to 60; on timeout the caller can call two_factor_finish again later.

Returns: dict: {'confirmed': True, 'logged_in': True, subdomain, user_id, role} on success, or a pending status when the confirmation wasn't approved within the poll window.

get_school_yearA

Return the current school year (starting year). Read-only.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'school_year': , 'subdomain': ...}.

get_my_timetableA

Get the timetable for the logged-in user on a date. Read-only.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'lessons': [serialized lessons]}.

Notes: - For another student use get_student_timetable (by name/id); for a teacher/class/room on a date or a range use get_timetable. - Next week for yourself: get_next_week_timetable.

get_student_timetableA

Get a student's timetable by first/last name OR person_id. Read-only. Without a subdomain, searches every school in the discovery scope (the configured EDUPAGE_SUBDOMAINS, or all logged-in schools when unset) and returns one result per school where the student is found — so a student attending multiple schools yields separate per-school timetables.

Args: name: Student's first/last/full name. student_id: person_id (preferred — unambiguous, see find_student). date_str: YYYY-MM-DD (default today). subdomain: Restrict to one school (default: all logged-in schools).

Returns: dict: {'results': [{student, student_id, class_id, date, subdomain, lessons}]}; with a query/matched_schools summary when more than one school is searched.

Notes: - If logged in as a parent this resolves the child agent-side and queries their timetable directly (no session switching). For your own timetable use get_my_timetable. - Use the student_id from find_student / get_my_students for unambiguous lookups.

get_timetableA

Get the timetable for a teacher, student, class or classroom on a date (or a date range, see end_date). Read-only.

Args: target_type: 'teacher' | 'student' | 'class' | 'classroom'. target_id: person/class/classroom id as returned by get_roster. date_str: Single day, YYYY-MM-DD (default today). Ignored when end_date is given. end_date: When set, returns the timetable for every day from date_str (default today) to end_date inclusive, keyed by date — the equivalent of the former get_timetable_range. subdomain: School to query (defaults to the active subdomain).

Returns: Single-day shape: {'target', 'date', 'subdomain', 'lessons'}. Range shape: {'subdomain', 'range': {: single-day result}}. Days with no published data get an empty lessons list.

Notes: - For the logged-in user's own timetable prefer get_my_timetable; for a student by name/id use get_student_timetable. - Any school's whole-week plan for yourself: get_next_week_timetable.

get_next_ringing_timeA

Get the type (break/lesson) and time of the next school-bell ringing. Read-only.

Args: date_time_str: ISO datetime to search onward from (default: now). subdomain: School to query (defaults to the active subdomain).

Returns: Serialized ringing: type (break/lesson) and time.

Notes: - See get_periods for the full bell schedule.

get_next_week_timetableA

Get the Mon-Fri timetable for next week for the logged-in user, grouped by weekday. Read-only.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'monday', 'subdomain', 'week': [{weekday, date, lessons} x5]}.

Notes: - Weekdays are 'Po','Ut','St','Št','Pi'. - For the logged-in user on a single day use get_my_timetable; for any target (teacher/class/room/student) over a range use get_timetable with end_date.

get_periodsA

Get the bell schedule (periods with start/end times). Read-only.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'periods': [{'starttime', 'endtime'}, ...]}.

Notes: - Combine with get_next_ringing_time for live bell timing.

get_gradesA

Get grades for the logged-in student. Read-only.

Args: year: School-year start year to filter by (e.g. 2025 for 2025/26). term: 'FIRST' or 'SECOND' to restrict the term. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'subdomain', 'grades': [serialized grades with subject, teacher, percent, ...]}.

Notes: - When both year and term are omitted returns the current gradebook. - Use get_school_year to resolve the current school-year start.

get_timelineA

Get EduPage timeline notifications, filtered by category. Read-only.

Args: category: Which event types to return: - 'recent' (default) — all currently visible notifications (homework, tests, messages, grades, events...). - 'history' — all notifications since date_from (incl. older ones). - 'homework' — homework assignments (formerly get_homework). - 'assignments' — homework, tests, exams and projects (formerly get_assignments). - 'absences' — absence records (formerly get_absences). - 'events' — upcoming events: trips, excursions, meetings, holidays... (formerly get_upcoming_events). - 'news' — school news (formerly get_news). date_from: YYYY-MM-DD. Only meaningful for category='history'. subdomain: School to query (defaults to the active subdomain).

Returns: dict with subdomain and the matching notifications, e.g. {'subdomain': ..., 'notifications': [...]} (key is the category name).

Notes: - Categories are derived from timeline notifications; a school that doesn't publish a given event type returns an empty list. - For a whole-day report (timetable, substitutions, meals, homework, events, news, grades) prefer get_day_summary — one call.

get_homework_materialA

Full body text and attachment list of one homework/assignment material. Read-only.

Fetches the school's material-player page for superid and flattens its widget tree into plain text plus absolute attachment URLs — the timeline notification carries only a short summary, never the assignment body or the attachment list, so this is the only way to read what the assignment says.

Args: superid: Material id, taken from additional_data.superid of a notification returned by get_timeline(category='homework'). subdomain: School whose session to use (defaults to the active subdomain).

Returns: dict: {'subdomain', 'superid', 'title', 'details', 'date_from', 'date_to', 'content', 'attachments'}. Each attachment is {'name', 'url'} with an absolute URL — hand url to download_attachment to save it.

Notes: - Discover ids first: get_timeline(category='homework') (or category='assignments') and read additional_data.superid. For a whole day of homework plus everything else prefer get_day_summary. - An invalid, expired or invisible id returns an actionable error, not a stack trace. - content is plain text; an enabled student upload area is rendered as '[Student answer / file upload area]'.

download_attachmentA

Download one attachment from the school to disk. Writes: creates a local file.

Saves any authenticated attachment — homework material, message and event attachments alike, which is what get_timeline puts in additional_data.attachements and get_homework_material lists — as the raw response bytes, never overwriting (a ' (1)', ' (2)', ... suffix is appended instead). School pages are not attachments: an HTML or login page is refused rather than written out. Because the bytes go to disk untouched, this is the only tool that returns a binary attachment intact; custom_request refuses one.

Args: url: Absolute attachment URL, or a school-relative path such as '/elearning/ruqjzfpv?z%3A…' — the exact form get_timeline(category='recent') returns in additional_data.attachements — resolved against the school origin. Any authenticated attachment URL works, not just homework: message and event attachments included. The request is authenticated with the school's session. dest_dir: Directory to save into, created when missing. Defaults to <tempdir>/homework (e.g. .../AppData/Local/Temp/homework). filename: Save under this name instead of the server-suggested one. Directory components and characters illegal on the filesystem are stripped, so the file always lands directly inside dest_dir. subdomain: School whose session authenticates the request (defaults to the active subdomain).

Returns: dict: {'saved_to', 'bytes', 'source_url', 'name'}.

Notes: - The only tool in this server that writes to disk, and the only one excluded from the read-only e2e suite — call it only on request. - Verified live 2026-10-02: a bad or expired attachment token is a plain HTTP 404, and a school page fetched without a session answers HTTP 200 with the login page. Both raise instead of writing a file, so a saved attachment is always real bytes. - The name comes from content-disposition when the server sends it, otherwise from the URL path. - Bounded by the library session's 5 s request timeout (Edupage(request_timeout=5)), so very large attachments can fail.

get_timetable_changesA

Get substitution/timetable changes for a date (default today). Read-only.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'changes': [serialized substitutions]}. Empty list when nothing changed or the school publishes none.

Notes: - Pair with get_missing_teachers for the full substitution picture. - For one student's plan on a day use get_student_timetable / get_timetable.

get_missing_teachersA

Get teachers missing on a date (default today). Read-only.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'teachers': [serialized missing teachers]}. Empty list when no teacher is missing.

Notes: - Pair with get_timetable_changes for the full substitution picture.

get_mealsA

Get the meal menu for a date. Read-only. Always returns all five meal slots (breakfast, snack, lunch, afternoon_snack, dinner) — slots not published by the school are None.

Args: date_str: YYYY-MM-DD (default today). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'date', 'subdomain', 'meals': {breakfast/snack/lunch/ afternoon_snack/dinner: menus}}. Each menu carries chooseable/ordered info usable with choose_meal / sign_off_meal.

Notes: - Tries the personal ordering endpoint first; when the school hasn't enabled it, falls back to the school's public canteen menu widget.

choose_mealA

Order/choose a meal. Writes: books the selected menu for the date.

Args: date_str: YYYY-MM-DD to order for. meal_type: 'snack' | 'lunch' | 'afternoon_snack'. number: 1-based menu choice among the chooseable menus (see get_meals). subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'ordered': True, meal_type, date, number}.

Notes: - Read get_meals first for the date to pick a valid number. - To cancel, use sign_off_meal.

sign_off_mealA

Cancel an ordered meal for a date. Writes: releases the booking.

Args: date_str: YYYY-MM-DD to cancel. meal_type: 'snack' | 'lunch' | 'afternoon_snack'. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'ordered': False, meal_type, date}.

rate_mealB

Rate a meal. Writes: submits quality/quantity ratings for a date and meal type.

Args: date_str: YYYY-MM-DD of the meal. meal_type: 'snack' | 'lunch' | 'afternoon_snack'. quality: Taste rating, 1-5. quantity: Portion-size rating, 1-5. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'rated': True, meal_type, date}.

get_day_summaryA

One-call daily school report for a date (default today): timetable, substitutions, missing teachers, grades received that day, meals, homework, assignments, absences, news, events, and timeline notifications.

Composes the individual section tools so you don't need to fire 8-10 calls to answer "what happened yesterday at school" or "what's coming tomorrow".

  • If name/student_id is provided: report for that specific student (found across all schools unless subdomain scopes it).

  • If omitted: discovery-first — for a parent this returns a lightweight per-school index of the account's children (no per-child section fetching), so you can then call per child with name/student_id. Set full=True to instead build the full report for every child.

  • If omitted and logged in as student/teacher: report on the logged-in account. Every section is isolated — a failure in one section yields {"ok": false, "error": ...} without failing the report.

Args: date_str: YYYY-MM-DD to report on (default today). name: Student to report for, by first/last/full name. Ambiguous names surface every candidate instead of guessing. Ignored when student_id is given; see find_student to resolve an id first. student_id: person_id of the student (preferred, unambiguous). Found across all schools unless subdomain scopes the lookup. subdomain: School to report on (defaults to the active subdomain). full: When no name/student_id is given and the account is a parent, build the full report for every child instead of returning the lightweight per-school index. Costs one section sweep per child.

get_rosterA

Get a school roster: students, teachers, classes, classrooms or subjects. Read-only.

Args: roster_type: Which roster to return: - 'students' — students in the logged-in user's class (formerly get_students). - 'all_students' — a short list of all students in the school (formerly get_all_students). - 'teachers' — all teachers (formerly get_teachers). - 'classes' — all classes (formerly get_classes). - 'classrooms' — all classrooms (formerly get_classrooms). - 'subjects' — all subjects (formerly get_subjects). subdomain: School to query (defaults to the active subdomain).

Returns: dict keyed by the roster name, e.g. {'subdomain': ..., 'teachers': [...], ...}.

Notes: - For the students visible to the logged-in account (parents: their linked children; students: classmates) prefer get_my_students. - To look one student up by name use find_student. - Returned person/class ids feed get_timetable (target_type/target_id) and switch_to_student.

send_messageA

Send a message to a recipient. Writes: posts a new message on the recipient's timeline.

Args: recipient_id: EduPage id like 'Student123' or 'Teacher456' (see get_roster). body: Message text. Must not be empty. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'sent': True, 'timeline_id': }.

Notes: - Recipient ids come from get_roster(roster_type='students'|'teachers') or get_my_students.

get_my_studentsA

Get the students visible to the logged-in account. Read-only: parent accounts see their linked children (parsed from the school homepage); student/teacher accounts see classmates. Uses cached data.

Args: subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'subdomain', 'students': [{person_id, name, class_id, ...}]} usable with switch_to_student and get_student_timetable.

Notes: - Prefer get_my_students over get_roster to see your children / classmates; get_roster(roster_type='all_students') lists the whole school. - Cache is refreshed by clear_student_cache; scan_students returns the same visibility across all schools.

switch_to_studentA

Switch the session to a student account (parent accounts only). Writes: changes which account subsequent tools operate as.

Args: student_id: person_id of the child (from get_my_students). name: first/last/full name of the child, used when student_id is omitted. subdomain: School to query (defaults to the active subdomain).

Returns: dict: {'switched_to_student': , 'user_id': ...}.

Notes: - Revert with switch_to_parent. Prefer the stateless get_student_timetable (name/student_id) over switching when you only need a timetable.

find_studentA

Look up a student by first/last/full name using tiered matching. Read-only. Without a subdomain, searches ALL logged-in schools and returns one result per school where the student is found.

Args: name: First, last or full student name (also 'Novák V.' short names). subdomain: Restrict the search to one school (default: all logged-in schools in scope).

Returns: dict with results: [{name, student_id, class_id, subdomain, tier, confidence}] sorted by confidence. Tiers: 1=exact, 2=first name, 3=last name, 4=substring.

Notes: - Use a student_id from the results with get_student_timetable / get_day_summary for unambiguous lookups. - Ambiguous matches surface all candidates instead of guessing.

get_subdomainsA

List the school subdomains available to the logged-in account and the server's session status per school, plus overall login state. Read-only.

Args: None. This tool takes no arguments — the active credentials and EDUPAGE_SUBDOMAINS determine the result entirely.

Returns: dict with: - subdomains: list of school subdomains available to the account. For a logged-in parent account this is live-discovered (via edupage-api's get_subdomains, read from the profile page) and includes schools with no session yet; it is limited to EDUPAGE_SUBDOMAINS when that is set (allowlist), otherwise every accessible school is listed. For student/teacher accounts or when not logged in it falls back to the configured / current sessions. - schools: per subdomain {subdomain, logged_in, role (student/parent/teacher), user_id, two_factor_pending, active}. When EDUPAGE_SUBDOMAINS is set it contains only the in-scope schools (sessions for unconfigured schools are never listed). - active_subdomain: the school used by tools without an explicit subdomain argument. - failed_logins: subdomain → error for login attempts that failed or are blocked (e.g. pending 2FA at startup). - env_*_set: whether EDUPAGE_USERNAME / EDUPAGE_PASSWORD / EDUPAGE_SUBDOMAINS are configured.

Notes: - Use this instead of the former auth_status / user_id tools. - To connect to a discovered subdomain that has no session yet, pass it to login_all. - A school with two_factor_pending: True needs two_factor_finish.

clear_student_cacheA

Force refresh of cached student data. Writes: drops the local cache so the next student lookup re-fetches from EduPage. Call this after students are added/removed from a school, or if scan_students/find_student seems stale.

Args: subdomain: School whose cache to clear. Without it, clears ALL schools.

Returns: dict: {'cleared': <subdomain|'all'>, 'entries_removed': }.

scan_studentsA

Discover all students visible to the logged-in account across the discovery scope (the configured EDUPAGE_SUBDOMAINS, or every school when unset). Read-only; uses cached data to avoid redundant API calls.

Args: None. This tool takes no arguments — the discovery scope is fixed by EDUPAGE_SUBDOMAINS and the logged-in role, both of which are environment/login state rather than per-call input.

Returns: dict: {'students': [{name, student_id, class_id, subdomain}], 'total': n}. For a parent account: their linked children in each school; for a student/teacher: classmates. One entry per student per school.

Notes: - get_my_students returns the same view for the active subdomain; clear_student_cache refreshes it.

switch_to_parentA

Switch the session back to the parent account (parent accounts only). Writes: changes which account subsequent tools operate as.

Args: subdomain: School whose session to restore (defaults to the active).

Returns: dict: {'switched_to_parent': True, 'user_id': ...}.

Notes: - Pair with switch_to_student; only relevant after a parent session was switched to a child.

custom_requestA

Send a raw request to the Edupage server using the active session. Can perform writes depending on the endpoint — treat as write-capable.

Args: url: Absolute URL, or a path like '/export/ajax_prevedene_meno.php' (resolved against https://<subdomain>.edupage.org). method: 'GET' or 'POST'. data: Request body (for POST). headers: JSON string of extra headers, e.g. '{"Accept": "application/json"}'. subdomain: School whose session to use (defaults to the active).

Returns: dict: {'status_code': int, 'content_type': str, 'bytes': int, 'text': str, 'truncated': bool}. bytes is the raw body length and text is it decoded.

Notes: - Low-level escape hatch for endpoints not covered by the dedicated tools — prefer those when available. Parse the returned text yourself; fields are not pre-serialized. - Text only. A binary body (an Office/PDF/image/video attachment) is refused rather than returned as lossily-decoded errors="replace" garbage; use download_attachment(url=…, subdomain=…), which writes the raw bytes to disk and accepts any authenticated attachment URL. - text is capped at 60000 characters so the cap is this tool's, not the MCP client's silent one. EduPage's own pages ignore Range, so a truncated text body cannot be paged through: narrow the request instead (a more specific path or a dedicated tool), or save the whole body with download_attachment.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A4.1/5.0

Scored across 31 tools

Disambiguation4/5

Most tools have clearly distinct purposes, and descriptions actively cross-reference siblings to disambiguate. Overlap exists in the timetable family (get_my_timetable/get_student_timetable/get_timetable/get_next_week_timetable) and the student-lookup family (find_student/get_my_students/scan_students/get_roster), which could cause occasional misselection despite the clarifying notes.

Naming Consistency5/5

Names follow a highly consistent snake_case verb_noun (or verb) pattern throughout: get_*, login, login_all, switch_to_*, choose_meal, rate_meal, send_message, find_student. No mixing of camelCase or inconsistent verb styles.

Tool Count3/5

At 31 tools the surface is heavy for a single-school portal client, and several clusters (four timetable tools, four student-lookup tools, three meal-write tools) could plausibly be consolidated. The domain is broad enough that the count is defensible, but it sits at the borderline-heavy end.

Completeness4/5

Strong coverage across auth (login/2FA/session switch), timetables, grades, meals (read/order/cancel/rate), messages, rosters, substitutions, homework material, attachments, and a raw custom_request escape hatch. Minor gaps remain (e.g., no inbox/read-message or event-detail tools), but core workflows are fully closed.

Maintenance

ActivityMaintained
ResponsivenessResponsive