claude.ai's connector dialog takes a name and a URL. Sending a bearer token needs a "Request headers" beta most accounts lack, and OAuth is not built yet, so with MCP_PATH_SECRET set the endpoint is also served at /<secret>/mcp without the bearer token — a trial until OAuth replaces it. The path is the credential there. It is compared in constant time, and a wrong one answers 404 like any unknown path. The config refuses fewer than 32 URL-safe characters and never echoes the value, nothing in the server logs request paths, and the Caddy snippet rewrites the segment before an access log entry is written (verified against Caddy 2.11). Claude Code and the CLI keep the bearer token; DEPLOYMENT.md says what the path trades away. 178 tests. Smoke 76/76 and 74/74 on the local instance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
134 lines
4.6 KiB
Markdown
134 lines
4.6 KiB
Markdown
# 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. 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.
|
|
|
|
A fresh Schulcloud token goes in with `schulcloud token set`, or on the page at
|
|
`http://127.0.0.1:8080/token` — no restart; see docs/CLI.md. Editing
|
|
`TSC_JWT_COOKIE` in `.env` instead needs the container **recreated**, because
|
|
`env_file` is read when the container is created, not on restart:
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```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 # 178 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 (76 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
|
|
```
|