Skip to main content
Glama
README.md
# osiris-mcp-server

An MCP server exposing Dutch university Osiris course catalogs as tools. It
reads from the public, read-only Osiris Student API that universities publish
at `https://<host>.osiris-student.nl/` (e.g. TU/e, TU Delft, Radboud,
Twente, Wageningen).

## Quick start

Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).

```bash
uv sync
uv run osiris          # run the MCP server over stdio
```

The server targets TU/e (`tue`) by default and accepts an `institution` code
per call, e.g. `tue`, `tudelft`, `rug`, `ut`:

```text
search_courses("4DM00", institution="tudelft")
get_course("4DM00", institution="tue")
```

## Usage with MCP clients

Add the server to your MCP client configuration.

```json
{
  "mcpServers": {
    "osiris": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/osiris-mcp-server", "osiris"]
    }
  }
}
```

For clients that support environment variables (e.g. `OSIRIS_LANGUAGE`,
`OSIRIS_TIMEOUT`), pass them via the `env` key.

## Tools

| Tool | Description |
| --- | --- |
| `list_faculties()` | Faculties/departments, code + name |
| `list_course_types()` | Course types (Bachelor College, Graduate School, ...) |
| `get_available_years()` | Academic years in the catalog |
| `get_categories()` | Course categories/levels |
| `get_blocks()` | Period/block tokens (e.g. `1`, `GS1`, `JAAR`) |
| `get_timeslots()` | Timeslots (e.g. `A`, `B1`) |
| `get_units()` | Coordinating units (capaciteitsgroep/sectie) |
| `get_lecturers()` | Lecturer names |
| `get_languages()` | Instruction languages |
| `get_education_types()` | Education types (e.g. `Challenge based`) |
| `get_course_structure()` | Sections/fields a course contains |
| `list_courses(faculty, course_type, ...)` | `{code, id, name, year}` list for a faculty + type |
| `get_course(code, year)` | Course header (id, code, name, ECTS, course_type, owner, level, year, blocks) |
| `get_course_details(code)` | Full detail: content, exams, lecturers, materials, deep link, ... |
| `get_course_details_by_id(id)` | Full detail by internal course id |
| `search_courses(keyword, ...)` | Full-text search with filters |
| `search_minors(keyword, ...)` | Search minors (coherent packages, certificates) |
| `get_minor_details(code)` | Full minor detail: study programme, entrance requirements, ... |
| `get_minor_details_by_id(id)` | Full minor detail by internal minor id |

The `list_courses` and `search_courses` tools accept the same filters; every
one is optional except `faculty` for `list_courses`:

| Filter | Description |
| --- | --- |
| `faculty` | Faculty code (e.g. `EE`) or full name |
| `course_type` | Course-type code or full name (default `BC`) |
| `category` | Category/level (e.g. `Introductory`, `Advanced`) |
| `year` | Start year (`2025`) or range (`2025-2026`) |
| `period` | Block token (e.g. `1` for bachelor Q1, `GS1` for graduate Q1) |
| `timeslot` | Timeslot code, prefix match (e.g. `A` matches `A1`, `A2`, ...) |
| `unit` | Coordinating unit (capaciteitsgroep/sectie) |
| `lecturer` | Lecturer name |
| `language` | Instruction language (name or 2-letter code) |
| `education_type` | Education type (e.g. `Challenge based`) |
| `available` | Only courses with places available (`true`) or not (`false`) |

Search results include a `blocks` list per course with the offering periods:
block token, dates, timeslots, location and availability — plus `lecturers`,
`education_type`, faculty and coordinating unit.

`search_minors` accepts the same `year`, `faculty` and `education_type` filters;
results include `startmoments` (academic year + block the minor can be started)
and `study_programs` (degree programmes the minor serves).

## Configuration

| Variable | Default | Purpose |
| --- | --- | --- |
| `OSIRIS_RELEASE_VERSION` | *(unset)* | Optional search-index header |
| `OSIRIS_MANIFEST` | *(unset)* | Optional search-index header |
| `OSIRIS_LANGUAGE` | `EN` | Content language (`NL` or `EN`) |
| `OSIRIS_TIMEOUT` | `30` | HTTP timeout in seconds |

Most institutions serve a sensible default index without `OSIRIS_RELEASE_VERSION`
or `OSIRIS_MANIFEST`. Only set them to pin a specific Elasticsearch index
version (e.g. one extracted from a university's Osiris app bundle).

## Development

```bash
uv run pytest           # unit tests (offline)
uv run pytest -m e2e    # live tests against the configured catalog (default TU/e)
uv run ruff check .
uv run ruff format --check .
uv run mypy src
```

## License

The code is MIT licensed; see [LICENSE](LICENSE).

The course data comes from the relevant university's Osiris API and is owned
by that institution. This project is not affiliated with or endorsed by any
university, and the MIT license does not cover the underlying course data.

TDQS

B3.1/5.0

Scored across 10 tools

Disambiguation4/5

Most tools are clearly distinct: list/get operations separate by resource (years, categories, faculties, course_types, courses). The main ambiguity is between get_course (header), get_course_details (full details), and get_course_details_by_id (full details by internal id) — get_course_details and get_course_details_by_id return the same data but keyed differently, and get_course's 'header' vs details boundary could cause misselection.

Naming Consistency4/5

The set follows a consistent get_/list_ verb prefix pattern (get_available_years, get_categories, list_courses, get_course, search_courses). Minor deviations exist: search_courses uses a different verb while other exploration tools use get_/list_, and get_course_details_by_id introduces the '_by_id' suffix that isn't used elsewhere.

Tool Count4/5

Ten tools is within the ideal range for a catalog/query server. Each tool earns a place: three discovery tools (years, categories, faculties/course_types), course listing/search, and course retrieval at different granularities.

Completeness4/5

The tool surface is strong for a read-only catalog: discovery (faculties, years, course types, categories), search, listing, and multi-level retrieval (header vs full details). The main gap is that get_course_details_by_id is redundant with get_course_by_code — agents could hit dead ends if they don't know which lookup path to use; a by-code variant of full details is also missing from the direct route.

Maintenance

ActivitySlowing
ResponsivenessNo issues