## What the MCP server offers

Institflow runs a [Model Context Protocol](https://modelcontextprotocol.io) server at `/mcp` (Streamable HTTP, stateless — every call is independent, so it scales like any other API request). It exposes the same domain services every REST endpoint calls, through 74 task-oriented tools, a handful of read-only resources, and a few report-style prompts — so an AI agent (Claude, or any MCP client) can search students, check a schedule, record attendance, or draft a progress report, using **exactly the caller's own permissions**, never more.

## Connecting a client

Institflow is its own OAuth 2.1 authorization server for this — no separate API key or manual token needed. A compliant MCP client discovers everything it needs automatically:

1. It fetches `GET /.well-known/oauth-protected-resource` (RFC 9728) from `/mcp`'s `WWW-Authenticate` challenge, then `GET /.well-known/oauth-authorization-server` (RFC 8414) for the token/registration/authorize endpoints.
2. If it doesn't already have a `client_id`, it registers one dynamically: `POST /oauth/register` (RFC 7591 — a public client, no secret; disable this endpoint tenant-wide with the `OAUTH_DYNAMIC_REGISTRATION` setting if you'd rather pre-register clients yourself).
3. It opens `GET /oauth/authorize` with PKCE (S256 — required, not optional, under OAuth 2.1) in your browser. You sign in (if needed) and land on a consent screen showing exactly which tenant/membership you're connecting and what the client can do.
4. After you approve, the client exchanges the authorization code — `POST /oauth/token` with your PKCE `code_verifier` — for an access token (a normal Institflow JWT, just `typ: "mcp"`) and a refresh token good for 30 days.

```http
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...
```

> **An MCP token never authenticates a REST call**
>
> The reverse also isn't possible — a `typ: "mcp"` token is rejected with `401` on every ordinary `/api/v1/*` route, and a regular access token can't call `/mcp`. They're deliberately separate.

## Scopes & permissions

Getting a token at all requires the `mcp.access` permission on the membership you connect with — a member without it (most `secretary`/`teacher` roles, by default) is refused at the consent step, before any token is ever issued. Every tool additionally checks its own declared permission on every call — identical to what the equivalent REST endpoint would require.

Tools marked **destructive** below need `mcp.destructive` as well. Calling one without `confirm: true` never mutates anything — it returns a preview of what *would* happen (e.g. "would change status from active to withdrawn") and `requiresConfirmation: true`. The agent (or the person driving it) re-calls the same tool with `confirm: true` to actually execute it. Every call — preview or execution — is audited with `channel: "mcp"`, the tool name, and a hash of its arguments (never the raw arguments, which may carry PII).

## Tool catalogue

Every tool, generated directly from `apps/api/src/modules/mcp/tools/*.ts` — the exact source the MCP server itself registers at boot.

