package bm import ( "net/http" "git.golem15.com/golem15/summercms/modules/pact" ) // The sentences the management tool descriptions end with (D-15): the scope // and the permission code the route checks. const ( readCourses = " Needs scope read and the jz.bm.access_courses permission." writeCourses = " Needs scope write and the jz.bm.access_courses permission." readSchedule = " Needs scope read and the jz.bm.access_schedule permission." writeSchedule = " Needs scope write and the jz.bm.access_schedule permission." readMaterials = " Needs scope read and the jz.bm.access_materials permission." writeMaterials = " Needs scope write and the jz.bm.access_materials permission." ) // Shared description fragments (D-22). const ( recordAnswer = " Returns the row as an object of its columns (id, created_at and updated_at included); times are RFC 3339 in Europe/Warsaw, dates YYYY-MM-DD." invalidAnswer = " A value that breaks a rule is refused with 422 and the errors per field." patchSemantics = " Only the fields sent change; null or an empty string clears an optional column." deleteForever = " The delete cannot be undone." replacedAsSent = " body, source and url are replaced as sent (null clears them): read the material with include_body true first to keep its body." ) // --- courses ------------------------------------------------------------- // listCoursesInput filters bm_manage_list_courses. type listCoursesInput struct { Semester int `json:"semester,omitempty" query:"semester" jsonschema:"only courses of this semester, 1 to 4"` Form string `json:"form,omitempty" query:"form" jsonschema:"only courses of this form, one of: wyklad, cwiczenia, konwersatorium, seminarium, inne"` Code string `json:"code,omitempty" query:"code" jsonschema:"only the course with this code; null for courses without one"` } // createCourseInput is the body of bm_manage_create_course. type createCourseInput struct { Code *string `json:"code,omitempty" jsonschema:"course code, at most 50 characters"` Name string `json:"name" jsonschema:"course name, at most 255 characters"` Form string `json:"form" jsonschema:"one of: wyklad, cwiczenia, konwersatorium, seminarium, inne"` Lecturer *string `json:"lecturer,omitempty" jsonschema:"lecturer, at most 255 characters, with the title as the university writes it"` Semester int `json:"semester" jsonschema:"semester, 1 to 4"` ECTS *float64 `json:"ects,omitempty" jsonschema:"ECTS points, 0 to 60"` Assessment *string `json:"assessment,omitempty" jsonschema:"form of assessment, at most 100 characters"` ElectiveGroup *string `json:"elective_group,omitempty" jsonschema:"elective group, at most 100 characters"` Description *string `json:"description,omitempty" jsonschema:"description"` SyllabusURL *string `json:"syllabus_url,omitempty" jsonschema:"syllabus link, at most 2048 characters"` SortOrder *int `json:"sort_order,omitempty" jsonschema:"position within the semester, an integer"` } // updateCourseInput is the body of bm_manage_update_course. type updateCourseInput struct { ID uint `json:"id" path:"id" jsonschema:"the course id"` Code *string `json:"code,omitempty" jsonschema:"course code, at most 50 characters"` Name *string `json:"name,omitempty" jsonschema:"course name, at most 255 characters; required, so not null"` Form *string `json:"form,omitempty" jsonschema:"one of: wyklad, cwiczenia, konwersatorium, seminarium, inne"` Lecturer *string `json:"lecturer,omitempty" jsonschema:"lecturer, at most 255 characters"` Semester *int `json:"semester,omitempty" jsonschema:"semester, 1 to 4"` ECTS *float64 `json:"ects,omitempty" jsonschema:"ECTS points, 0 to 60"` Assessment *string `json:"assessment,omitempty" jsonschema:"form of assessment, at most 100 characters"` ElectiveGroup *string `json:"elective_group,omitempty" jsonschema:"elective group, at most 100 characters"` Description *string `json:"description,omitempty" jsonschema:"description"` SyllabusURL *string `json:"syllabus_url,omitempty" jsonschema:"syllabus link, at most 2048 characters"` SortOrder *int `json:"sort_order,omitempty" jsonschema:"position within the semester, an integer"` } // --- meetings ------------------------------------------------------------ // createMeetingInput is the body of bm_manage_create_meeting. type createMeetingInput struct { Number int `json:"number" jsonschema:"the zjazd number, 1 or more, unique"` StartsOn string `json:"starts_on" jsonschema:"first day, YYYY-MM-DD"` EndsOn string `json:"ends_on" jsonschema:"last day, YYYY-MM-DD, not before starts_on"` Notes *string `json:"notes,omitempty" jsonschema:"notes"` } // updateMeetingInput is the body of bm_manage_update_meeting. type updateMeetingInput struct { ID uint `json:"id" path:"id" jsonschema:"the meeting id"` Number *int `json:"number,omitempty" jsonschema:"the zjazd number, 1 or more, unique"` StartsOn *string `json:"starts_on,omitempty" jsonschema:"first day, YYYY-MM-DD"` EndsOn *string `json:"ends_on,omitempty" jsonschema:"last day, YYYY-MM-DD, not before starts_on"` Notes *string `json:"notes,omitempty" jsonschema:"notes"` } // --- class sessions ------------------------------------------------------ // listClassSessionsInput filters bm_manage_list_class_sessions. type listClassSessionsInput struct { CourseID string `json:"course_id,omitempty" query:"course_id" jsonschema:"only sessions of this course id; null for sessions without a course"` MeetingID string `json:"meeting_id,omitempty" query:"meeting_id" jsonschema:"only sessions of this meeting (zjazd) id; null for sessions without one"` } // createClassSessionInput is the body of bm_manage_create_class_session. type createClassSessionInput struct { CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id"` MeetingID *uint `json:"meeting_id,omitempty" jsonschema:"an existing meeting (zjazd) id"` StartsAt string `json:"starts_at" jsonschema:"start time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-03 08:00"` EndsAt string `json:"ends_at" jsonschema:"end time, after starts_at: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-03 09:30"` Room *string `json:"room,omitempty" jsonschema:"room, at most 100 characters"` Kind *string `json:"kind,omitempty" jsonschema:"kind of class, at most 100 characters"` Lecturer *string `json:"lecturer,omitempty" jsonschema:"lecturer, at most 255 characters"` Notes *string `json:"notes,omitempty" jsonschema:"notes"` IsOnline *bool `json:"is_online,omitempty" jsonschema:"true when the class is online"` } // updateClassSessionInput is the body of bm_manage_update_class_session. type updateClassSessionInput struct { ID uint `json:"id" path:"id" jsonschema:"the class session id"` CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id"` MeetingID *uint `json:"meeting_id,omitempty" jsonschema:"an existing meeting (zjazd) id; null clears it"` StartsAt *string `json:"starts_at,omitempty" jsonschema:"start time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-03 08:00"` EndsAt *string `json:"ends_at,omitempty" jsonschema:"end time, after starts_at: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-03 09:30"` Room *string `json:"room,omitempty" jsonschema:"room, at most 100 characters"` Kind *string `json:"kind,omitempty" jsonschema:"kind of class, at most 100 characters"` Lecturer *string `json:"lecturer,omitempty" jsonschema:"lecturer, at most 255 characters"` Notes *string `json:"notes,omitempty" jsonschema:"notes"` IsOnline *bool `json:"is_online,omitempty" jsonschema:"true when the class is online"` } // --- readings ------------------------------------------------------------ // listReadingsInput filters bm_manage_list_readings. type listReadingsInput struct { CourseID string `json:"course_id,omitempty" query:"course_id" jsonschema:"only readings of this course id; null for readings without a course"` MeetingID string `json:"meeting_id,omitempty" query:"meeting_id" jsonschema:"only readings of this meeting (zjazd) id; null for readings without one"` IsRequired string `json:"is_required,omitempty" query:"is_required" jsonschema:"true for required readings only, false for optional ones only"` } // createReadingInput is the body of bm_manage_create_reading. type createReadingInput struct { CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id"` MeetingID *uint `json:"meeting_id,omitempty" jsonschema:"an existing meeting (zjazd) id"` Title string `json:"title" jsonschema:"title, at most 500 characters"` Author *string `json:"author,omitempty" jsonschema:"author, at most 255 characters"` Source *string `json:"source,omitempty" jsonschema:"source (book, journal, chapter), at most 500 characters"` URL *string `json:"url,omitempty" jsonschema:"link, at most 2048 characters"` IsRequired *bool `json:"is_required,omitempty" jsonschema:"true when the reading is required"` SortOrder *int `json:"sort_order,omitempty" jsonschema:"position within the course, an integer"` } // updateReadingInput is the body of bm_manage_update_reading. type updateReadingInput struct { ID uint `json:"id" path:"id" jsonschema:"the reading id"` CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id"` MeetingID *uint `json:"meeting_id,omitempty" jsonschema:"an existing meeting (zjazd) id; null clears it"` Title *string `json:"title,omitempty" jsonschema:"title, at most 500 characters; required, so not null"` Author *string `json:"author,omitempty" jsonschema:"author, at most 255 characters"` Source *string `json:"source,omitempty" jsonschema:"source (book, journal, chapter), at most 500 characters"` URL *string `json:"url,omitempty" jsonschema:"link, at most 2048 characters"` IsRequired *bool `json:"is_required,omitempty" jsonschema:"true when the reading is required"` SortOrder *int `json:"sort_order,omitempty" jsonschema:"position within the course, an integer"` } // --- deadlines ----------------------------------------------------------- // listDeadlinesInput filters bm_manage_list_deadlines. type listDeadlinesInput struct { CourseID string `json:"course_id,omitempty" query:"course_id" jsonschema:"only deadlines of this course id; null for deadlines without a course"` Kind string `json:"kind,omitempty" query:"kind" jsonschema:"only deadlines of this kind, one of: zaliczenie, egzamin, praca, organizacyjne, inne"` } // createDeadlineInput is the body of bm_manage_create_deadline. type createDeadlineInput struct { Title string `json:"title" jsonschema:"title, at most 255 characters"` DueAt string `json:"due_at" jsonschema:"due time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-03 23:59"` CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id"` Kind string `json:"kind" jsonschema:"one of: zaliczenie, egzamin, praca, organizacyjne, inne"` Description *string `json:"description,omitempty" jsonschema:"description"` } // updateDeadlineInput is the body of bm_manage_update_deadline. type updateDeadlineInput struct { ID uint `json:"id" path:"id" jsonschema:"the deadline id"` Title *string `json:"title,omitempty" jsonschema:"title, at most 255 characters; required, so not null"` DueAt *string `json:"due_at,omitempty" jsonschema:"due time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-03 23:59"` CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id; null clears it"` Kind *string `json:"kind,omitempty" jsonschema:"one of: zaliczenie, egzamin, praca, organizacyjne, inne"` Description *string `json:"description,omitempty" jsonschema:"description"` } // --- materials ----------------------------------------------------------- // listManagedMaterialsInput is the input of bm_manage_list_materials. type listManagedMaterialsInput struct { IncludeBody bool `json:"include_body,omitempty" query:"include_body" jsonschema:"true to include each material's body (it may hold transcript text)"` } // showManagedMaterialInput is the input of bm_manage_show_material. type showManagedMaterialInput struct { ID uint `json:"id" path:"id" jsonschema:"the material id"` IncludeBody bool `json:"include_body,omitempty" query:"include_body" jsonschema:"true to include the material's body (it may hold transcript text)"` } // saveMaterialInput is the body of bm_manage_save_material. body, source // and url have no omitempty: the route replaces them as sent, so the agent // must state each (null clears it). type saveMaterialInput struct { CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id; together with title it identifies the material (absent or null: no course)"` MeetingID *uint `json:"meeting_id,omitempty" jsonschema:"an existing meeting (zjazd) id; absent keeps the stored one, null clears it"` Title string `json:"title" jsonschema:"title, 1 to 255 characters"` Type string `json:"type" jsonschema:"one of: pdf, notatka, link, inne"` Body *string `json:"body" jsonschema:"Markdown body, or null for none; replaced as sent"` Source *string `json:"source" jsonschema:"source, at most 500 characters, or null; replaced as sent"` URL *string `json:"url" jsonschema:"link, at most 2048 characters, or null; replaced as sent"` PublishedAt *string `json:"published_at,omitempty" jsonschema:"publication time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-16 20:00; absent publishes a new material now and keeps an existing one as it is; null makes a draft"` Tags *[]string `json:"tags,omitempty" jsonschema:"tag names (1 to 100 characters each); absent keeps the tags, a list replaces them"` } // updateMaterialInput is the body of bm_manage_update_material. type updateMaterialInput struct { ID uint `json:"id" path:"id" jsonschema:"the material id"` CourseID *uint `json:"course_id,omitempty" jsonschema:"an existing course id (absent or null: no course)"` MeetingID *uint `json:"meeting_id,omitempty" jsonschema:"an existing meeting (zjazd) id; absent keeps the stored one, null clears it"` Title string `json:"title" jsonschema:"title, 1 to 255 characters"` Type string `json:"type" jsonschema:"one of: pdf, notatka, link, inne"` Body *string `json:"body" jsonschema:"Markdown body, or null for none; replaced as sent"` Source *string `json:"source" jsonschema:"source, at most 500 characters, or null; replaced as sent"` URL *string `json:"url" jsonschema:"link, at most 2048 characters, or null; replaced as sent"` PublishedAt *string `json:"published_at,omitempty" jsonschema:"publication time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-16 20:00; absent keeps it; null makes a draft"` Tags *[]string `json:"tags,omitempty" jsonschema:"tag names (1 to 100 characters each); absent keeps the tags, a list replaces them"` } // manageTools are the tools over the management API (/api/v1/bm): typed // CRUD of the BM tables (D-13). The routes' user.scope and user.permission // middleware filter the listing, so only a user whose groups hold the // jz.bm.access_* codes sees them, and write tools only with the write // grant. There is no tool for POST /seed or GET /me, and no dry_run (D-14). func manageTools() []pact.MCPTool { tool := func(name, title, desc, method, path string, input any) pact.MCPTool { return pact.MCPTool{Name: name, Title: title, Description: desc, Guard: "user", Method: method, Path: ManagePrefix + path, Input: input} } g, po, pa, pu, d := http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodPut, http.MethodDelete return []pact.MCPTool{ tool("manage_list_courses", "List courses (manage)", "Lists every course as rows of columns, ordered by semester and position, optionally filtered."+readCourses, g, "/courses", listCoursesInput{}), tool("manage_show_course", "Show a course (manage)", "Returns one course; 404 when unknown."+recordAnswer+readCourses, g, "/courses/{id}", idInput{}), tool("manage_create_course", "Create a course", "Creates a course."+recordAnswer+invalidAnswer+writeCourses, po, "/courses", createCourseInput{}), tool("manage_update_course", "Update a course", "Updates one course."+patchSemantics+recordAnswer+invalidAnswer+writeCourses, pa, "/courses/{id}", updateCourseInput{}), tool("manage_list_meetings", "List meetings (manage)", "Lists every zjazd (meeting) as rows of columns, ordered by number."+readSchedule, g, "/meetings", noInput{}), tool("manage_show_meeting", "Show a meeting (manage)", "Returns one zjazd (meeting); 404 when unknown."+recordAnswer+readSchedule, g, "/meetings/{id}", idInput{}), tool("manage_create_meeting", "Create a meeting", "Creates a zjazd (meeting)."+recordAnswer+invalidAnswer+writeSchedule, po, "/meetings", createMeetingInput{}), tool("manage_update_meeting", "Update a meeting", "Updates one zjazd (meeting)."+patchSemantics+recordAnswer+invalidAnswer+writeSchedule, pa, "/meetings/{id}", updateMeetingInput{}), tool("manage_list_class_sessions", "List class sessions (manage)", "Lists every class session as rows of columns, ordered by start time, optionally filtered by course or meeting."+readSchedule, g, "/class-sessions", listClassSessionsInput{}), tool("manage_show_class_session", "Show a class session (manage)", "Returns one class session; 404 when unknown."+recordAnswer+readSchedule, g, "/class-sessions/{id}", idInput{}), tool("manage_create_class_session", "Create a class session", "Creates a class session; course_id and meeting_id must exist."+recordAnswer+invalidAnswer+writeSchedule, po, "/class-sessions", createClassSessionInput{}), tool("manage_update_class_session", "Update a class session", "Updates one class session."+patchSemantics+recordAnswer+invalidAnswer+writeSchedule, pa, "/class-sessions/{id}", updateClassSessionInput{}), tool("manage_delete_class_session", "Delete a class session", "Deletes one class session; 404 when unknown."+deleteForever+writeSchedule, d, "/class-sessions/{id}", idInput{}), tool("manage_list_readings", "List readings (manage)", "Lists every reading as rows of columns, ordered by course and position, optionally filtered."+readCourses, g, "/readings", listReadingsInput{}), tool("manage_show_reading", "Show a reading (manage)", "Returns one reading; 404 when unknown."+recordAnswer+readCourses, g, "/readings/{id}", idInput{}), tool("manage_create_reading", "Create a reading", "Creates a reading; course_id and meeting_id must exist."+recordAnswer+invalidAnswer+writeCourses, po, "/readings", createReadingInput{}), tool("manage_update_reading", "Update a reading", "Updates one reading."+patchSemantics+recordAnswer+invalidAnswer+writeCourses, pa, "/readings/{id}", updateReadingInput{}), tool("manage_delete_reading", "Delete a reading", "Deletes one reading with every student's read and owned marks of it; 404 when unknown."+deleteForever+writeCourses, d, "/readings/{id}", idInput{}), tool("manage_list_deadlines", "List deadlines (manage)", "Lists every deadline as rows of columns, ordered by due time, optionally filtered by course or kind."+readSchedule, g, "/deadlines", listDeadlinesInput{}), tool("manage_show_deadline", "Show a deadline (manage)", "Returns one deadline; 404 when unknown."+recordAnswer+readSchedule, g, "/deadlines/{id}", idInput{}), tool("manage_create_deadline", "Create a deadline", "Creates a deadline; course_id must exist."+recordAnswer+invalidAnswer+writeSchedule, po, "/deadlines", createDeadlineInput{}), tool("manage_update_deadline", "Update a deadline", "Updates one deadline."+patchSemantics+recordAnswer+invalidAnswer+writeSchedule, pa, "/deadlines/{id}", updateDeadlineInput{}), tool("manage_delete_deadline", "Delete a deadline", "Deletes one deadline; 404 when unknown."+deleteForever+writeSchedule, d, "/deadlines/{id}", idInput{}), tool("manage_list_materials", "List materials (manage)", "Lists every material, drafts included, newest first, with course, zjazd, type, source, link, publication time and tags. "+MCPMaterialPrivacy+readMaterials, g, "/materials", listManagedMaterialsInput{}), tool("manage_show_material", "Show a material (manage)", "Returns one material, draft or not, with its public files (id, absolute link, name, size and type); 404 when unknown. "+MCPMaterialPrivacy+readMaterials, g, "/materials/{id}", showManagedMaterialInput{}), tool("manage_save_material", "Save a material", "Creates a material, or updates the one with the same title and course (answer created true or false); a renamed material is a new one here, use bm_manage_update_material to rename."+replacedAsSent+" "+MCPMaterialPrivacy+invalidAnswer+writeMaterials, po, "/materials", saveMaterialInput{}), tool("manage_update_material", "Update a material", "Updates one material, so its title or course can change; 404 when unknown, 422 when another material has the same title and course."+replacedAsSent+" "+MCPMaterialPrivacy+invalidAnswer+writeMaterials, pu, "/materials/{id}", updateMaterialInput{}), tool("manage_delete_material", "Delete a material", "Deletes one material with all its files; 404 when unknown."+deleteForever+" "+MCPMaterialPrivacy+writeMaterials, d, "/materials/{id}", idInput{}), } }