Files
Schulcloud-MCP/docs/LOCAL.md
MechaCat02 bed3923902 Browse the file manager ("Dateien") as a filesystem
Many teachers never use topics or boards; their material sits in the
course's file area, and the tools answered "0 files" for courses holding
dozens of worksheets — 21 of 26 courses on the live account. Persönliche,
Kurs-, Team- and Geteilte Dateien live in the legacy file store, not in
files-storage, and its service is not in the public ingress. The only way in
is the legacy client: HTML listings, and GET /files/signedurl for a
pre-signed download.

core/legacy-files.ts turns that into one path tree — /my, /courses/<course>,
/teams/<team>, /shared — resolving names that contain "/", ids anywhere in a
path, and wrong or ambiguous names with a message saying what is there. A
listing that does not parse throws; it never reads as an empty folder.

Some of the legacy client's GET routes write (GET /files/share/ mints a
share token), so getFileManagerPage allows only the listing routes, by
pattern. Signed URLs are fetched with no credentials and must be https.

- MCP: fs_list, fs_tree, fs_find and fs_read; get_course lists course files.
- CLI: schulcloud fs ls, tree, find and get, recursive and resumable.
- API: /api/fs/list, tree, find and file.
- Index: the crawl walks the file manager (INDEX_FILE_MANAGER, on by
  default), so search covers the text inside those files and sync mirrors
  them under <course>/Kurs-Dateien.

The local instance gains a fixture for all four areas. It needed a loopback,
so signed URLs open from the host, and a pre-created bucket, since MinIO
does not implement PutBucketCors.

135 tests. Smoke 55/55 live; 57/57 and 55/55 on the local instance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:19:16 +02:00

4.5 KiB

Running it locally

Everything runs on your machine: Postgres, the server, the CLI, and Claude Code talking to all of it. Only Schulcloud itself is remote.

Start the stack

cp .env.example .env     # fill in TSC_URL, TSC_JWT_COOKIE, MCP_AUTH_TOKEN
npm install && npm run build
docker compose up -d --build
curl -s http://127.0.0.1:8080/healthz     # {"status":"ok","sessions":0,"index":"on"}

docker-compose.override.yml is merged automatically and is local-only: it publishes the server on 127.0.0.1:8080 and Postgres on 127.0.0.1:55432, and switches crawling to on-demand. Two .env settings adjust it: MCP_HOST_PORT moves the published port when 8080 is taken, and CRAWL_INTERVAL_MS restores a crawl timer — worth doing against a real account, because what_changed can only report what happened between crawls.

After pasting a new TSC_JWT_COOKIE into .env, recreate the container — env_file is read when the container is created, not on restart:

docker compose up -d --force-recreate schulcloud-mcp
``` On the Pi neither port is published — Caddy
reaches the container over the Docker network.

Loopback binding is deliberate. The bearer token is the only thing in front of
your account's data, so it should not be listening on your LAN while you test.

## Populate the index

A full crawl is ~270 requests; start with one course:

```bash
node dist/bin/cli.js login --server http://127.0.0.1:8080 --token "$MCP_AUTH_TOKEN"
node dist/bin/cli.js refresh --course <courseId>    # or omit --course for everything
node dist/bin/cli.js status

Get a course id from curl -s -H "Authorization: Bearer $TSC_JWT_COOKIE" \ "$TSC_URL/api/v3/courses?limit=5" | jq -r '.data[] | "\(.id) \(.title)"'.

Connect Claude Code

Claude Code supports MCP over stdio, SSE and HTTP. The VS Code extension runs the same Claude Code underneath, so it reads the same configuration — register once and both work.

Use --scope user. Claude Code's default scope is local, which is per-project: the server is stored under that directory in ~/.claude.json and claude mcp list will not show it from anywhere else. Since the point is to ask about your coursework from wherever you happen to be, user scope is what you want. (project scope would write a .mcp.json into the repo — wrong here, because the HTTP config carries a token.)

HTTP — matches the deployed setup:

claude mcp add --scope user --transport http schulcloud http://127.0.0.1:8080/mcp \
  --header "Authorization: Bearer $MCP_AUTH_TOKEN"

stdio — no server or Docker needed:

claude mcp add --scope user schulcloud-stdio \
  -e DATABASE_URL=postgresql://schulcloud:schulcloud@127.0.0.1:55432/schulcloud \
  -- node --env-file=/absolute/path/to/.env /absolute/path/to/dist/bin/stdio.js

--env-file keeps the JWT in .env rather than copying it into the MCP config. The HTTP form has no equivalent — its header holds the token — so that token lives in ~/.claude.json.

Check it:

claude mcp list       # schulcloud: http://127.0.0.1:8080/mcp (HTTP) - ✔ Connected
claude mcp get schulcloud

If claude mcp list does not show it, the usual cause is scope: run claude mcp list from the directory you added it in, and if it appears there, re-add it with --scope user.

Then just ask: "which courses am I in?", "what's due this week?", "find the material about Verschlüsselung". Inside a session, /mcp lists the servers and their tools.

Test the CLI

node dist/bin/cli.js ls --long
node dist/bin/cli.js sync --dry-run        # shows what it would mirror
node dist/bin/cli.js sync

npm link puts it on your PATH as schulcloud.

Run the test suites

npm test              # 135 offline tests
npm run smoke         # end-to-end against the live instance, live-only mode
DATABASE_URL=… npm run smoke    # end-to-end with the index (57 checks)

Store tests need a database and skip without one:

docker exec schulcloud-mcp-db psql -U schulcloud -d schulcloud \
  -c "CREATE DATABASE schulcloud_test OWNER schulcloud"
TEST_DATABASE_URL=postgresql://schulcloud:schulcloud@127.0.0.1:55432/schulcloud_test npm test

These tests TRUNCATE. They refuse to run unless the database name contains "test", because aiming them at the dev database once was enough — the fixtures ended up in real data.

Tearing down

docker compose down          # keep the index and mirror
docker compose down -v       # discard them too
claude mcp remove schulcloud