Skip to main content
Glama
Jayb1u3

MyAndrews MCP

by Jayb1u3
README.md
# MyAndrews MCP

Local, read-only MCP access to Andrews University's Ellucian Experience portal (`MyAndrews`).

## Tools

- `connect`, `status`
- `academic_profile`
- `classes`, `class_schedule`
- `degree_progress`
- `advisors`
- `account_balances`
- `tasks`, `notifications`
- `portal_cards`
- `dashboard` — generates an optional interactive Hermes widget; finances and advisors are opt-in

Authentication opens Andrews SAML/Duo in a Playwright browser. The resulting browser state is stored locally as `auth-state.json` with mode `0600`. Passwords and Duo codes are never accepted by the MCP.

Tenant requests (`/api/user`, tasks, notifications, Degree Works, …) run as same-origin `fetch` calls inside a headless Chromium page seeded from that saved state (Playwright consumes it as `storage_state`; cookie values are never replayed over plain HTTP). The session is validated by loading the tenant root; any redirect away from it yields `session expired — reconnect required (needs a visible browser / possibly Duo)`. Nested tool calls (for example `dashboard`) share one browser.

`academic_profile` and `account_balances` use a split transport because the browser blocks their cross-origin fetch to `integrate.elluciancloud.com` (CORS): the browser session first mints the short-lived OAuth bearer token from the tenant `/api/oauth` route, then a direct `httpx` client calls the exact-host-allow-listed HTTPS URL with only `Authorization: Bearer`, `Accept`, `Origin`, and `Referer` headers. No cookies or saved state are ever handed to `httpx` (`test_no_raw_cookie_replay` enforces this). A 401/403 from the bearer API is reported as a rejected token with the reconnect message; other failures are bounded and never include the token.

Outputs carry provenance: `status` reports `transport`, `saved_state`, and `checked_at`; `degree_progress` reports `source`, `fetched_at`, `card_id`, `audit_query`, `choice_count`, `audit_populated`, and per-route `errors`; `classes`/`class_schedule` add `_provenance` distinguishing a live refresh from the cached localStorage snapshot. Degree Works uses the origin-level `/api/degree-works/degrees/…` and `/api/degree-works/audit/…` routes with the `all-accounts|Ellucian|DegreeWorks|DegreeWorksAuditCard` identifier.

The live degrees payload is `{degrees: [{degree: {description, key}, level: {description, key}, school: {description, key}, …}], reportFormat, resDashUrl}`. The audit query is `?degree=<degree.key>&level=<school.key>`, raw strings percent-encoded with trailing spaces preserved. **Quirk (recon-proven 2026-09-03 by live browser comparison):** despite its name, the audit API's `level` parameter is filled by the SPA from `record.school.key` (12-char padded), not `record.level.key` (6 chars). Sending `level.key` returns an HTTP 200 empty list; sending `school.key` returns the populated audit. `degree_choices()` therefore prefers `school.key`, then falls back to nested `level.key`, then flat `level`/`levelCode` for legacy shapes. An HTTP 200 audit that is structurally empty (`[]`/`{}`) is never reported as success: it is recorded in `errors.audit`, and `degree_progress` raises when neither a recognised choice nor a populated audit is available. `dashboard` then falls back to the cached audit snapshot and reports `degree_error`.

The server is read-only against Ellucian. Class tools can refresh the portal's own cache; they do not change registration.

## Run

```bash
uv venv .venv
uv pip install --python .venv/bin/python -r requirements.txt
.venv/bin/playwright install chromium
.venv/bin/python server.py
```

## Test

```bash
.venv/bin/python -m py_compile server.py test_server.py
.venv/bin/python test_server.py          # offline, deterministic, no credentials
.venv/bin/fastmcp inspect server.py:mcp
hermes mcp test ellucian                 # expects 12 tools
```

`test_server.py` uses fakes for Playwright and never reads `auth-state.json`. A privacy-safe live smoke prints only connection state and shapes:

```bash
.venv/bin/python .backups/smoke_live.py   # prints booleans/types/key names/errors only
```

Exit codes of the private recon script (`~/.hermes/private/ellucian-recon/recon_browser.py`): 0 verified, 2 session expired, 3 Degree Works unreachable, 4 reachable but unverified (no choice derived or empty audit).

Maintenance

ActivityMaintained
ResponsivenessNo issues