feat: MCP view of BM answers (absolute links, no material body by default)

- jz.bm.mcp-links on the token twins and the management API: under MCP,
  url and *_url strings starting with a single / get the app base
- material answers under MCP leave the body out unless include_body,
  marking it with body_omitted; write answers never echo it
- jz.bm.manage keys a dispatched call by user
- bm_materials and bm_material student tools
Plain HTTP answers are unchanged.
This commit is contained in:
2026-10-28 14:37:10 +01:00
parent 464c74b80f
commit df42e30034
9 changed files with 301 additions and 46 deletions

View File

@@ -72,7 +72,7 @@ func TestMCPMaterialView(t *testing.T) {
}
t.Run("plain request unchanged", func(t *testing.T) {
got := keysOf(t, materialView(httptest.NewRequest(http.MethodGet, "/", nil), detail))
got := keysOf(t, materialView(detail, viaMCP(httptest.NewRequest(http.MethodGet, "/", nil)), false))
want := keysOf(t, detail)
gotRaw, _ := json.Marshal(got)
wantRaw, _ := json.Marshal(want)
@@ -82,7 +82,7 @@ func TestMCPMaterialView(t *testing.T) {
})
t.Run("MCP without include_body", func(t *testing.T) {
got := keysOf(t, materialView(mcpRequest(http.MethodGet, "/"), detail))
got := keysOf(t, materialView(detail, viaMCP(mcpRequest(http.MethodGet, "/")), includeBody(mcpRequest(http.MethodGet, "/"))))
if _, ok := got["body"]; ok {
t.Errorf("body present: %v", got)
}
@@ -107,7 +107,7 @@ func TestMCPMaterialView(t *testing.T) {
t.Run("MCP without include_body, empty body", func(t *testing.T) {
empty := detail
empty.Body = nil
got := keysOf(t, materialView(mcpRequest(http.MethodGet, "/"), empty))
got := keysOf(t, materialView(empty, true, false))
if _, ok := got["body"]; ok {
t.Errorf("body present: %v", got)
}
@@ -117,7 +117,7 @@ func TestMCPMaterialView(t *testing.T) {
})
t.Run("MCP with include_body", func(t *testing.T) {
got := keysOf(t, materialView(mcpRequest(http.MethodGet, "/?include_body=true"), detail))
got := keysOf(t, materialView(detail, true, includeBody(mcpRequest(http.MethodGet, "/?include_body=true"))))
if got["body"] != body {
t.Errorf("body = %v, want %q", got["body"], body)
}
@@ -129,7 +129,7 @@ func TestMCPMaterialView(t *testing.T) {
managed := managedMaterialJSON{ID: 3, Title: "T", Type: "notatka", Body: &body, Tags: []tagJSON{}}
t.Run("managed plain unchanged", func(t *testing.T) {
got := keysOf(t, managedMaterialView(httptest.NewRequest(http.MethodGet, "/", nil), managed, false))
got := keysOf(t, managedMaterialView(managed, false, false))
if got["body"] != body {
t.Errorf("plain managed body = %v", got["body"])
}
@@ -139,23 +139,24 @@ func TestMCPMaterialView(t *testing.T) {
})
t.Run("managed MCP read", func(t *testing.T) {
got := keysOf(t, managedMaterialView(mcpRequest(http.MethodGet, "/"), managed, false))
got := keysOf(t, managedMaterialView(managed, true, false))
if _, ok := got["body"]; ok || got["body_omitted"] != true {
t.Errorf("managed MCP read = %v, want no body and body_omitted", got)
}
got = keysOf(t, managedMaterialView(mcpRequest(http.MethodGet, "/?include_body=1"), managed, false))
got = keysOf(t, managedMaterialView(managed, true, includeBody(mcpRequest(http.MethodGet, "/?include_body=1"))))
if got["body"] != body {
t.Errorf("managed MCP read with include_body = %v", got)
}
})
// A write answer passes withBody false whatever the request asks.
t.Run("managed MCP write never echoes the body", func(t *testing.T) {
got := keysOf(t, managedMaterialView(mcpRequest(http.MethodPost, "/?include_body=true"), managed, true))
got := keysOf(t, managedMaterialView(managed, true, false))
if _, ok := got["body"]; ok || got["body_omitted"] != true {
t.Errorf("managed MCP write = %v, want no body and body_omitted", got)
}
d := managedMaterialDetailJSON{managedMaterialJSON: managed, Files: []fileJSON{}}
got = keysOf(t, managedMaterialDetailView(mcpRequest(http.MethodPost, "/?include_body=true"), d, true))
got = keysOf(t, managedMaterialDetailView(d, true, false))
if _, ok := got["body"]; ok || got["body_omitted"] != true {
t.Errorf("managed MCP write detail = %v, want no body and body_omitted", got)
}