Metadata-Version: 2.4
Name: librus-mcp
Version: 2.3.0
Summary: MCP server for Librus Synergia using the independent librus-python-api
Project-URL: Homepage, https://github.com/krzysztofbury/librus-mcp
Project-URL: Repository, https://github.com/krzysztofbury/librus-mcp
Project-URL: Issues, https://github.com/krzysztofbury/librus-mcp/issues
Author-email: Krzysztof Bury <contact@datacraze.io>
License-Expression: MIT
License-File: LICENSE
Keywords: claude,gemini,gradebook,librus,llm,mcp,synergia
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.14
Requires-Dist: librus-python-api==1.6.4
Requires-Dist: mcp<3,>=2.2.0
Requires-Dist: pydantic<3,>=2.12
Requires-Dist: pywin32>=312; sys_platform == 'win32'
Requires-Dist: uvicorn<1,>=0.35
Description-Content-Type: text/markdown

# Librus MCP Server

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![PyPI](https://img.shields.io/pypi/v/librus-mcp)](https://pypi.org/project/librus-mcp/)

An [MCP](https://modelcontextprotocol.io/) 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`](https://github.com/krzysztofbury/librus-python-api).

<!-- mcp-name: io.github.krzysztofbury/librus-mcp -->

Version 2.0 replaces the `librus-apix` backend used by every earlier version.
Upgrading from 1.x? See [Upgrading from 1.x](#upgrading-from-1x).

## Quick start

The commands below pin **2.3.0**, using the native API 1.6.4. 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](#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](#desktop-bundle-preview) | one-click install in apps that support MCP Bundles | the app's settings form, one login |
| [Self-hosted with Docker](#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](#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](#privacy).

### On your computer

You do not need to clone this repository or install Python yourself.
[uv](https://docs.astral.sh/uv/) downloads Python 3.14 and the server
automatically.

**1. Install uv.** On macOS or Linux, open a terminal and run:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

On Windows, open PowerShell and run:

```powershell
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:

```json
{
  "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:

```json
{
  "accounts": [
    {"alias": "anna", "username": "12345", "password": "FIRST_PASSWORD"},
    {"alias": "jan", "username": "67890", "password": "SECOND_PASSWORD"}
  ]
}
```

Suggested locations:

- macOS: `/Users/YOUR_NAME/.config/librus-mcp/config.json`
- Linux: `/home/YOUR_NAME/.config/librus-mcp/config.json`
- Windows: `C:\Users\YOUR_NAME\.config\librus-mcp\config.json`

On 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. To change that, add for example:

```json
"features": {"notifications": true, "attachments": false, "send_message": 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:

```bash
claude mcp add --scope user --transport stdio librus \
  -- uvx --python 3.14 librus-mcp==2.3.0 --config /absolute/path/to/config.json
```

Claude Desktop (Settings > Developer > Edit Config):

```json
{
  "mcpServers": {
    "librus": {
      "command": "uvx",
      "args": ["--python", "3.14", "librus-mcp==2.3.0", "--config", "/absolute/path/to/config.json"]
    }
  }
}
```

Gemini CLI:

```bash
gemini mcp add --scope user --transport stdio \
  librus uvx --python 3.14 librus-mcp==2.3.0 --config /absolute/path/to/config.json
```

OpenAI Codex CLI:

```bash
codex mcp add librus \
  -- uvx --python 3.14 librus-mcp==2.3.0 --config /absolute/path/to/config.json
```

OpenCode:

```bash
opencode mcp add librus --global -- \
  uvx --python 3.14 librus-mcp==2.3.0 --config /absolute/path/to/config.json
```

Hermes Agent (`~/.hermes/config.yaml`):

```yaml
mcp_servers:
  librus:
    command: "uvx"
    args: ["--python", "3.14", "librus-mcp==2.3.0", "--config", "/absolute/path/to/config.json"]
```

Check the setup without contacting Librus:

```bash
uvx --python 3.14 librus-mcp==2.3.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.

### Desktop bundle (preview)

Each release attaches `librus-mcp-2.3.0.mcpb` to its
[GitHub release](https://github.com/krzysztofbury/librus-mcp/releases). 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](#on-your-computer) for several.

### Self-hosted with Docker

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:

```bash
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.3.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`:

```yaml
services:
  librus-mcp:
    image: ghcr.io/krzysztofbury/librus-mcp:2.3.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
```

```bash
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:

```yaml
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:

- One token grants every configured login. Run one deployment per trusted
  person, and replace the token if it leaks.
- Let one agent poll notifications for each login: delivery and
  acknowledgement are designed for a single consumer.
- `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.
- If you enable `send_message`, anyone with the token can confirm a send
  preview. The server warns about this at startup.
- Without a `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.
- Instead of files, `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.3.0`.

### MCP Registry

The server is listed in the official
[MCP Registry](https://registry.modelcontextprotocol.io/) 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`.

## Upgrading from 1.x

Change the version in your assistant's configuration to `librus-mcp==2.3.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.

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](MIGRATION_2_0.md).

## Tools and safety

The default catalog has 31 typed tools:

- Accounts/profile: `list_accounts`, `get_student_information`.
- Grades: `get_grades`, `get_final_grades`, `get_grades_window`.
- Attendance: `get_attendance`, `get_attendance_window`, `get_attendance_detail`,
  `get_attendance_frequency`, `get_subject_frequency`.
- School: `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`.
- Communication: `get_messages`, `get_message_content`, `get_recipient_types`,
  `get_recipient_choices`, `get_recipients`, `get_message_correspondents`,
  `get_teacher_subjects`, `get_message_unread_counts`.
- Notifications: `get_new_notifications`, `get_notification_status`,
  `acknowledge_notifications` (feature `notifications`).
- Files: `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`. 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.

### What's new in 2.1

Keep your existing 2.0 configuration, context key and durable state; reconnect
your assistant to refresh its tool catalog. The server pins API 1.6.4.

- Ask for formative assessments or observation cards using `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. Restart old grade cursors.
- Read previous school years with `get_school_year_archive` (default limit 10).
  Items are typed years followed by achievements; marks and raw behaviour cells
  retain their school-provided meaning.
- Read date and optional lesson ranges with `get_class_free_days`. Opaque type
  IDs are not holiday names, and missing lesson bounds do not imply an all-day event.
- On modern accounts, use `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.
- Use `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 requires `allow_mark_read=true`, because Librus marks
it read. 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.

## Privacy

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](PRIVACY.md).

## Development

```bash
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](MIGRATION_2_0.md) for configuration precedence and
operator commands, [MCP_CONTRACT.md](MCP_CONTRACT.md) for protocol rules,
[NATIVE_TEST_PLAN.md](NATIVE_TEST_PLAN.md) for verification evidence and
[SPEC.md](SPEC.md) for ownership.

## License and limitations

2.x is MIT licensed. [LICENSE_REVIEW.md](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](SECURITY.md).
