` where the client accepts only a URL. Any token in a URL can land in browser history, connector settings, and proxy logs. `/health` stays open. Unset means no authentication, which is only safe on loopback. |
| `ANTEATER_ALLOWED_ORIGINS` | HTTP mode only. Comma-separated extra origins to allow. |
| `ANTEATER_TRUSTED_PROXIES` | HTTP mode only. Which direct peers may set `X-Forwarded-*`. Accepts CIDRs, bare addresses, and the shorthands `private` and `loopback`. **Unset means the headers are ignored**, because they are client-supplied and would otherwise let anyone forge their address in your log. Behind a reverse proxy set this, or every request looks like it came from the proxy. `private` covers the Docker bridge. |
| `HOST` / `PORT` | HTTP mode only; equivalent to `--host` / `--port`. |
### Command line
```bash
node anteater-mcp.mjs # stdio (what MCP clients use)
node anteater-mcp.mjs --http # HTTP on 127.0.0.1:8787, endpoint /mcp
node anteater-mcp.mjs --http --port 9000 # different port
node anteater-mcp.mjs --http --host 0.0.0.0 # bind all interfaces (warns; see below)
node anteater-mcp.mjs --list-tools # list every tool
```
### HTTP mode security
- **Binds `127.0.0.1` by default.** `server.listen(port)` with no host binds every
interface, which would expose an unauthenticated server to the whole LAN. `--host`
is required to change that, and it warns when you do.
- **Validates `Origin`.** The MCP specification requires this of local HTTP servers:
without it, any page you visit can POST to your port and drive every tool (DNS
rebinding / CSRF). Requests with no `Origin` — native MCP clients — are allowed;
requests with one must be localhost or listed in `ANTEATER_ALLOWED_ORIGINS`.
- **`Access-Control-Allow-Origin` echoes the single validated origin**, never `*`.
- **Unauthenticated unless `ANTEATER_MCP_TOKEN` is set.** That is fine on loopback and
not fine anywhere else; the server warns at startup if it is bound off-loopback without
one. See [DEPLOY.md](DEPLOY.md).
- **HTTP activity is logged to stderr as one JSON request line and one response line.**
Logs include a request ID, method, sanitized target, remote address, status, duration,
RPC/tool name, and outcome when available. Authorization values and RPC arguments are
never logged; URL token values are shown as `[REDACTED]`.
- `/health` reports status and the source URL; `/source` redirects to this repository,
which helps anyone deploying a modified copy comply with AGPL section 13.
---
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Tools don't appear in Claude Desktop | The path must be **absolute**, and you must fully quit and reopen the app. Check the config parses: `node -e "require('./claude_desktop_config.json')"`. |
| `Anteater API rate limit hit` | The anonymous quota is shared and replenishes hourly. Set `ANTEATER_API_KEY`. |
| `search_courses` returns odd results | Fuzzy search needs a privileged key that ordinary keys are not granted; it falls back to substring matching and says so. Use the structured filters instead. |
| `Too broad` from `search_sections` | A whole term is tens of thousands of sections. Add `department`, `courseNumber`, `ge`, `instructor` or `sectionCodes`. |
| `"2026 summer" is ambiguous` | UCI has three summer terms. Use `Summer1`, `Summer2` or `Summer10wk`. |
| `Unknown department "..."` | Use `list_departments`, or read `anteater://reference/departments`. |
| A course has no grade data | Recent quarters lag, and P/NP-only courses have none. |
| Seats look stale | Live figures are cached for 5 minutes; the catalogue for 24 hours. |
| Schedule fallback appears | Anteater API was unavailable, so the schedule came from Zotcourse's WebSoc-backed endpoint. Verify seats and section pairing in WebReg. Set `ZOTCOURSE_API_BASE=off` if you do not want this network fallback. |
---
## Development
```bash
npm test # 20 conformance tests; makes no API calls
npm run test:live # live calls; needs a key in practice
npm ci # build tooling only; the shipped server has no runtime packages
npm run build:sea # standalone binary; requires the exact Node in .node-version
```
[UPSTREAM.md](UPSTREAM.md) records the exact Anteater API version and the upstream commits
this server was validated against, endpoint by endpoint — start there when the API changes
and something begins returning wrong or empty results.
[VERIFICATION.md](VERIFICATION.md) is a full release checklist — per-regression pass
criteria, security checks and integration checks — written so someone who has never read
the code can run it.
CI uses the same pinned Node 24 LTS release as Docker and native packaging. It runs the
offline suite, syntax and OpenAPI checks, a standalone-executable smoke test, and a
locked-down container protocol test. **It deliberately makes no live Anteater API calls**,
so it never draws on the public rate limit.
Pushing a tag that exactly matches `v` plus the `package.json` version builds six native
executables, generates checksums and provenance attestations, pushes an amd64/arm64 image
to GHCR, and publishes the GitHub Release. Stable tags update `:latest`; every release
updates `:beta`; exact `:vX.Y.Z` tags are immutable.
After the first image publish, set the GHCR package visibility to **Public** once. The
workflow uses the scoped `GITHUB_TOKEN`; if repository policy blocks bot-created Releases,
add a fine-grained `RELEASE_PAT` secret with Contents write access, matching the upstream
project's fallback.
### Scope
Anteater API also serves dining halls, library traffic and study-room bookings. Those are
deliberately not wrapped — this server stays focused on choosing and registering for
classes.
LARC tutoring sections are in scope but effectively dead upstream: `/v2/rest/larc` returns
22 courses for 2024 Fall and nothing for any term since, so a tool would always answer
"none" for the term a student is actually planning.
---
## Known limitations
These are properties of the upstream data, not bugs. The server states them rather than
papering over them, because the alternative is confident wrong advice.
- **WebSoc does not publish which discussion belongs to which lecture.** Some courses
encode it in the section number (`Lec A` → `Dis A1`), others number companions
independently (`I&C SCI 31`: lectures A/B, labs 1–9). `check_schedule` therefore tells
you when a required component is *missing entirely*, and when a pairing *cannot be
verified* — but it cannot confirm that a given lab goes with a given lecture. Confirm
that on WebReg. Neither of ICSSC's own clients infers this either: AntAlmanac and
PeterPortal both render sections as a flat list and leave the pairing to the student,
which is good evidence the data simply is not there.
- **Enrollment restrictions are not evaluated against you.** `search_sections` and
`recommend_courses` surface the codes and their meanings; whether you satisfy
"Major only" or "Graduate only" is enforced by the registrar.
- **Transfer and community-college coursework is absent.** `check_prerequisites` reports
any completed course it does not recognise instead of silently ignoring it, but only an
advisor can clear transfer credit.
- **No registration-window dates.** The academic calendar covers instruction and finals;
it does not say when your enrollment window opens. Check StudentAccess.
- **Grade data lags.** Recent quarters may be missing, and a recent term's grades are
often filed under `STAFF`. A small sample size makes an average GPA unreliable — the
tools always print `n` so you can judge.
- **Historical GPA is course-wide.** `recommend_courses` ranks by the average across all
past instructors, which may not be whoever is teaching this term. Use `get_course_grades`
to check the specific instructor before deciding.
## Bugs found and fixed before release
A six-dimension parallel review (logic, MCP conformance, live-API contract, security,
robustness, repo readiness) produced 41 raw findings, 38 after deduplication. The ones
that would actually have misled someone:
| Bug | Consequence |
|---|---|
| **A bare `null` JSON-RPC message killed the process** | Both transports. Over HTTP that was an unauthenticated 4-byte denial of service. |
| **Every final exam date was one month early** | WebSoc's `finalExam.month` is **0-indexed** (11 = December); the code treated it as 1-indexed. "Tue Nov 8" was really December 8 — someone books a flight on the wrong date. |
| **7 of 19 restriction codes were wrong** | `K` was labelled "Cross-listed" but means **Graduate only**, and appears 608 times in a single term. `X` was "Separate final exam" but means authorization codes are needed even to drop. |
| **`"2026 Summer 1"` silently returned Spring** | The bare `s` alias swallowed `summer`. Three of six quarters were unreachable by natural phrasing, and it returned a confident, entirely wrong schedule. |
| **Standalone lab courses counted as 0 units** | `check_schedule` excluded `Lab` sections, so CHEM 1LD (3 units, no lecture) vanished — a student could misjudge the 12-unit full-time threshold that gates financial aid and F-1 status. |
| **`recommend_courses` forced `sectionType: Lec`** | Every seminar-only GE was invisible; GE-1A returned nothing at all. |
| **`get_program_requirements` with `ugrad` always failed** | The endpoint has a required `id` parameter the tool never sent. |
| **HTTP mode bound `0.0.0.0` without validating `Origin`** | While logging "listening on localhost". |
| **Fall sorted as the earliest term of its year** | Reversed the chronology in `get_enrollment_history` and `get_course_grades`. |
A second round, driving the server through six realistic student scenarios end to end,
found 54 more — 10 of them blockers. The worst:
| Bug | Consequence |
|---|---|
| **`check_schedule` gave a clean all-clear to unenrollable schedules** | It checked only times. A lecture with no required lab, or a full or cancelled section, passed silently — the student would be rejected at WebReg. |
| **`days` meant "meets on at least one of"** | A student who could only attend Tu/Th was shown three- and four-day courses, and courses whose mandatory labs were all MWF. |
| **A bare instructor surname matched nothing** | `get_course_grades` blamed the course — "may be new or graded P/NP only" — for a professor with 1,642 grades on record. |
| **Degree requirements defaulted to the 2023–2024 catalogue** | Three years stale, with no indication, for a student on 2026–2027. |
| **`check_prerequisites` silently dropped unrecognised courses** | Then printed a confident "NOT satisfied" for work the student had actually done. |
| **`recommend_courses` hid restriction codes** | Its highest-ranked GE picks were courses the student could not enrol in. |
| **`check_schedule` threw a raw `TypeError`** | When given the comma-separated string that `search_sections` documents for the same parameter name. |
Also fixed: inverted check marks in `NOT` prerequisite subtrees, course numbers lost for
the 48 department codes containing a space, multi-byte UTF-8 corrupted across HTTP chunk
boundaries, a quadratic-backtracking regex on unvalidated input, and `socAvailable`
mislabelled as "enrollment opens" when it is the schedule publication date.
## API quirks worth knowing
Undocumented upstream; all handled here, and listed in case they save you the debugging.
- **`days` must be comma-separated.** `Tu,Th` works, `TuTh` is rejected. It matches
**at least one** of the listed days, not all of them.
- **`enrollmentHistory` returns parallel arrays** (`dates[]`, `totalEnrolledHistory[]`,
`requestedHistory[]`, …), not scalars. `-1` means "not tracked".
- **`finalExam.month` is 0-indexed.**
- **Course comments are raw HTML fragments**, complete with `` and `"`.
- **`startTime` / `endTime` mean "starts at or after" and "ends at or before"**, not
interval overlap.
- **`/v2/rest/search` needs a *privileged* API key.** An ordinary key is refused with
`not permitted to access this resource`, which is a different error from the
`key is required` you get with no key at all — worth distinguishing, since telling
someone who already has a key to get a key is a dead end.
- **`/v2/rest/websoc/syllabi` takes `courseId`**, not `department` + `courseNumber`.
- **`/v2/rest/courseMaterials` needs `department` AND `courseNumber` together.** Either
alone is refused, and it collapses the three summer sessions into a single `Summer`.
- **Restriction strings contain prose.** `"A and N"` must be split on whitespace *and*
have the literal `and`/`or` discarded, or the conjunction renders as a code.
- **Section status is an enum, not free text**: `OPEN`, `Waitl`, `FULL`, `NewOnly`, or
empty. Only `OPEN` means a continuing student can enrol now — `NewOnly` marks seats
held for incoming students. `numNewOnlyReserved` counts seats inside the capacity that
are reserved the same way, so the apparent opening overstates the real one.
---
## Distribution
Published to the [MCP Registry](https://registry.modelcontextprotocol.io) as
`io.github.KKazuhaK/anteater-mcp`, listing the container image only. The registry entry
describes how to run your own instance; it does not point at anyone's deployment, since a
hosted instance is gated behind its own token and its upstream quota belongs to whoever
runs it.
Each release also publishes the GHCR image and standalone binaries for six platforms.
## Licence and attribution
Data from **[Anteater API](https://icssc.link/about-anteaterapi)**, maintained by ICSSC
Projects.
**This is not an official UCI tool.** Verify on WebReg or the General Catalogue before
registering. Use is subject to Anteater API's
[attribution policy](https://docs.icssc.club/docs/developer/anteaterapi/attribution-policy).
Licensed **[AGPL-3.0-or-later](LICENSE)**, matching the upstream Anteater API server so
code can move freely in either direction if this is ever contributed upstream. This is an
independent HTTP client and contains no upstream source code. If you modify it and run it
as a network service, AGPL section 13 requires you to offer users your Corresponding
Source — see [NOTICE](NOTICE).
For academic use:
```bibtex
@misc{anteater-api,
author = {ICS Student Council},
title = {Anteater API},
year = {2024},
howpublished = {\url{https://github.com/icssc/anteater-api}},
}
```