feat: add the BM management MCP tools
- 28 typed tools over /api/v1/bm: list, show, create and update for courses and meetings, plus delete for class sessions, readings, deadlines and materials (save upserts on title and course) - inputs mirror Fillable and Rules; enums, lengths and date formats in the field descriptions; no seed, me or dry_run tool - materials accept meeting_id (absent keeps, null clears, must exist) and the managed material JSON shows it - README: MCP tools section
This commit is contained in:
@@ -141,6 +141,7 @@ type managedMaterialJSON struct {
|
||||
ID uint `json:"id"`
|
||||
CourseID *uint `json:"course_id"`
|
||||
Course *courseRef `json:"course"`
|
||||
MeetingID *uint `json:"meeting_id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Body *string `json:"body"`
|
||||
@@ -152,7 +153,7 @@ type managedMaterialJSON struct {
|
||||
|
||||
func newManagedMaterialJSON(m models.Material) managedMaterialJSON {
|
||||
out := managedMaterialJSON{
|
||||
ID: m.ID, CourseID: m.CourseID, Course: newCourseRef(m.Course), Title: m.Title, Type: m.Type,
|
||||
ID: m.ID, CourseID: m.CourseID, Course: newCourseRef(m.Course), MeetingID: m.MeetingID, Title: m.Title, Type: m.Type,
|
||||
Body: m.Body, Source: m.Source, URL: m.URL, Tags: newTagsJSON(m.Tags),
|
||||
}
|
||||
if m.PublishedAt != nil {
|
||||
@@ -185,6 +186,7 @@ func (h *Handlers) ManagedMaterials(w http.ResponseWriter, r *http.Request) {
|
||||
// materialInput is the body of POST /materials and PUT /materials/{id}.
|
||||
type materialInput struct {
|
||||
CourseID *uint `json:"course_id"`
|
||||
MeetingID *uint `json:"meeting_id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Body *string `json:"body"`
|
||||
@@ -200,8 +202,12 @@ type materialDraft struct {
|
||||
// hasPublishedAt tells a sent published_at (null included) from an
|
||||
// absent one.
|
||||
hasPublishedAt bool
|
||||
publishedAt *time.Time
|
||||
tags []models.Tag
|
||||
// hasMeetingID tells a sent meeting_id (null included) from an absent
|
||||
// one: absent keeps the stored meeting, so scripts that never send it
|
||||
// keep working.
|
||||
hasMeetingID bool
|
||||
publishedAt *time.Time
|
||||
tags []models.Tag
|
||||
}
|
||||
|
||||
// readMaterial decodes and validates the body of POST /materials and
|
||||
@@ -222,6 +228,7 @@ func (h *Handlers) readMaterial(w http.ResponseWriter, r *http.Request, db *gorm
|
||||
var keys map[string]json.RawMessage
|
||||
_ = json.Unmarshal(raw, &keys)
|
||||
_, d.hasPublishedAt = keys["published_at"]
|
||||
_, d.hasMeetingID = keys["meeting_id"]
|
||||
|
||||
in := &d.in
|
||||
in.Title = strings.TrimSpace(in.Title)
|
||||
@@ -268,6 +275,16 @@ func (h *Handlers) readMaterial(w http.ResponseWriter, r *http.Request, db *gorm
|
||||
errs["course_id"] = []string{"The selected course_id is invalid."}
|
||||
}
|
||||
}
|
||||
if in.MeetingID != nil {
|
||||
var n int64
|
||||
if err := db.Model(&models.Meeting{}).Where("id = ?", *in.MeetingID).Count(&n).Error; err != nil {
|
||||
writeOpaque500(w)
|
||||
return d, false
|
||||
}
|
||||
if n == 0 {
|
||||
errs["meeting_id"] = []string{"The selected meeting_id is invalid."}
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
writeValidation(w, errs)
|
||||
return d, false
|
||||
@@ -287,19 +304,23 @@ func (d materialDraft) identity(q *gorm.DB) *gorm.DB {
|
||||
|
||||
// apply writes the draft onto m inside tx. published_at: absent publishes
|
||||
// a new material now and keeps an existing one as it is; null makes a
|
||||
// draft; otherwise the sent time. tags: absent keeps them; otherwise the
|
||||
// list replaces them, and unknown tags are created.
|
||||
// draft; otherwise the sent time. meeting_id: absent keeps it; null clears
|
||||
// it. tags: absent keeps them; otherwise the list replaces them, and unknown
|
||||
// tags are created.
|
||||
func (d materialDraft) apply(tx *gorm.DB, m *models.Material, created bool, now time.Time) error {
|
||||
in := d.in
|
||||
m.CourseID, m.Title, m.Type = in.CourseID, in.Title, in.Type
|
||||
m.Body, m.Source, m.URL = in.Body, in.Source, in.URL
|
||||
if d.hasMeetingID {
|
||||
m.MeetingID = in.MeetingID
|
||||
}
|
||||
switch {
|
||||
case d.hasPublishedAt:
|
||||
m.PublishedAt = d.publishedAt
|
||||
case created:
|
||||
m.PublishedAt = &now
|
||||
}
|
||||
if err := tx.Omit("Course", "Tags").Save(m).Error; err != nil {
|
||||
if err := tx.Omit("Course", "Meeting", "Tags").Save(m).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Tags == nil {
|
||||
|
||||
Reference in New Issue
Block a user