| Tool | Description | Permission | Destructive |
| --- | --- | --- | --- |
| cancel_session | Cancel a scheduled class session, with a reason. Destructive: previews unless confirm: true is passed. | groups.write | Yes |
| create_group | Create a new teaching group (class section) for a course, with an optional weekly schedule. | groups.write | No |
| drop_enrollment | Drop a student's enrollment from a group. Destructive: previews unless confirm: true is passed. | enrollments.write | Yes |
| enroll_student | Enroll a student into a teaching group. | enrollments.write | No |
| get_group | Get full details for one teaching group by id. | groups.read | No |
| get_schedule | Get scheduled class sessions for a group, teacher, or student within an optional date range. Exactly one of groupId/teacherStaffId/studentId is required. | groups.read | No |
| list_courses | List the course catalogue (subjects/programs offered), optionally filtered by category or active status. | courses.read | No |
| list_groups | List teaching groups (class sections), optionally filtered by branch, course, status, or teacher. | groups.read | No |
| transfer_student | Transfer a student's enrollment from their current group to another group (drop + enroll in one operation). Destructive: previews unless confirm: true is passed. | enrollments.write | Yes |
| audit_search | Search the tenant's audit log by action, entity, actor, or date range. | audit.read | No |
| dashboard_summary | Owner/manager dashboard summary — currently the tenant-wide billing summary (outstanding balance, collections, overdue). | invoices.read | No |
| get_settings | Get the tenant's effective settings (general, attendance, grading, billing, notifications, curriculum, reminders). | settings.manage | No |
| invite_member | Invite a new member to the tenant by email, with a role. Destructive: previews unless confirm: true is passed. | members.manage | Yes |
| list_members | List members of the current tenant, with their assigned roles. | members.manage | No |
| list_roles | List every role (system and custom) in the tenant, with its permissions. | roles.manage | No |
| set_member_roles | Set a member's complete role assignment (within one branch scope, or tenant-wide) to exactly the given list of role ids. Destructive: previews the add/remove diff unless confirm: true is passed. | members.manage | Yes |
| update_settings | Update one or more tenant settings keys. Destructive: previews the before/after values unless confirm: true is passed. | settings.manage | Yes |
| create_exam | Create a new exam/quiz/homework/project for a group. | assessment.write | No |
| generate_progress_report | Queue generation of a progress report for a student or an entire group over a period. Asynchronous — returns immediately once queued. | assessment.write | No |
| get_student_grades | Get a student's grades (per-group weighted average and per-exam breakdown), optionally scoped to one group. | assessment.read | No |
| publish_exam | Publish an exam so its results become visible to students/guardians. Destructive: previews unless confirm: true is passed. | assessment.write | Yes |
| record_results | Bulk-record (or update) student scores for an exam. | assessment.write | No |
| send_progress_reports | Send every published (not-yet-sent) progress report for a student or group to guardians. Destructive: previews the list unless confirm: true is passed. | assessment.write | Yes |
| curriculum.assignments.create | Create an assignment unit in a course: a lesson of kind "assignment" plus its instructions, due date, max score, late policy and allowed submission kinds, in one transaction. | curriculum.write | No |
| curriculum.assignments.grade_submission | Grade a submission (score at most the assignment’s max score, optional feedback). Emits assignment.graded. | curriculum.assignments.grade | No |
| curriculum.assignments.list_submissions | List an assignment’s submissions (cursor-paginated), optionally filtered by status. lessonId is the assignment unit’s id, or the id of the video lesson that carries the task. | curriculum.assignments.grade | No |
| curriculum.assignments.publish_to_gradebook | Publish every currently-graded submission into the gradebook as a "homework" exam per group — upserts on repeat. Destructive: the first publish for a group creates and publishes a real exam, notifying students/guardians, so it previews unless confirm: true is passed. | curriculum.assignments.grade | Yes |
| curriculum.assignments.return_submission | Send a submission back to the student for revision (clears any earlier grade). | curriculum.assignments.grade | No |
| curriculum.assignments.update | Update an assignment’s own fields (instructions, due date, max score, late policy, allowed submission kinds). Lesson-shared fields (title, section, drip gate) go through update_lesson. | curriculum.write | No |
| curriculum.lesson_tasks.remove | Remove the task under a video lesson. Destructive: previews (with the number of submissions that would be deleted with it) unless confirm: true is passed, which also confirms deleting those submissions. | curriculum.write | Yes |
| curriculum.lesson_tasks.set | Create or replace «مهمة هذا الدرس», the task under a video lesson (instructions, due date, max score, late policy, accepted submission kinds). Submissions, grading and the gradebook publish then use the curriculum.assignments.* tools with the same lessonId. | curriculum.write | No |
| attendance_report | Attendance report for a group (per-student rates) or a single student (full history), within an optional date range. | attendance.read | No |
| get_session_roster | Get the attendance roster for a scheduled session — every enrolled student and their current attendance mark, if any. | attendance.read | No |
| record_attendance | Bulk-record (or update) attendance marks for every student on a session's roster. | attendance.mark | No |
| billing_summary | Tenant-wide billing summary — outstanding balance, this month's collections, and overdue counts/totals. | invoices.read | No |
| create_invoice | Create a new draft invoice for a student, with one or more line items. | invoices.write | No |
| get_invoice | Get full details for one invoice, including its lines and payments. | invoices.read | No |
| list_invoices | List invoices, filterable by status (including an "overdue" shorthand), student, guardian, or due-date range. | invoices.read | No |
| record_payment | Record a payment against an invoice. | payments.record | No |
| student_statement | Get a student's full billing statement — every invoice/payment entry and the running balance. | invoices.read | No |
| void_invoice | Void an invoice, with a reason. Destructive: previews unless confirm: true is passed. | invoices.write | Yes |
| create_lesson | Create a lesson in a course. Omit sectionId to use (or auto-create) the course’s default section. audienceGroupIds restricts it to those groups of the course; omit or null for every group. | curriculum.write | No |
| delete_lesson | Delete a lesson and its video and progress rows. Destructive: previews (with how many students have progress on it) unless confirm: true is passed. | curriculum.write | Yes |
| get_lesson | Get one lesson by id, including its markdown body and video source. | curriculum.read | No |
| grant_course_access | Grant a student access to a self-paced (video) course. Idempotent — granting twice returns the existing enrollment. | enrollments.write | No |
| list_course_curriculum | List a course’s curriculum: its sections, each with the lessons inside it. Published lessons only unless includeDrafts is true. | curriculum.read | No |
| publish_lesson | Publish a lesson so enrolled students can see it. Destructive: notifies real people, so it previews unless confirm: true is passed. | curriculum.publish | Yes |
| revoke_course_access | Revoke a student’s self-paced course access. Destructive: previews unless confirm: true is passed. | enrollments.write | Yes |
| set_lesson_video | Attach a video to a lesson from a YouTube URL. The URL is validated and normalized exactly as the web editor does. | curriculum.write | No |
| student_course_progress | One student’s progress through their video courses — lessons completed vs. available, and the last lesson they opened. | curriculum.read | No |
| update_lesson | Update a lesson’s title, summary, markdown body, estimate, release rules or audience (audienceGroupIds: a list of the course’s group ids to restrict the lesson to those groups, null for every group). | curriculum.write | No |
| get_notification_rule | One notification type: the effective rule, the override the institute made, what the type allows and locks, and per channel whether it can deliver, who bills it and the price per message. | notifications.read | No |
| get_notification_spend | Notification usage this month per channel and, when the install is on the Institflow messaging relay, the credit balance. | notifications.manage | No |
| list_notification_rules | The notification rules of the institute: for every kind of notice the system sends — absence, invoices, payments, grades, lessons, assignments, quizzes, class and deadline reminders, subscription alerts — who receives it, on which channels, and whether the institute changed it from the Institflow default. | notifications.read | No |
| notification_usage | Notification channel usage/cost for a billing period (defaults to the current month, format YYYY-MM). | notifications.read | No |
| preview_notification_rule | Changes nothing: the estimated monthly cost (a range, in credit units — 10 000 units per credit), how many people a proposed rule would reach, whether saving it needs cost confirmation, and warnings. Omit `rule` to describe the current rule. | notifications.read | No |
| reset_notification_rule | Deletes the override the institute made for one notification type: it goes back to the Institflow default. Destructive: previews the change unless confirm: true is passed. Going back to the default can switch the type or a paid channel on again; that additionally needs confirmCost: true, exactly like update_notification_rule. | notifications.manage | Yes |
| send_message | Send a message to a student/guardian/group/branch audience via the notification channel ladder (email/SMS/WhatsApp/push, falling back down the ladder). SMS/WhatsApp are platform/licence-approved per tenant — naming one explicitly in `channels` when it is not approved is rejected; leaving `channels` unset falls through the ladder to an approved channel (in-app always reachable) instead. Destructive: previews recipient count and quota impact unless confirm: true is passed. | notifications.send | Yes |
| update_notification_rule | Replaces the override the institute made for one notification type (a field left out goes back to the Institflow default). Destructive: previews before/after AND the estimated cost unless confirm: true is passed. A change that raises what the institute pays for — a paid channel switched on, recipients, lead times or scope added — additionally needs confirmCost: true, so an assistant cannot spend credit by accident even with confirm. | notifications.manage | Yes |
| curriculum.quizzes.add_question | Add a question to a quiz. Returns the whole quiz (questions included). | curriculum.quizzes.manage | No |
| curriculum.quizzes.create | Create a quiz unit in a course: a lesson of kind "quiz" plus its settings, in one transaction. No questions yet. | curriculum.quizzes.manage | No |
| curriculum.quizzes.delete_question | Delete a quiz question. Returns the whole quiz (questions included). | curriculum.quizzes.manage | No |
| curriculum.quizzes.grade_answer | Grade a short-answer (short_text) answer (points at most the question’s own points). Finalises the attempt’s score/passed once nothing is left ungraded. Emits quiz.graded on finalisation. | curriculum.assignments.grade | No |
| curriculum.quizzes.list_attempts | List a quiz’s attempts (cursor-paginated), optionally filtered by student or status. | curriculum.assignments.grade | No |
| curriculum.quizzes.publish_to_gradebook | Publish each student’s best fully-graded attempt into the gradebook as a "quiz" exam per group — upserts on repeat. Destructive: the first publish for a group creates and publishes a real exam, notifying students/guardians, so it previews unless confirm: true is passed. | curriculum.assignments.grade | Yes |
| curriculum.quizzes.reorder_questions | Whole-list reorder of a quiz’s questions. Returns the whole quiz (questions included). | curriculum.quizzes.manage | No |
| curriculum.quizzes.update | Update a quiz’s own settings (time limit, attempts allowed, shuffle, show-answers-after, pass score, due date). Lesson-shared fields go through update_lesson. | curriculum.quizzes.manage | No |
| curriculum.quizzes.update_question | Update a quiz question. Returns the whole quiz (questions included). | curriculum.quizzes.manage | No |
| add_guardian | Attach a guardian to a student — either link an existing guardian by id, or create a new one and link it. | guardians.write | No |
| create_student | Create a new student record in the current tenant. | students.write | No |
| get_student | Get full details for one student by id, including guardians and status. | students.read | No |
| search_students | Search/list students in the current tenant, filtered by free-text query, status, branch, group, or tag. Cursor-paginated. | students.read | No |
| set_student_status | Change a student's lifecycle status (lead/active/inactive/graduated/withdrawn). Destructive: previews the transition unless confirm: true is passed. | students.write | Yes |
| update_student | Update a student's profile fields (name, contact info, tags, ...). Does not change status — use set_student_status for that. | students.write | No |

## Resources & prompts

Read-only resources, addressed by URI, for a client that wants to load context without calling a tool:

| URI | Returns |
| --- | --- |
| `institflow://student/{id}` | A student's profile, guardians, and status. |
| `institflow://group/{id}` | A teaching group's schedule and roster. |
| `institflow://invoice/{id}` | An invoice with its lines and payments. |
| `institflow://schedule/today` | Today's sessions across the tenant (capped at 100). |

Prompts — reusable instruction templates a client can surface as a slash command or button:

- `monthly_report` (args: `groupId` or `studentId`, `month`)
- `overdue_followup` — draft a follow-up plan for overdue invoices.
- `daily_briefing` — today's schedule, absences, and anything overdue, summarized.
