Skip to main content
Glama
kesavakantipudi

University Course Catalog MCP Server

README.md
# University Course Catalog MCP Server

A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that exposes a
university's course catalog to LLM assistants. It gives AI agents the ability to search
courses, inspect prerequisites, build prerequisite dependency graphs, and look up
instructors — backed by a local SQLite database and fully containerized with Docker.

This is the backend for an **AI-powered academic advisor**: a model can query the server
in real time to help students plan schedules, understand course dependencies, and find
the right instructor.

---

## Features

- **MCP Tools** — four validated, LLM-callable functions:
  - `search_courses` — keyword search across titles, descriptions and codes, optionally
    filtered by department code.
  - `get_prerequisites` — the direct prerequisites of a course.
  - `lookup_instructor` — instructor contact details by name.
  - `get_prerequisite_graph` — the full transitive prerequisite dependency graph
    (computed with NetworkX) as an adjacency list.
- **MCP Resources** — contextual text bodies the model can load:
  - `course_descriptions` — a formatted list of every course and its description.
  - `department_directory` — the full department list with their codes.
- **MCP Prompt Templates**:
  - `course_comparison_template` — a reusable template (`{{course_code_1}}`,
    `{{course_code_2}}`) that guides structured course comparisons.
- **Data integrity** — every tool input/output is validated with **Pydantic** schemas;
  data access uses **SQLAlchemy** (an ORM, which prevents SQL injection).
- **Persistence** — SQLite database stored in `./data/catalog.db`, mounted as a volume.
- **Containerized** — one command: `docker compose up`.

---

## Project Structure

```
.
├── data/
│   ├── catalog.db            # Seeded SQLite database
│   └── seed_script/
│       └── seed.py           # Idempotent seeding script
├── src/
│   ├── __init__.py
│   ├── config.py             # Environment configuration
│   ├── database.py           # Engine + session helpers
│   ├── models.py             # SQLAlchemy ORM models
│   ├── schemas.py            # Pydantic validation contracts
│   ├── seed.py               # Shared seeding logic + seed data
│   ├── server.py             # MCP server: tools, resources, prompts
│   └── main.py               # Entry point (seeds + serves HTTP)
├── .env.example              # Documented environment variables
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md
```

---

## Quick Start with Docker (recommended)

Prerequisites: Docker with the Compose plugin.

```bash
# From the repository root
docker compose up --build
```

The service builds the image, maps port `8080`, mounts `./data` so the database
persists, seeds the catalog on first start, and runs a health check.

- Health check: <http://localhost:8080/health>
- MCP endpoint: <http://localhost:8080/mcp>
- Stop the server: `docker compose down`

To confirm the container is healthy:

```bash
docker compose ps
```

You should see `mcp-server` with a `healthy` status within about a minute.

---

## Running Locally (without Docker)

Requires Python 3.11+.

```bash
# 1. Create and activate a virtual environment
python -m venv .venv
# Windows: .venv\Scripts\activate   |  macOS/Linux: source .venv/bin/activate

# 2. Install dependencies
pip install -r requirements.txt

# 3. (Optional) configure environment
# Copy .env.example to .env and adjust if needed.
# Default: DATABASE_URL=sqlite:///./data/catalog.db

# 4. Seed the database (idempotent — safe to run repeatedly)
python data/seed_script/seed.py

# 5. Start the server
python -m src.main
```

The server listens on `http://localhost:8080`.

---

## Connecting an MCP Client

Point any MCP client at the Streamable HTTP endpoint:

```
http://localhost:8080/mcp
```

Example using the **MCP Inspector**:

```bash
npx @modelcontextprotocol/inspector
# URL: http://localhost:8080/mcp
```

You can also connect programmatically with the official `mcp` Python SDK:

```python
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    async with streamablehttp_client("http://localhost:8080/mcp") as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool("get_prerequisites", {"course_code": "CS201"})
            print(result)

asyncio.run(main())
```

---

## Tools

All tool inputs and outputs are validated with Pydantic. On unknown input the tools
return a structured error, e.g. `{"error": "Course not found"}`.

### `search_courses`

Searches the catalog by keyword (case-insensitive match against title, description and
course code), optionally restricted to a department code.

| Parameter        | Type   | Required | Description                                    |
| ---------------- | ------ | -------- | ---------------------------------------------- |
| `query`          | string | yes      | Keyword to search for.                         |
| `department_code`| string | no       | Restrict results to a department (e.g. `CS`).  |

Output (success):

```json
[{ "course_code": "CS101", "title": "Introduction to Programming", "credits": 3 }]
```

Returns `[]` when nothing matches.

### `get_prerequisites`

Returns the **direct** prerequisites of a course.

| Parameter    | Type   | Required | Description                       |
| ------------ | ------ | -------- | --------------------------------- |
| `course_code`| string | yes      | E.g. `CS201`.                     |

