Initial schulcloud-mcp server
Read-only MCP server exposing a Schulcloud account to Claude: courses,
column boards, lessons, tasks, and file downloads with text extraction.
The API surface was verified against the live instance rather than
inferred from upstream source, which changed several design decisions:
- The `jwt` cookie works verbatim as `Authorization: Bearer` and lasts 30
days, so there is no cookie jar and no refresh-session timer.
- Course contents live at /api/v3/course-rooms/{courseId}/board; there is
no GET /api/v3/courses/{id}.
- Files are a separate service (/api/v3/file/*) with its own OpenAPI doc.
- Board file elements carry no file id; attachments are resolved by
listing files-storage with parentType=boardnodes and the element id.
Read-only by construction: every client method is a GET, including the
api_get escape hatch. The endpoint is internet-facing by necessity, so a
leaked token being unable to act as the user is the key safety property.
Deploys as a container behind the Pi's existing Caddy, guarded by a
constant-time bearer check. Stateless — no database.
Verified: 28 unit tests, plus a 30-check end-to-end run driving a real
MCP client over Streamable HTTP against the live account.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
123
README.md
Normal file
123
README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# schulcloud-mcp
|
||||
|
||||
An MCP server that gives Claude read-only access to a
|
||||
[Schulcloud](https://github.com/hpi-schul-cloud) account — courses, boards,
|
||||
lessons, tasks — and reads the attached files, so you can ask about your
|
||||
coursework instead of downloading PDFs and uploading them by hand.
|
||||
|
||||
Built and verified against `schulcloud-thueringen.de` with a live student
|
||||
account. Everything in `docs/API.md` was confirmed against the running
|
||||
instance, not inferred from the upstream source.
|
||||
|
||||
## What Claude can do with it
|
||||
|
||||
> *"What do I have due this week?"*
|
||||
> *"Find the material about Verschlüsselung and explain the Caesar cipher worksheet."*
|
||||
> *"Summarise the routing lesson from the LF10 course."*
|
||||
|
||||
Thirteen tools, all read-only:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `whoami` | account, school, roles — also a connectivity check |
|
||||
| `list_courses` | all courses, with ids |
|
||||
| `get_dashboard` | the tiles as pinned on the web dashboard |
|
||||
| `get_course` | one course's boards, topics and tasks |
|
||||
| `get_board` | a column board in full: columns, cards, text, links, files |
|
||||
| `get_lesson` | a topic's text sections, materials, files and tasks |
|
||||
| `list_tasks` | homework across all courses, by due date |
|
||||
| `get_task` | one task: description, due date, status, attachments |
|
||||
| `list_files` | files attached to any entity |
|
||||
| `download_file` | fetch a file and extract its text, or view an image |
|
||||
| `search` | keyword search across courses, boards, files and tasks |
|
||||
| `list_news` | school and course announcements |
|
||||
| `api_get` | GET-only escape hatch for uncovered API surface |
|
||||
|
||||
`download_file` extracts text from **PDF, DOCX, XLSX, PPTX and OpenDocument**
|
||||
files and returns **images inline** for Claude to look at. Verified against
|
||||
real files in the account: a 93-file PDF corpus, DOCX, ODT and PPTX all
|
||||
extract correctly.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cp .env.example .env # fill in TSC_URL and TSC_JWT_COOKIE
|
||||
npm install
|
||||
npm run build
|
||||
npm run probe # verifies the token and API against the live instance
|
||||
```
|
||||
|
||||
Then either deploy it as a remote connector, or point Claude Code at
|
||||
`dist/bin/stdio.js`. Both paths are in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
|
||||
|
||||
Getting `TSC_JWT_COOKIE` takes four clicks in DevTools and lasts 30 days —
|
||||
see [docs/AUTH.md](docs/AUTH.md).
|
||||
|
||||
## Design decisions
|
||||
|
||||
**Bearer token, not a cookie jar.** The instance's `jwt` cookie works verbatim
|
||||
as `Authorization: Bearer`, and is valid for 30 days. There is no session to
|
||||
keep alive and no `refresh-session` timer — a simplification that only became
|
||||
apparent by testing against the live instance.
|
||||
|
||||
**Read-only by construction.** Every method on the API client is a `GET`,
|
||||
including `api_get`. The endpoint is internet-facing by necessity (Claude's
|
||||
connectors call it from Anthropic's cloud), so the fact that a leaked token
|
||||
cannot be used to *act* as the user is the main safety property. Adding one
|
||||
write tool would forfeit it.
|
||||
|
||||
**Stateless.** No database, despite one being available on the host. 26 courses
|
||||
is not a caching problem, and a cache would introduce staleness questions that
|
||||
live calls simply do not have.
|
||||
|
||||
**Assembled, not raw.** `get_board` makes three kinds of upstream call and
|
||||
stitches the results — board skeleton, card bodies, and a files-storage lookup
|
||||
per file element — because a model asking "what's on this board" wants the
|
||||
answer, not a traversal plan. Output is Markdown with ids preserved for
|
||||
follow-up calls, not raw JSON.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
bin/ stdio and http entry points
|
||||
schulcloud/ API client, response types, board assembly
|
||||
tools/ one module per group of MCP tools
|
||||
http/ express app, bearer auth
|
||||
extract.ts document → text
|
||||
render.ts formatting helpers
|
||||
docs/ API findings, auth, deployment
|
||||
deploy/ Caddyfile snippet
|
||||
scripts/ probe (verify against live) and smoke (end-to-end)
|
||||
vendor/ upstream clones, git-ignored, for reference only
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run dev # watch mode, runs src/ directly
|
||||
npm test # unit tests, no network
|
||||
npm run probe # check assumptions against the live instance
|
||||
npm run smoke # full end-to-end: real server, real client, real data
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
`npm run smoke` starts the HTTP server, connects a real MCP client over
|
||||
Streamable HTTP and exercises every tool against the live account — 30 checks
|
||||
covering the auth gate, the protocol handshake, every content chain, file
|
||||
extraction, `api_get`'s guard rails and error handling.
|
||||
|
||||
## Upstream
|
||||
|
||||
Reference clones live in `vendor/` (git-ignored):
|
||||
|
||||
```bash
|
||||
git clone --depth 1 --filter=blob:none https://github.com/hpi-schul-cloud/schulcloud-server.git vendor/schulcloud-server
|
||||
git clone --depth 1 --filter=blob:none https://github.com/hpi-schul-cloud/file-storage.git vendor/file-storage
|
||||
git clone --depth 1 --filter=blob:none https://github.com/hpi-schul-cloud/nuxt-client.git vendor/nuxt-client
|
||||
```
|
||||
|
||||
Most of that organisation's ~100 repositories are archived or superseded; those
|
||||
three are the live ones that matter. The instance's own OpenAPI documents
|
||||
(`/api/v3/docs-json`, `/api/v3/file/docs-json`) are more authoritative than any
|
||||
of them — see [docs/API.md](docs/API.md).
|
||||
Reference in New Issue
Block a user