Files
sm-bm-plugin/mcp_student.go
Jakub Zych e106b36a9a feat: add the BM student MCP tools
- token twins of the student API under /_bm/api/v1/token (user.api_token,
  per-user jz.bm.token bucket, a scope on each), from one mountStudent
- pact.HasMCP with the eleven student tools over the twins
- TestBMMCP and TestTokenMirrorRoutes
2026-10-28 14:32:03 +01:00

157 lines
5.9 KiB
Go

package bm
import (
"net/http"
"git.golem15.com/golem15/summercms/modules/pact"
)
// The sentences every student tool description ends with (D-15).
const (
needsScopeRead = " Needs scope read."
needsScopeWrite = " Needs scope write."
)
// noInput is the input of a tool that takes no arguments.
type noInput struct{}
// idInput names one record by its id.
type idInput struct {
ID uint `json:"id" path:"id" jsonschema:"the record id, as listed by the matching list tool"`
}
// readingIDInput names one reading by its id.
type readingIDInput struct {
ID uint `json:"id" path:"id" jsonschema:"the reading id, as listed by bm_readings or bm_course"`
}
// coursesInput is the input of bm_courses.
type coursesInput struct {
Semester int `json:"semester,omitempty" query:"semester" jsonschema:"only courses of this semester, 1 to 4"`
}
// rangeInput is the input of bm_schedule and bm_deadlines.
type rangeInput struct {
From string `json:"from,omitempty" query:"from" jsonschema:"first day, YYYY-MM-DD (default: today)"`
To string `json:"to,omitempty" query:"to" jsonschema:"last day (inclusive), YYYY-MM-DD, not before from (default: no end)"`
}
// readingsInput is the input of bm_readings.
type readingsInput struct {
Course uint `json:"course,omitempty" query:"course" jsonschema:"only readings of this course id"`
}
// boolPtr returns a pointer to b, for the tool annotations.
func boolPtr(b bool) *bool { return &b }
// studentTools are the tools over the token twins of the student API. Each
// call runs as the session's user, who reaches only their own progress and
// published data, as on the HTTP API (D-09).
func studentTools() []pact.MCPTool {
return []pact.MCPTool{
{
Name: "dashboard",
Title: "BM dashboard",
Description: "Returns the student's dashboard: the next class sessions, the deadlines not yet due, the zjazd (meeting) under way today and the next one, and the student's reading progress counts." + needsScopeRead,
Guard: "user",
Method: http.MethodGet,
Path: TokenPrefix + "/dashboard",
Input: noInput{},
},
{
Name: "courses",
Title: "BM courses",
Description: "Lists the programme's courses with code, form, lecturer, semester, ECTS, assessment, syllabus link and counters." + needsScopeRead,
Guard: "user",
Method: http.MethodGet,
Path: TokenPrefix + "/courses",
Input: coursesInput{},
},
{
Name: "course",
Title: "BM course",
Description: "Returns one course with its description, syllabus link, counters, class sessions, readings (with the student's read and owned marks) and published materials." + needsScopeRead,
Guard: "user",
Method: http.MethodGet,
Path: TokenPrefix + "/courses/{id}",
Input: idInput{},
},
{
Name: "schedule",
Title: "BM schedule",
Description: "Lists the class sessions starting in a date range (default: from today, no end) with course, zjazd, start and end time, room, kind, lecturer, notes and whether the class is online." + needsScopeRead,
Guard: "user",
Method: http.MethodGet,
Path: TokenPrefix + "/schedule",
Input: rangeInput{},
},
{
Name: "meetings",
Title: "BM meetings",
Description: "Lists the zjazdy (weekend meetings) with number, first and last day, notes and the number of class sessions in each." + needsScopeRead,
Guard: "user",
Method: http.MethodGet,
Path: TokenPrefix + "/meetings",
Input: noInput{},
},
{
Name: "deadlines",
Title: "BM deadlines",
Description: "Lists the deadlines due in a date range (default: from today, no end) with title, due time, kind, course and description." + needsScopeRead,
Guard: "user",
Method: http.MethodGet,
Path: TokenPrefix + "/deadlines",
Input: rangeInput{},
},
{
Name: "readings",
Title: "BM readings",
Description: "Lists readings with author, source, link, course, zjazd, whether required, and the student's read and owned marks." + needsScopeRead,
Guard: "user",
Method: http.MethodGet,
Path: TokenPrefix + "/readings",
Input: readingsInput{},
},
{
Name: "mark_reading_read",
Title: "Mark a reading as read",
Description: "Marks a reading as read by the signed-in student and returns the reading id with the read time. Marking again keeps the first time." + needsScopeWrite,
Guard: "user",
Method: http.MethodPut,
Path: TokenPrefix + "/readings/{id}/progress",
Input: readingIDInput{},
},
{
Name: "unmark_reading_read",
Title: "Unmark a reading as read",
Description: "Removes the signed-in student's read mark from a reading (it can be marked again) and returns the reading id with an empty read time." + needsScopeWrite,
Guard: "user",
Method: http.MethodDelete,
Path: TokenPrefix + "/readings/{id}/progress",
Input: readingIDInput{},
Destructive: boolPtr(false),
Idempotent: boolPtr(true),
},
{
Name: "mark_reading_owned",
Title: "Mark a reading as owned",
Description: "Marks that the signed-in student has a copy of a reading and returns the reading id with the owned time." + needsScopeWrite,
Guard: "user",
Method: http.MethodPut,
Path: TokenPrefix + "/readings/{id}/owned",
Input: readingIDInput{},
},
{
Name: "unmark_reading_owned",
Title: "Unmark a reading as owned",
Description: "Removes the signed-in student's owned mark from a reading (it can be marked again) and returns the reading id with an empty owned time." + needsScopeWrite,
Guard: "user",
Method: http.MethodDelete,
Path: TokenPrefix + "/readings/{id}/owned",
Input: readingIDInput{},
Destructive: boolPtr(false),
Idempotent: boolPtr(true),
},
}
}