# Polygon LMS > Internal video LMS for Polygon employees. Learners watch courses and playlists; instructors publish content. Agents connect over MCP at `/api/mcp` (Streamable HTTP). Do not treat a playlist as a course. Polygon LMS delivers internal training: instructors upload videos, assemble them into courses (modules + ordered lessons) or flat playlists, and publish. Learners open a course or playlist and watch. Progress is per user + video — the same watch record counts in every container that video belongs to. This file is the agent entry point. Fetch it, then connect to MCP. Human docs live in the repo (`README.md`, `docs/MCP.md`). ## Connect - [MCP endpoint](/api/mcp): Streamable HTTP. `POST /api/mcp`. No browser cookie — use a bearer or MCP OAuth. - [OAuth protected-resource metadata](/.well-known/oauth-protected-resource): RFC 9728. This origin is the authorization server. - [OAuth authorization server](/.well-known/oauth-authorization-server): RFC 8414. Dynamic client registration at `/register`; authorize at `/authorize`; tokens at `/token`. - [Admin API tokens](/admin/tokens): mint a catalog-read bearer (shown once). Prefer this over a shared env key. **OAuth (authoring + learner tools).** Point the client at `{origin}/api/mcp` with no static header. On 401, follow `WWW-Authenticate` → register → `/authorize` (LMS login, then Allow). Tools match the consenting user's roles. ``` claude mcp add --transport http polygon-lms {origin}/api/mcp ``` **Static token (published catalog only).** Admin-minted token or optional `MCP_API_KEY`. Never gets authoring or per-user progress tools. ``` claude mcp add --transport http polygon-lms {origin}/api/mcp \ --header "Authorization: Bearer $MCP_TOKEN" ``` `{origin}` is this site (local default `http://localhost:3000`). Exclude `/api/mcp`, `/register`, `/authorize`, `/token`, and `/.well-known/oauth-*` from any front-door auth that would block discovery. ## Product model - **Video** — library item. Identity is the video id. Watch at `/videos/{id}`. Status: `uploading` | `ready` | `failed`. Progress is `(userId, videoId)`, not per course or playlist. - **Course** — assigned training. Nested `course → modules → videos`. Learner URLs: `/courses`, `/courses/{slug}`. Has draft / published / archived. - **Playlist** — ordered list of existing videos, no modules. Use this for recaps and weekly sessions (e.g. AI learning recordings). Learner URLs: `/playlists`, `/playlist/{slug}`. Same publish lifecycle. A video may belong to many playlists and many course modules at once. - **Roles** — learner (published catalog + own progress), instructor (author), admin (author + users/tokens), manager (team progress). Authoring is instructor/admin only. Do not create a course when the user wants a playlist. Do not invent modules on a playlist. ## MCP tools Registered per caller. Learners never see authoring tools. Static tokens never see learner or authoring tools. **Authenticated (everyone, including static tokens)** - `list_courses` — newest first. Learners/tokens forced to `published`. - `list_modules` — newest first. Learners/tokens: modules on published courses only. - `list_videos` — newest first. Learners/tokens: `ready` videos on a published course or playlist. - `get_course` — nested modules + videos. Unpublished 404 for learners. - `list_playlists` — newest first. Learners/tokens forced to `published`. - `get_playlist` — playlist + videos in order. Unpublished 404 for learners. - `search` — catalog full-text search over titles, descriptions, and video transcripts. Short snippets only; never a full transcript. - `grep_transcript` — search one video's transcript (`videoId` + `pattern`). Literal substring only (regex metacharacters are not special). Returns matching lines with optional context. Does **not** return the full transcript. **OAuth user only** - `whoami` — user id, email, roles, `canAuthor`. - `my_dashboard` — continue-watching, new videos, new courses, new playlists. - `my_in_progress` — started, unfinished videos. - `my_course_progress` — time-weighted percent for one course (`courseId`). **OAuth instructor / admin only** - `create_course` — defaults to `draft`. Attributed to the actor. - `create_module` — inside a course; optional 1-based `position`. - `add_module_to_course` — move/attach; idempotent if already there. - `prepare_video_upload` — mint a 15-minute PUT URL for an `.mp4` on **your** machine. Requires exact `sizeBytes`. Returns `curl` plus `videoId`. Row stays `uploading` until finalize. - `finalize_video_upload` — after the PUT, HEAD storage and mark `ready`. Optional `durationSeconds`. Then attach. - `add_video_to_module` — attach; optional position; idempotent. - `create_playlist` — defaults to `draft`. - `add_video_to_playlist` — attach; optional position; idempotent. - `update_video` — update title and/or description (`videoId`, optional `title`, optional `description`). Omit a field to leave it unchanged; empty/`null` description clears it. Does not change slug. - `set_video_transcript` — replace or clear the full spoken transcript on a video (`videoId`, `transcript`; empty string clears). Cap 500,000 characters. Returns `{ hasTranscript, characterCount }`, not the body. Then use `grep_transcript` to search it. - `set_video_published_at` — set the learner-facing recorded/published date (`videoId`, `publishedAt` as ISO-8601 with timezone). Does not change `created_at` / `updated_at`. Typical course: `create_course` → `create_module` → upload → `add_video_to_module` → `get_course`. Typical playlist: `create_playlist` → upload (or reuse an existing id) → `add_video_to_playlist` → `get_playlist`. Then publish via REST `PATCH` (`status: "published"`) — there is no MCP publish tool. **Upload from your machine:** `prepare_video_upload` (`title`, exact `sizeBytes` from `wc -c`) → run the returned `curl -X PUT` with `--data-binary @file.mp4` and the given headers (size must match) → `finalize_video_upload`. Production Docker has no `ffprobe`; pass `durationSeconds` on finalize or duration stays null. ## HTTP surfaces agents may use Session-cookie REST exists for the web UI. MCP OAuth tokens are not that cookie. If you only have MCP, stay on MCP tools. Learner (session): `/dashboard`, `/courses`, `/courses/{slug}`, `/playlists`, `/playlist/{slug}`, `/videos/{id}`, `/api/search?q=`. Author (session, instructor/admin): `/admin/courses`, `/admin/playlists`, `/admin/videos`, `POST/PATCH /api/courses`, `POST/PATCH /api/playlists`, attach videos under `/api/playlists/{id}/videos` and `/api/modules/{id}/videos`. Health: `GET /api/health`. ## Constraints - Content is internal. Do not scrape learner PII or dump unpublished drafts to a public channel. - Creates are attributed to the OAuth actor. Do not impersonate another user. - Re-attaching a video to the same module or playlist is a no-op, not an error. - Positions are 1-based. Inserts shift existing rows. - Max upload 5 GB; only `.mp4`. `prepare_video_upload` `sizeBytes` must match the file exactly (R2 signs Content-Length). - Prefer published content for learners. Drafts are author-only. ## Optional - [README](/README.md): local setup (may not be served from this origin). - [MCP details](/docs/MCP.md): auth, security, tests (repo path; may not be served from this origin). - [Product](/PRODUCT.md): who it is for and design principles (repo path).