Output (success):

```json
{
  "course_code": "CS201",
  "prerequisites": [
    { "course_code": "CS102", "title": "Data Structures and Algorithms" }
  ]
}
```

Empty list when the course has no prerequisites; `{"error": "Course not found"}` for an
unknown code.

### `lookup_instructor`

Finds an instructor by full or partial name.

| Parameter         | Type   | Required | Description                     |
| ----------------- | ------ | -------- | ------------------------------- |
| `instructor_name` | string | yes      | E.g. `Grace Hopper`.            |

Output (success):

```json
{
  "name": "Dr. Grace Hopper",
  "email": "grace.hopper@university.edu",
  "department_name": "Computer Science"
}
```

`{"error": "Instructor not found"}` when no match exists.

### `get_prerequisite_graph`

Returns the full prerequisite dependency graph for a course — the course itself plus
every course in its transitive prerequisite chain — as an adjacency list. The graph is
built with **NetworkX** (`source` is a prerequisite for `target`).

| Parameter    | Type   | Required | Description              |
| ------------ | ------ | -------- | ------------------------ |
| `course_code`| string | yes      | E.g. `CS401`.            |

Output (success):

```json
{
  "nodes": [{ "id": "CS401" }, { "id": "CS201" }, { "id": "CS102" }, { "id": "CS101" }],
  "edges": [
    { "source": "CS101", "target": "CS102" },
    { "source": "CS102", "target": "CS201" },
    { "source": "CS201", "target": "CS401" }
  ]
}
```

---

## Resources

### `course_descriptions`

`catalog://course_descriptions` — a single plain-text body listing every course:

```
[CS101] Introduction to Programming: A foundational course on programming principles...
[CS102] Data Structures and Algorithms: ...
```

### `department_directory`

`catalog://department_directory` — a directory of all departments:

```
Computer Science (CS)
Mathematics (MATH)
Physics (PHYS)
```

---

## Prompt Template

### `course_comparison_template`

A reusable template that guides the model to produce a structured comparison of two
courses:

> Create a table comparing the following two courses: `{{course_code_1}}` and
> `{{course_code_2}}`. Include columns for Course Code, Title, Credits, Description,
> and Prerequisites. ...

---

## Example Natural Language Queries

Once connected to an assistant, the model can answer questions like:

- "Which courses are about machine learning?"
- "What do I need to take before CS401, and is there a chain of prerequisites?"
- "Does MATH101 have any prerequisites?"
- "Who teaches Database Systems and what is their email?"
- "Compare CS301 and CS401 side by side."
- "List all courses offered by the Physics department."

The model resolves these by calling the tools above and reading the resources.

---

## Database

SQLite file: `./data/catalog.db`. Schema:

| Table          | Columns                                                                 |
| -------------- | ----------------------------------------------------------------------- |
| `departments`  | `id` (PK), `name`, `code` (UNIQUE)                                      |
| `instructors`  | `id` (PK), `name`, `email`, `office`, `department_id` (FK)              |
| `courses`      | `id` (PK), `course_code` (UNIQUE), `title`, `description`, `credits`, `instructor_id` (FK), `department_id` (FK) |
| `prerequisites`| `course_id` (FK), `prerequisite_id` (FK) — many-to-many mapping         |

Seed data: **3 departments**, **5 instructors**, **10 courses** (8 with prerequisites,
including multi-level chains such as `CS101 → CS102 → CS201 → CS401`).

Re-seeding is automatic and idempotent — the server checks whether the catalog is empty
before seeding, and the standalone script can be run anytime:

```bash
python data/seed_script/seed.py
```

---

## Environment Variables

| Variable      | Default                          | Description                                     |
| ------------- | -------------------------------- | ----------------------------------------------- |
| `DATABASE_URL`| `sqlite:///./data/catalog.db`    | SQLite connection string (path inside container)|
| `HOST`        | `0.0.0.0`                        | Interface the HTTP server binds to.             |
| `PORT`        | `8080`                           | Port the HTTP server listens on.                |
| `SERVER_NAME` | `University Course Catalog MCP Server` | Name advertised during MCP initialize.     |

All variables are documented in [`.env.example`](.env.example).

---

## Verification Checklist

- [x] `search_courses`, `get_prerequisites`, `lookup_instructor`, `get_prerequisite_graph` tools
- [x] `course_descriptions`, `department_directory` resources
- [x] `course_comparison_template` prompt (`{{course_code_1}}`, `{{course_code_2}}`)
- [x] Pydantic-validated inputs/outputs and consistent `{"error": ...}` responses
- [x] Seeded `data/catalog.db` with required schema
- [x] `Dockerfile`, `docker-compose.yml`, `.env.example`, `README.md`