# 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 ```bash 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. 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 # 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. **HTTP — matches the deployed setup:** ```bash claude mcp add --transport http schulcloud http://127.0.0.1:8080/mcp \ --header "Authorization: Bearer $MCP_AUTH_TOKEN" ``` **stdio — no server or Docker needed:** ```bash claude mcp add 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 such option — its header holds the token — so leave it at the default `local` scope, which writes to `~/.claude.json` rather than a `.mcp.json` that would be committed. Check it: ```bash claude mcp list # schulcloud: http://127.0.0.1:8080/mcp (HTTP) - ✔ Connected claude mcp get schulcloud ``` 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 ```bash 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 ```bash npm test # 58 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 (34 checks) ``` Store tests need a database and skip without one: ```bash 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 ```bash docker compose down # keep the index and mirror docker compose down -v # discard them too claude mcp remove schulcloud ```