Skip to main content
Glama
README.md
# UNC LibCal MCP

MCP server for booking [UNC Davis Library](https://calendar.lib.unc.edu) study spaces from Claude, Cursor, Codex, or any MCP client.

Built for UNC students, faculty, and staff with a valid **Onyen**. Not affiliated with or endorsed by UNC Libraries.

## What it does

Ask your agent:

> Book me a Davis cube tomorrow at 2pm

The server will:

1. Check LibCal availability (public API — no login needed)
2. Suggest ranked options (`libcal_suggest`) or book a slot you confirm (`libcal_book`)
3. Complete the reservation in your browser session (Playwright + saved Onyen login)

## Requirements

- **Node.js 20+**
- **UNC Onyen** (for booking; availability checks work without login)
- **Chromium** (installed automatically via Playwright)

## Quick start

### Already on this machine?

If you already have the repo and `~/.unc-libcal/`:

```bash
cd ~/Projects/unc-libcal-mcp   # or wherever you cloned it
npm run build
npm test
```

- **`~/Projects/unc-libcal-mcp`** — the project (clone from GitHub)
- **`~/.unc-libcal/`** — your private session + config (never commit this)
  - `storage-state.json` — saved login cookies from `npm run login`

Skip to [step 2](#2-log-in-to-libcal) if you've logged in before, or re-run login if booking fails.

### 1. Clone and install

```bash
git clone https://github.com/Thespaceblade/unc-libcal-mcp.git
cd unc-libcal-mcp
npm install
npx playwright install chromium
npm run build
npm test
```

### 2. Log in to LibCal

```bash
npm run login
```

A browser opens to the **Davis cubes page** (this is normal — the calendar is public and does not ask for Onyen immediately).

The script auto-clicks a test slot and opens **UNC Onyen**. Sign in (+ Duo if prompted).

You may land on **Booking Details** with a held test slot — that is normal. **Do not click "Submit my Booking".**

Press **Enter** in the terminal. The script clears the held slot and returns you to the Davis cubes calendar where **Logout** should appear.

Your session is saved to `~/.unc-libcal/storage-state.json` (never commit this file).

### 3. Configure (optional)

On first run, `~/.unc-libcal/config.json` is created with defaults:

```json
{
  "defaultCategory": "davis-cubes",
  "preferSameDay": true,
  "minLeadMinutes": 30,
  "searchHorizonDays": 7,
  "bookingPurpose": "Study session"
}
```

### 4. Connect your MCP client

Every client needs the **absolute path** to `dist/index.js`. From inside the repo:

```bash
pwd   # e.g. /Users/you/projects/unc-libcal-mcp
# Use: <that-path>/dist/index.js
```

Or one-liner:

```bash
node -e "const p=require('path'); console.log(p.join(process.cwd(),'dist/index.js'))"
```

All clients below run the same stdio server:

```json
{
  "command": "node",
  "args": ["/absolute/path/to/unc-libcal-mcp/dist/index.js"]
}
```

Restart the client after editing config.

#### Claude Desktop

File: `~/Library/Application Support/Claude/claude_desktop_config.json`

If the file is **new or empty**, paste:

```json
{
  "mcpServers": {
    "unc-libcal": {
      "command": "node",
      "args": ["/absolute/path/to/unc-libcal-mcp/dist/index.js"]
    }
  }
}
```

If the file **already has other keys** (e.g. `preferences`, `coworkUserFilesPath`), add only the `unc-libcal` block inside the existing `mcpServers` object — do not replace the whole file.

Fully quit and reopen Claude Desktop (Cmd+Q, not just closing the window).

#### Cursor

**Option A — UI:** Settings → **MCP** → Add server → paste the JSON block above.

**Option B — project file:** `.cursor/mcp.json` in this repo (good for sharing with teammates):

```json
{
  "mcpServers": {
    "unc-libcal": {
      "command": "node",
      "args": ["./dist/index.js"]
    }
  }
}
```

Use `./dist/index.js` only if Cursor's MCP cwd is the project root; otherwise use the absolute path.

#### OpenAI Codex (CLI)

Codex uses **TOML**, not JSON. File: `~/.codex/config.toml` (or `.codex/config.toml` in a trusted project).

```toml
[mcp_servers.unc-libcal]
command = "node"
args = ["/absolute/path/to/unc-libcal-mcp/dist/index.js"]
```

Or via CLI:

```bash
codex mcp add unc-libcal -- node /absolute/path/to/unc-libcal-mcp/dist/index.js
codex mcp list   # verify it appears
```

If servers don't show up, confirm the project is **trusted** (`codex trust` in the repo) when using a project-local `.codex/config.toml`.

#### Claude Code (CLI)

File: `~/.claude.json` (global) or `.mcp.json` in the project:

```json
{
  "mcpServers": {
    "unc-libcal": {
      "command": "node",
      "args": ["/absolute/path/to/unc-libcal-mcp/dist/index.js"]
    }
  }
}
```

In a Claude Code session, run `/mcp` to confirm tools are loaded.

#### Other MCP clients

Any client that supports **stdio MCP** can use the same `command` + `args`. Point it at `dist/index.js` after `npm run build`.

### 5. Verify it works

In your agent, try:

```
Check libcal auth status
```

Then:

```
Suggest Davis cubes for 2 hours tomorrow
```

You should see `libcal_auth_status`, `libcal_suggest`, `libcal_check_availability`, and `libcal_book` available once the server is connected.

## Troubleshooting

| Problem | Fix |
|---|---|
| `npm run login` opens cubes page, no Onyen prompt | Wait — the script auto-clicks a slot and Submit Times to reach Onyen |
| `libcal_auth_status` says not logged in | Run `npm run login` (see above) |
| Session expires quickly | Normal for UNC SSO — re-run `npm run login` when booking fails |
| Login saved but booking redirects to SSO | Re-run `npm run login` — press Enter only after you see Logout on LibCal |
| `libcal_book` fails after suggest | Slot was taken — run `libcal_suggest` again |
| MCP tools don't appear in Claude Desktop | Fully quit (Cmd+Q) and reopen; confirm `mcpServers` path points to `dist/index.js` |

Sessions expire periodically (LibCal auth ~24h; UNC SSO sooner with inactivity). Run `npm run login` again when `libcal_auth_status` reports expired.

## MCP tools

| Tool | Login required | Purpose |
|---|---|---|
| `libcal_suggest` | No | Ranked booking options; same-day priority unless you specify a date |
| `libcal_check_availability` | No | Open slots on one date |
| `libcal_book` | Yes | Book a confirmed slot |
| `libcal_auth_status` | Yes | Check if saved session is still valid |

### Booking workflow

1. For open-ended requests (“book a cube”, “max hours”) → agent calls **`libcal_suggest`** first
2. Agent shows numbered options; you pick one
3. Agent calls **`libcal_book`** with `user_confirmed: true` and the chosen date/time

`libcal_book` will not run without explicit confirmation.

## Space categories

| ID | Description |
|---|---|
| `davis-cubes` | Davis Collaboration Cubes (default) |
| `davis-study-rooms` | Davis group study rooms |
| `davis-computers` | Data Services lab computers |

## Example prompts

```
Book me a study room for as long as possible
→ libcal_suggest shows TODAY vs later; you pick; then libcal_book

Book me a Davis cube tomorrow 11am–1pm
→ libcal_suggest or direct libcal_book with your exact time

Any study rooms free Friday afternoon?
→ libcal_check_availability or libcal_suggest
```

## How it works

- **Availability** — reverse-engineered LibCal grid API (`/spaces/availability/grid`)
- **Booking** — Playwright drives the real LibCal UI: select slot → submit times → `/spaces/auth` checkout → confirm form

## CLI scripts

```bash
# Refresh Onyen session
npm run login

# Run unit tests (46 tests)
npm test

# Book via CLI (uses saved session)
node dist/scripts/run-task.js --date 2026-09-01 --start 14:00 --duration 120
```

## Cancelling bookings

This MCP server **cannot cancel** reservations. LibCal does not expose cancel links in the web UI or any API we can call.

To cancel, use the link in your **confirmation email** from `alerts@mail.libcal.com`. Search your inbox for that sender if you need an old booking.

## LibCal limits (UNC Davis)

- Up to **3 hours per day**, in **30-minute / 1-hour segments**
- Popular slots can be taken between suggest and book
- Cubes may require a “course or group name” on the checkout form (defaults to `bookingPurpose` in config)

## Development

```bash
npm run build    # compile TypeScript → dist/
npm test         # unit + integration tests
npm run dev      # build and start MCP server on stdio
```

See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup details, project layout, PR expectations, and how to report bugs.

## Contributing

Issues and PRs are welcome. For anything beyond a small docs/test fix, open an issue first so we can agree on the approach.

1. Fork (or branch) → `npm install` → `npx playwright install chromium` → `npm test`
2. Keep changes focused; add tests for behavior changes
3. Never commit `~/.unc-libcal/` session files or credentials

## Caveats

- Personal automation tool — use responsibly and follow UNC Library policies
- Only tested against `calendar.lib.unc.edu` (UNC Chapel Hill)
- Other LibCal institutions would need different `lid`/`gid` constants in `src/libcal/constants.ts`

## License

MIT — see [LICENSE](./LICENSE).

TDQS

A4.1/5.0

Scored across 5 tools

Disambiguation4/5

The core booking tools are clearly separated: check_availability is for a single date, suggest is for ranked multi-day searching, and book handles the actual reservation. Support tools like auth_status and list_calendars are distinct, though check_availability and suggest could still cause minor confusion without their descriptions.

Naming Consistency3/5

All tools share the libcal_ prefix and snake_case, but the patterns are mixed: some are verb_noun (check_availability, list_calendars), some are bare verbs (book, suggest), and one is noun_noun (auth_status). This is readable but not fully consistent.

Tool Count5/5

Five tools is a well-scoped set for a library booking server. Each tool serves a distinct role: availability checking, ranked suggestions, booking, authentication status, and calendar configuration.

Completeness2/5

The server covers finding and booking slots but lacks obvious lifecycle operations like listing existing bookings or cancelling a booking. Users or agents needing to modify or cancel a reservation would hit a dead end, which is a significant gap for a booking-focused server.

Maintenance

ActivityMaintained
ResponsivenessUnresponsive