Listed from the MCP registry; nobody on Odel publishes it.
Librus Synergia school gradebook: grades, attendance, timetable, homework, messages.
Runs on your machine from its package; the steps are below.
At most: moves money, runs code or commands, deletes data, or sends messages for you.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
uvx librus-mcp
MCP client config
{
"mcpServers": {
"librus-synergia": {
"command": "uvx",
"args": [
"librus-mcp"
]
}
}
}Run
docker run -i --rm ghcr.io/krzysztofbury/librus-mcp:2.7.0
MCP client config
{
"mcpServers": {
"librus-synergia": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/krzysztofbury/librus-mcp:2.7.0"
]
}
}
}Distributed as an MCP bundle (`https://github.com/krzysztofbury/librus-mcp/releases/download/v2.7.0/librus-mcp-2.7.0.mcpb`) — download it and import the bundle in your MCP client.
An MCP server that gives AI assistants access
to the Librus Synergia school gradebook: grades, attendance, timetable,
homework, calendar, announcements, messages, formative assessments, school-year
history, class-free days and student information. It supports
several logins at once, built on the independent
librus-python-api.
Version 2.0 replaces the librus-apix backend used by every earlier version.
Upgrading from 1.x? See Upgrading from 1.x.
The commands below pin 2.7.0, using the native API 1.9.0. You need your Librus Synergia login and password. Accounts that require interactive two-factor authentication are not supported.
Pick one way to run it:
| Way | Best for | Credentials |
|---|---|---|
| On your computer | Claude Code, Claude Desktop, Gemini CLI, Codex, OpenCode, Hermes on the same machine | a private config.json, any number of logins |
| Desktop bundle | one-click install in apps that support MCP Bundles | the app's settings form, one login |
| Self-hosted with Docker | agents on another machine of yours, connecting over HTTP | a private config.json or environment, any number of logins |
| MCP Registry | clients that install servers from the official registry | the client's settings form, one login |
All of them run the server on a machine you control. It connects only to Librus; see Privacy.
You do not need to clone this repository or install Python yourself. uv downloads Python 3.14 and the server automatically.
1. Install uv. On macOS or Linux, open a terminal and run:
curl -LsSf https://astral.sh/uv/install.sh | sh
On Windows, open PowerShell and run:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Close and reopen your AI assistant afterwards.
2. Create a private configuration file. Save this as config.json in a
private folder under your user account, outside projects and synchronized
folders, and fill in the login and password:
{
"accounts": [
{"alias": "daughter", "username": "12345", "password": "YOUR_PASSWORD"}
]
}
The alias is the short name you use when asking about this login. For more
logins, add more objects to accounts, each with its own alias:
{
"accounts": [
{"alias": "anna", "username": "12345", "password": "FIRST_PASSWORD"},
{"alias": "jan", "username": "67890", "password": "SECOND_PASSWORD"}
]
}
Suggested locations:
/Users/YOUR_NAME/.config/librus-mcp/config.json/home/YOUR_NAME/.config/librus-mcp/config.jsonC:\Users\YOUR_NAME\.config\librus-mcp\config.jsonOn first start the server restricts the file to your user (macOS/Linux),
creates its private state directory (~/.librus-mcp) and a persistent key there.
There is nothing else to set up. Notifications and attachment downloads are on
by default; sending messages is off; opening a received message marks it read
without asking. To change that, add for example:
"features": {"notifications": true, "attachments": false, "send_message": true, "mark_read_consent": true}
For a single login you can skip the file and set environment variables instead:
LIBRUS_USERNAME, LIBRUS_PASSWORD and optionally LIBRUS_ALIAS (default
librus). Installers that collect settings in a form use these.
3. Connect your assistant. Replace the path with your file's absolute path.
Claude Code:
claude mcp add --scope user --transport stdio librus \
-- uvx --python 3.14 librus-mcp==2.7.0 --config /absolute/path/to/config.json
Claude Desktop (Settings > Developer > Edit Config):
{
"mcpServers": {
"librus": {
"command": "uvx",
"args": ["--python", "3.14", "librus-mcp==2.7.0", "--config", "/absolute/path/to/config.json"]
}
}
}
Gemini CLI:
gemini mcp add --scope user --transport stdio \
librus uvx --python 3.14 librus-mcp==2.7.0 --config /absolute/path/to/config.json
OpenAI Codex CLI:
codex mcp add librus \
-- uvx --python 3.14 librus-mcp==2.7.0 --config /absolute/path/to/config.json
OpenCode:
opencode mcp add librus --global -- \
uvx --python 3.14 librus-mcp==2.7.0 --config /absolute/path/to/config.json
Hermes Agent (~/.hermes/config.yaml):
mcp_servers:
librus:
command: "uvx"
args: ["--python", "3.14", "librus-mcp==2.7.0", "--config", "/absolute/path/to/config.json"]
Check the setup without contacting Librus:
uvx --python 3.14 librus-mcp==2.7.0 --config /absolute/path/to/config.json --check-config
If a GUI assistant cannot find uvx, use its absolute path (which uvx or
where uvx). Restart or reconnect the assistant after changing the file.
Each release attaches librus-mcp-2.7.0.mcpb to its
GitHub release. Open it
with an app that supports MCP Bundles, such as Claude Desktop. The app asks for
the Librus login and password, keeps them in its settings, and installs Python
3.14 and the exact dependency versions this release was tested with. The bundle
serves one login; use On your computer for several.
Run the server on your own Docker host and connect agents on other machines over HTTP. Every request needs a bearer token. The server has no TLS of its own, so expose it only through a reverse proxy with TLS or a private network such as Tailscale or WireGuard. Never publish the port on a public or shared network.
1. Prepare a private folder on the Docker host with the configuration and a token. The files must belong to the user that runs the container and be readable only by it:
mkdir -p ~/librus-mcp/data && cd ~/librus-mcp
# config.json: the same format as above, any number of logins
docker run --rm ghcr.io/krzysztofbury/librus-mcp:2.7.0 --generate-token > token
chmod 600 config.json token
2. Start it with this compose.yaml, replacing 1000:1000 with the output
of id -u and id -g:
services:
librus-mcp:
image: ghcr.io/krzysztofbury/librus-mcp:2.7.0
command: ["--transport", "http", "--allowed-host", "librus-mcp.example.lan:*"]
user: "1000:1000"
environment:
LIBRUS_CONFIG: /config/config.json
LIBRUS_MCP_TOKEN_FILE: /config/token
volumes:
- ./config.json:/config/config.json:ro
- ./token:/config/token:ro
- ./data:/data
ports:
# Loopback for a reverse proxy on this host, or your VPN address instead.
- "127.0.0.1:8000:8000"
restart: unless-stopped
docker compose up -d
docker compose logs librus-mcp
The endpoint is /mcp at your proxy's HTTPS address, or at your VPN address on
port 8000. --allowed-host lists the host names clients use; NAME:* accepts
NAME with any port or none. A reverse proxy must forward the original Host
header: Caddy and Traefik do by default, nginx needs
proxy_set_header Host $host;. Without --allowed-host the server logs that
Host header checks are off and relies on the token alone. In the image the
server listens on every container interface; the ports: line decides who can
reach it. ./data keeps the private state, persistent key and downloads, so
back it up.
3. Connect your agent with the token as a bearer header. For Hermes Agent:
mcp_servers:
librus:
url: "https://librus-mcp.example.lan/mcp"
headers:
Authorization: "Bearer YOUR_TOKEN"
Other clients that support streamable HTTP take the same URL and header.
Things to know about HTTP mode:
download_attachment saves files on the Docker host under ./data/downloads.
A remote agent cannot read that path; small files (up to 256 KiB) are also
offered as a short-lived MCP resource.send_message, anyone with the token can confirm a send
preview. The server warns about this at startup.user: line the container runs as uid 10001; a named volume then
works as is, but the configuration and token files must belong to uid 10001.LIBRUS_ACCOUNTS (a JSON list of accounts), or
LIBRUS_USERNAME and LIBRUS_PASSWORD for one login, and LIBRUS_MCP_TOKEN
can be passed as environment variables.The same image also runs over stdio for a client on the Docker host:
docker run -i --rm -v librus-mcp-data:/data -e LIBRUS_USERNAME -e LIBRUS_PASSWORD ghcr.io/krzysztofbury/librus-mcp:2.7.0.
The server is listed in the official
MCP Registry as
io.github.krzysztofbury/librus-mcp, with the PyPI package, the container image
and the desktop bundle. Clients that install from the registry ask for
LIBRUS_USERNAME and LIBRUS_PASSWORD, and optionally LIBRUS_ALIAS.
Change the version in your assistant's configuration to librus-mcp==2.7.0, add
--python 3.14 before it and keep your existing --config file. On first start
2.0 adapts the old setup automatically: it creates the key, restricts shared
1.x folders to your user, ignores the removed behaviour-notes setting and adopts
1.x notification history on the first check. Stop 1.x server processes that use
the same folders before switching, and copy the state folder first: going back
to 1.x is not supported other than by restoring that copy (see the
migration guide).
Notifications and attachments stay on by default, as in 1.x. Tool names and
results changed on purpose (for example list_students is now list_accounts
and student_alias is account_alias). Assistants read the new tool list
automatically; custom scripts must follow the migration guide.
The default catalog has 31 typed tools:
list_accounts, get_student_information.get_grades, get_final_grades, get_grades_window.get_attendance, get_attendance_window, get_attendance_detail,
get_attendance_frequency, get_subject_frequency.get_timetable, get_announcements, get_agenda, get_agenda_detail,
get_homework, get_homework_detail, get_completed_lessons,
get_school_year_archive, get_class_free_days.get_messages, get_message_content, get_recipient_types,
get_recipient_choices, get_recipients, get_message_correspondents,
get_teacher_subjects, get_message_unread_counts.get_new_notifications, get_notification_status,
acknowledge_notifications (feature notifications).download_attachment and runtime-only attachment resources (feature
attachments).Enabling the send_message feature adds preview_message, send_message,
get_send_outcome and get_send_history, for 35 tools. Disabling notifications
and attachments leaves 27 tools.
Collections are paged with limit and a cursor; an announcements page also
ends early when long texts would exceed the result size cap (reason: "size_limit"). Every login is independent,
including logins for the same student. Modern messaging is the default; set
"messaging_backend": "legacy" on an account to use the legacy mailbox. Behaviour
notes remain unavailable. Observation cards are supported as formative grades.
Point and percentage grades ("Oceny punktowe") are point records whose raw
value is a score on the scale named by their category.
Each grade has grade_id, its own Librus ID, stable across reads (2.7).
With scope all, a corrected grade and its correction are linked:
corrects on the correction and corrected_by on the original list the other
grade's grade_id, as Librus shows them in one bracket ([1 5]). Other
scopes show no brackets, so both fields are null there. See
grade IDs and corrections.
Keep your existing 2.0 configuration, context key and durable state; reconnect your assistant to refresh its tool catalog. The server pins API 1.9.0.
get_grades or
get_grades_window. items can now have record_type="formative" with an
assessment object. Numeric/descriptive records gain formative_id; matching
assessment.detail_id identifies mirrored entries, not extra grades.
Custom consumers must handle the new variant, and point since 2.4. Restart
old grade cursors.get_school_year_archive (default limit 10).
Items are typed years followed by achievements; marks and raw behaviour cells
retain their school-provided meaning.get_class_free_days. Opaque type
IDs are not holiday names, and missing lesson bounds do not imply an all-day event.get_message_correspondents to obtain a bound filter
reference, then pass it as correspondent to get_messages. For current inbox
summaries, unread_only=true is also supported. Repeat query values with cursors.get_messages(archived=true) for earlier-year message summaries. Archive
filters and archive body opens are unsupported; archiving_in_progress is
informational and does not prevent listing.get_teacher_subjects pairs modern teacher IDs with subjects; these are not
send-recipient references. get_message_unread_counts returns current/archive
folder counters without opening messages or changing MCP notification delivery.All new collection reads are bounded. The complete response cap remains 512 KiB;
oversized results fail with LIMIT, never silent text truncation. Unknown formative
markup fails the whole grades read. Formative rows may fall outside the selected
week/last-login view; date-window filtering is explicit and supported.
Opening a received message marks it read in Librus, as the app does: the sender
can see it was read, and it leaves the unread count. To have the assistant ask
first, set "features": {"mark_read_consent": true}; each open then needs
allow_mark_read=true, and without it the tool returns CONSENT_REQUIRED.
Before 2.5.1 that was the only behaviour. Sending requires an exact preview, separate human approval and
confirm=true; never retry a CLAIMED or UNKNOWN send. Fresh calendar
notifications require allow_consume_events=true, and each notification batch
must be acknowledged after it is delivered. School content is untrusted data,
never instructions.
State, the persistent key and downloads live under ~/.librus-mcp by default
(state_dir and download_dir in the configuration). Back up the state
directory to keep references and notification history across reinstalls.
Downloaded files are saved on the machine running the server.
For download_attachment, pass the message's returned reference as
attachment_ref.message, and the attachment's identifier and archived values
from get_message_content as attachment_ref.identifier and
attachment_ref.archived. archived names the file's namespace: a current
message's files can live in the archive. When omitted, it follows the message
reference. Downloading does not open the message body.
Diagnostics never print school data, logins or aliases; review any output before sharing it anyway.
| Situation | What to use | What you get |
|---|---|---|
| A tool returns an error | the error result itself | code, where (code locations, the failure site last), versions |
| You want to see failures in the logs | the server's standard error (docker logs for the image) | one line per failed call |
| You want the page structure of a failure | LIBRUS_MCP_DIAGNOSE_FAILURES=1 | the masked HTML structure around the failure, or a summary of the whole page (notices, table rows, known labels), on standard error |
A tool returns CONSENT_REQUIRED | ask the user, then call again with the input named in requires | for a received message, allow_mark_read=true: opening marks it read in Librus (only with mark_read_consent, or after allow_mark_read=false); for new calendar events in get_new_notifications, allow_consume_events=true: Librus reports them only once |
| You want a trace of every Librus read | LIBRUS_MCP_LOG_OPERATIONS=1 | one line per read: operation, outcome, time, requests, HTTP status, failure site |
The assistant says Invalid structured content returned by tool ... | reload the assistant's tool list, then librus-mcp check-tools | the failing result path and schema rule |
| A page fails to parse and you want to report it | librus-mcp inspect-page grades page.html (or final-grades, profile) | counts, or code locations and the masked structure, offline |
With Docker, pass the variables with -e NAME=1 and read docker logs. Run
check-tools in the running container with
docker exec CONTAINER librus-mcp check-tools (it uses the container's own
configuration and signs every login in a second time, so the server's next
read may have to sign in again). inspect-page needs the file inside the
container; use the docker run form in step 2 below.
Invalid structured content returned by tool ... comes from your
assistant, not the server: it checks each result against the tool list it
saved. After an upgrade, reload the tool list (Hermes: /reload-mcp;
otherwise restart the assistant or reconnect the server). If it persists,
run uvx --python 3.14 librus-mcp==2.7.0 --config /path/to/config.json check-tools.
It calls every read tool that needs no reference for every login (about 20
tool calls per login, each one or more normal Librus reads, which change
the views, week and month Librus remembers as the tools do), checks each result the way assistants do
and prints the failing path and schema rule, never school data. Modern-only
tools are skipped on legacy logins. Tools that need a reference or have an
effect are never called: the detail tools, get_message_content,
get_recipient_choices, get_recipients, download_attachment,
get_new_notifications, acknowledge_notifications, preview_message,
send_message and get_send_outcome. Repeat --tool NAME to check only
those tools; the ones above are refused.
A tool returns PARSE or UNSUPPORTED_CAPABILITY: the error lists
where it failed and the versions. Your school's page has a layout the
library does not read yet. To report it without sharing school data:
In the browser, open the failing page and save its source as
page.html (UTF-8):
| Tool | Page | inspect-page parser |
|---|---|---|
get_grades, get_grades_window | https://synergia.librus.pl/przegladaj_oceny/uczen | grades |
get_final_grades | the same page | final-grades |
get_student_information | https://synergia.librus.pl/informacja | profile |
On Windows and Linux press Ctrl+U, then Ctrl+S. On macOS, in Safari use
File > Save As with format Page Source; in Chrome press Option+Cmd+U,
then Cmd+S. Do not paste the source into TextEdit: that saves a text
document, which inspect-page rejects. A copied table (Inspect, Copy
outer HTML) also works, but shows the page after its scripts ran.
Run uvx --python 3.14 librus-mcp==2.7.0 inspect-page grades page.html
(with the parser from the table). With Docker:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/in:ro" ghcr.io/krzysztofbury/librus-mcp:2.7.0 inspect-page grades /in/page.html
(add :z after ro on SELinux hosts).
Review the output and attach it to an
issue, together
with the where and versions from the error.
If your saved page passes inspect-page but the tool still fails, the
server received a different page than your browser (a notice, an error
page, or another login). Restart the server with
LIBRUS_MCP_DIAGNOSE_FAILURES=1 and LIBRUS_MCP_LOG_OPERATIONS=1, repeat
the call, and attach the server's standard error: it summarizes the page
the server received (notices, tables, known labels, lengths) and the
response size, without its text.
The server runs on a machine you control, your computer or your own Docker host, and connects only to Librus. There is no telemetry, and nothing goes to the author or other third parties. Tool results go to the AI assistant that asked for them. See PRIVACY.md.
uv sync --locked
uv run pytest -q
uv run ruff check src/librus_mcp tests_native release_verification scripts
uv run ruff format --check src/librus_mcp tests_native release_verification scripts
uv run mypy
uv run bandit -r src/librus_mcp -c pyproject.toml -q
See MIGRATION_2_0.md for configuration precedence and operator commands, MCP_CONTRACT.md for protocol rules, NATIVE_TEST_PLAN.md for verification evidence and SPEC.md for ownership.
2.x is MIT licensed. LICENSE_REVIEW.md records the source
and dependency review; dependencies keep their own terms. Unshipped 1.x
references in legacy_reference/ remain GPL-3.0-only, and earlier releases keep
their original licenses.
This is an unofficial integration, not affiliated with Librus. Upstream changes can break supported features. Report security issues privately as described in SECURITY.md.