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:
43
docker-compose.yml
Normal file
43
docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
# Standalone Compose file for the Pi.
|
||||
#
|
||||
# If you already run Caddy and PostgreSQL from another Compose project, either
|
||||
# merge the `schulcloud-mcp` service below into that project's file, or keep
|
||||
# this file separate and attach it to the existing Caddy network — see the
|
||||
# `networks` block at the bottom and deploy/Caddyfile.snippet.
|
||||
|
||||
services:
|
||||
schulcloud-mcp:
|
||||
build: .
|
||||
image: schulcloud-mcp:latest
|
||||
container_name: schulcloud-mcp
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
PORT: 8080
|
||||
BIND_HOST: 0.0.0.0
|
||||
# No ports are published to the host: Caddy reaches the container over the
|
||||
# shared Docker network, so the only way in from the internet is through
|
||||
# Caddy's TLS and this server's bearer check.
|
||||
expose:
|
||||
- "8080"
|
||||
networks:
|
||||
- caddy
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
# Set to true once this joins the network your existing Caddy already uses,
|
||||
# and change the name to match (`docker network ls` to find it).
|
||||
external: false
|
||||
name: caddy
|
||||
Reference in New Issue
Block a user