MCP & OAuth 2.1
What the MCP server offers
Institflow runs a Model Context Protocol 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 51 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:
- It fetches
GET /.well-known/oauth-protected-resource(RFC 9728) from/mcp'sWWW-Authenticatechallenge, thenGET /.well-known/oauth-authorization-server(RFC 8414) for the token/registration/authorize endpoints. - 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 theOAUTH_DYNAMIC_REGISTRATIONsetting if you'd rather pre-register clients yourself). - It opens
GET /oauth/authorizewith 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. - After you approve, the client exchanges the authorization code —
POST /oauth/tokenwith your PKCEcode_verifier— for an access token (a normal Institflow JWT, justtyp: "mcp") and a refresh token good for 30 days.
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.
Academics
| 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 |
Admin
| Tool | Description | Permission | Destructive |
|---|---|---|---|
| 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). | 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 |
Assessment
| Tool | Description | Permission | Destructive |
|---|---|---|---|
| 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 |
Attendance
| Tool | Description | Permission | Destructive |
|---|---|---|---|
| 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
| Tool | Description | Permission | Destructive |
|---|---|---|---|
| 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 |
curriculum
| Tool | Description | Permission | Destructive |
|---|---|---|---|
| create_lesson | Create a lesson in a course. Omit sectionId to use (or auto-create) the course’s default section. | 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 or TeraBox share 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 or release rules. | curriculum.write | No |
Notifications
| Tool | Description | Permission | Destructive |
|---|---|---|---|
| notification_usage | Notification channel usage/cost for a billing period (defaults to the current month, format YYYY-MM). | notifications.read | No |
| 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 |
Students
| Tool | Description | Permission | Destructive |
|---|---|---|---|
| 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:groupIdorstudentId,month)overdue_followup— draft a follow-up plan for overdue invoices.daily_briefing— today's schedule, absences, and anything overdue, summarized.