Files
Schulcloud-MCP/src/mcp/server.ts
MechaCat02 1de026ca43 Serve rooms ("Räume"), which are not courses however the urls read
The account this was built against is in no rooms, so the whole space was
invisible and easy to dismiss as an empty endpoint. It is not empty in
general — the user had rooms until a teacher removed access — and the
UI's naming actively hides the distinction: the sidebar's *Kurse* entry
links to `/rooms/courses-overview` and lists courses, while *Räume* links
to `/rooms` and lists rooms. A url containing `/rooms` identifies neither.

list_rooms and get_room cover the latter. A room holds boards and nothing
else, so get_room lists boards for get_board (which already reports "in
room" from the board context) plus who else is in it. Room boards report
`isVisible`, which the course-page projection does not, so a draft is
named as a draft instead of being offered and then answering 403.

Rooms also go through the crawl, or they would have become the next
blind spot: their boards are indexed, searchable by both the index and
the live-crawl path, diffed by what_changed, and mirrored by the CLI
under the room's name. The board traversal and the snapshot matcher are
now shared between courses and rooms rather than duplicated, which also
fixed the live-crawl path silently not searching pad contents.

The CLI needed no new command — it is file-centric and inherits rooms
through the manifest — but `--course` now accepts a room id, and says so.

`kind` gains 'room'; the column is plain TEXT, so no migration. 112 tests.
Smoke: 42/42 and 44/44 local, 41/41 and 43/43 live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 19:24:53 +02:00

61 lines
3.3 KiB
TypeScript

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { Config } from '../config.ts';
import { ServerContext } from '../context.ts';
import type { Services } from '../services.ts';
import { registerContentTools } from './tools/content.ts';
import { registerFileTools } from './tools/files.ts';
import { registerOverviewTools } from './tools/overview.ts';
import { registerRawTool } from './tools/raw.ts';
import { registerIndexTools } from './tools/index-tools.ts';
import { registerSearchTool } from './tools/search.ts';
import { registerRoomTools } from './tools/rooms.ts';
import { registerSubmissionTools } from './tools/submissions.ts';
export const SERVER_NAME = 'schulcloud-mcp';
export const SERVER_VERSION = '0.1.0';
const INSTRUCTIONS = `Read-only access to a Schulcloud (HPI Schul-Cloud / Schulcloud-Verbund-Software) account.
How the content is organised, and the usual path through it:
- **Courses** ("Kurse") are the top level — list_courses, or get_dashboard for the ones the user has pinned.
- A course page (get_course) holds three kinds of thing:
- **Column boards** — where most current teaching material lives. get_board returns every column, card,
text block, link and attached file in one call.
- **Topics / lessons** ("Themen") — the older format. get_lesson.
- **Tasks** ("Aufgaben") — homework. list_tasks across all courses, get_task for one.
- **Files** hang off boards, lessons and tasks. Every listing shows file ids; download_file fetches one and
extracts its text (PDF, Word, Excel, PowerPoint, OpenDocument) or returns an image inline.
- **Submissions** ("Abgaben") — what the user handed in. get_task shows that task's submission: the files,
the graded flag, the grade, what the user wrote, and the teacher's written feedback. A grade is a
percentage (0-100) or absent — there is no textual grade — and teachers often grade with the written
feedback alone, so "graded by feedback" is a complete result, not missing data. list_submissions
surveys them across tasks ("what is still ungraded?"). The written parts are read from the web page
because no API exposes them, so they can be missing even when feedback exists — if none is shown for a
graded submission, say it was not found rather than that none was given. On a teacher account these
tools report other people's submissions too.
When the user names a topic rather than a course, use search — the API has no search endpoint, so it walks the
courses and matches client-side, which takes a few seconds but covers board text and file names.
Everything here is read-only; nothing in this server can modify the account.`;
export function createServer(config: Config, services?: Services): { server: McpServer; context: ServerContext } {
const context = new ServerContext(config, services);
const server = new McpServer(
{ name: SERVER_NAME, version: SERVER_VERSION },
{ capabilities: { tools: {}, logging: {} }, instructions: INSTRUCTIONS },
);
registerOverviewTools(server, context);
registerContentTools(server, context);
registerRoomTools(server, context);
registerFileTools(server, context);
registerSearchTool(server, context);
registerSubmissionTools(server, context);
registerIndexTools(server, context);
registerRawTool(server, context);
return { server, context };
}