feat: attach material files over MCP as base64
- storeMaterialFile: the one attach.Store call of the management API,
shared by the multipart route and the new inline route
- POST /api/v1/bm/materials/{id}/files/inline: JSON {name, mime?,
content_base64}, at most 3000000 bytes decoded, body.limit 4300000
- bm_manage_attach_material_file and bm_manage_detach_material_file,
both warning that material files are public to anyone with the link
This commit is contained in:
65
mcp_test.go
65
mcp_test.go
@@ -18,6 +18,7 @@ import (
|
||||
"git.golem15.com/golem15/summercms/modules/pier"
|
||||
"git.golem15.com/golem15/summercms/modules/surf"
|
||||
bm "git.golem15.com/jakub/sm-bm-plugin"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/controllers/api"
|
||||
"git.golem15.com/jakub/sm-bm-plugin/models"
|
||||
)
|
||||
|
||||
@@ -52,6 +53,7 @@ var pinnedTools = []string{
|
||||
"bm_courses",
|
||||
"bm_dashboard",
|
||||
"bm_deadlines",
|
||||
"bm_manage_attach_material_file",
|
||||
"bm_manage_create_class_session",
|
||||
"bm_manage_create_course",
|
||||
"bm_manage_create_deadline",
|
||||
@@ -61,6 +63,7 @@ var pinnedTools = []string{
|
||||
"bm_manage_delete_deadline",
|
||||
"bm_manage_delete_material",
|
||||
"bm_manage_delete_reading",
|
||||
"bm_manage_detach_material_file",
|
||||
"bm_manage_list_class_sessions",
|
||||
"bm_manage_list_courses",
|
||||
"bm_manage_list_deadlines",
|
||||
@@ -306,6 +309,13 @@ func testManageTools(t *testing.T, tools []pact.MCPTool) {
|
||||
}
|
||||
}
|
||||
fields := bodyFields(tool.Input)
|
||||
if segment == "materials" && strings.Contains(rest, "/files") {
|
||||
testFileTool(t, tool, fields)
|
||||
if tool.Method == http.MethodDelete {
|
||||
deletes[segment] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if segment == "materials" {
|
||||
if !strings.Contains(tool.Description, bm.MCPMaterialPrivacy) {
|
||||
t.Errorf("%s: description lacks the material privacy sentence", tool.Name)
|
||||
@@ -353,8 +363,8 @@ func testManageTools(t *testing.T, tools []pact.MCPTool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if manage != 28 {
|
||||
t.Errorf("%d management tools, want 28", manage)
|
||||
if manage != manageToolCount {
|
||||
t.Errorf("%d management tools, want %d", manage, manageToolCount)
|
||||
}
|
||||
want := map[string]bool{"class-sessions": true, "readings": true, "deadlines": true, "materials": true}
|
||||
if !reflect.DeepEqual(deletes, want) {
|
||||
@@ -362,6 +372,57 @@ func testManageTools(t *testing.T, tools []pact.MCPTool) {
|
||||
}
|
||||
}
|
||||
|
||||
// manageToolCount is the number of management tools: 28 record and
|
||||
// material tools plus the material file tools.
|
||||
const manageToolCount = 30
|
||||
|
||||
// fileBodyKeys are the body keys of each material file tool, exactly.
|
||||
var fileBodyKeys = map[string]map[string]bool{
|
||||
"manage_attach_material_file": {"name": false, "mime": true, "content_base64": false},
|
||||
"manage_detach_material_file": {},
|
||||
}
|
||||
|
||||
// testFileTool (D-10, D-11, D-17) checks one material file tool: its body
|
||||
// keys, the public-file warning and the privacy sentence, and that the
|
||||
// detach tool is destructive.
|
||||
func testFileTool(t *testing.T, tool pact.MCPTool, fields map[string]bool) {
|
||||
t.Helper()
|
||||
want, ok := fileBodyKeys[tool.Name]
|
||||
if !ok {
|
||||
t.Errorf("%s: not a known material file tool", tool.Name)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(fields, want) {
|
||||
t.Errorf("%s: body keys (optional) %v, want %v", tool.Name, fields, want)
|
||||
}
|
||||
if !strings.Contains(tool.Description, api.MaterialFileWarning) {
|
||||
t.Errorf("%s: description lacks the public-file warning", tool.Name)
|
||||
}
|
||||
if !strings.Contains(tool.Description, bm.MCPMaterialPrivacy) {
|
||||
t.Errorf("%s: description lacks the material privacy sentence", tool.Name)
|
||||
}
|
||||
if !strings.Contains(tool.Description, "Needs scope write and the jz.bm.access_materials permission") {
|
||||
t.Errorf("%s: description does not name scope write and jz.bm.access_materials", tool.Name)
|
||||
}
|
||||
rt := reflect.TypeOf(tool.Input)
|
||||
idPath := false
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
if rt.Field(i).Tag.Get("path") == "id" {
|
||||
idPath = true
|
||||
}
|
||||
}
|
||||
if !idPath {
|
||||
t.Errorf("%s: no id path field", tool.Name)
|
||||
}
|
||||
if tool.Name == "manage_detach_material_file" {
|
||||
if tool.Method != http.MethodDelete || (tool.Destructive != nil && !*tool.Destructive) {
|
||||
t.Errorf("%s is not destructive (method %s)", tool.Name, tool.Method)
|
||||
}
|
||||
} else if tool.Method == http.MethodDelete || (tool.Destructive != nil && *tool.Destructive) {
|
||||
t.Errorf("%s is destructive", tool.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// studentRoutes are the routes mountStudent registers, in order.
|
||||
var studentRoutes = []string{
|
||||
"GET /dashboard",
|
||||
|
||||
Reference in New Issue
Block a user