feat(mcp): delete tasks, create task lists and lists, agent badge; export/import settings category
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m21s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m21s
New MCP tools delete_task (tasks:write), create_task_list (tasks:write) and create_list (lists:write); list_task_lists now returns the board columns so agents know the valid kanbanStatus values. Task deletion is a shared helper (deleteTask) for REST and MCP. Tasks and list items get agent_mark (migration 12): "created" when an agent adds them via MCP, "edited" when an agent changes them; a change by the user clears it. The new AgentBadge shows "KI" / "KI geändert" in task rows, Kanban cards, the task detail, the inbox and lists. Settings: export and import move out of "Webhooks, KI-Agenten & Export" into their own category "Export & Import". Docs, help, README and changelog updated; v0.29.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8faed7a160
commit
ac068248d7
28 files changed
+441
-65
No files matched your search
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -88,7 +89,10 @@ func TestMCPToolsFollowScopes(t *testing.T) {
|
||||
|
||||
// Read-only task key: lists tasks, cannot complete them
|
||||
readKey, _ := store.CreateAPIKey(claims.UserID, "Reader", []string{"tasks:read"})
|
||||
names := strings.Join(toolNames(func() map[string]interface{} { _, r := rpc(t, router, readKey.RawKey, `{"jsonrpc":"2.0","id":5,"method":"tools/list"}`); return r }()), ",")
|
||||
names := strings.Join(toolNames(func() map[string]interface{} {
|
||||
_, r := rpc(t, router, readKey.RawKey, `{"jsonrpc":"2.0","id":5,"method":"tools/list"}`)
|
||||
return r
|
||||
}()), ",")
|
||||
if !strings.Contains(names, "list_tasks") || strings.Contains(names, "complete_task") || strings.Contains(names, "list_lists") {
|
||||
t.Fatalf("tasks:read key sees the wrong tools: %s", names)
|
||||
}
|
||||
@@ -192,3 +196,82 @@ func TestMCPTasksAndLists(t *testing.T) {
|
||||
t.Fatalf("anonymous MCP call should be 401, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Agents can create task lists and lists and delete tasks; their entries carry a mark until the user
|
||||
// changes them.
|
||||
func TestMCPCreateDeleteAndAgentMark(t *testing.T) {
|
||||
srv, store, token := setupTestServer(t)
|
||||
router := srv.Router()
|
||||
claims, _ := auth.ValidateToken(testJWTSecret, token)
|
||||
key, _ := store.CreateAPIKey(claims.UserID, "Agent", []string{"tasks:read", "tasks:write", "lists:read", "lists:write"})
|
||||
call := func(id int, name, args string) map[string]interface{} {
|
||||
t.Helper()
|
||||
_, res := rpc(t, router, key.RawKey, fmt.Sprintf(`{"jsonrpc":"2.0","id":%d,"method":"tools/call","params":{"name":%q,"arguments":%s}}`, id, name, args))
|
||||
result, isErr := callResult(t, res)
|
||||
if isErr {
|
||||
t.Fatalf("%s failed: %v", name, result)
|
||||
}
|
||||
return result["structuredContent"].(map[string]interface{})
|
||||
}
|
||||
|
||||
tl := call(1, "create_task_list", `{"title":"Agentenliste"}`)
|
||||
tlID, _ := tl["id"].(string)
|
||||
if tlID == "" || tl["permission"] != "owner" || len(tl["columns"].([]interface{})) == 0 {
|
||||
t.Fatalf("create_task_list: %v", tl)
|
||||
}
|
||||
task := call(2, "create_task", `{"content":"Von Claude","taskListId":"`+tlID+`"}`)
|
||||
taskID := task["id"].(string)
|
||||
th, _ := store.GetThoughtByID(claims.UserID, taskID)
|
||||
if th.AgentMark != "created" || th.TaskListID != tlID {
|
||||
t.Fatalf("agent task should be marked created: %+v", th)
|
||||
}
|
||||
// An agent's change keeps "created"; the user's own change removes the mark
|
||||
call(3, "update_task", `{"id":"`+taskID+`","priority":"high"}`)
|
||||
if th, _ = store.GetThoughtByID(claims.UserID, taskID); th.AgentMark != "created" {
|
||||
t.Fatalf("mark after agent edit: %q", th.AgentMark)
|
||||
}
|
||||
notes := "selbst ergänzt"
|
||||
if _, err := srv.updateTask(claims.UserID, taskID, taskPatch{Notes: ¬es}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if th, _ = store.GetThoughtByID(claims.UserID, taskID); th.AgentMark != "" {
|
||||
t.Fatalf("user edit should clear the mark, got %q", th.AgentMark)
|
||||
}
|
||||
call(4, "complete_task", `{"id":"`+taskID+`"}`)
|
||||
if th, _ = store.GetThoughtByID(claims.UserID, taskID); th.AgentMark != "edited" {
|
||||
t.Fatalf("agent edit of a user task should mark edited, got %q", th.AgentMark)
|
||||
}
|
||||
|
||||
call(5, "delete_task", `{"id":"`+taskID+`"}`)
|
||||
if th, err := store.GetThoughtByID(claims.UserID, taskID); err == nil && th != nil {
|
||||
t.Fatalf("task should be gone")
|
||||
}
|
||||
|
||||
l := call(6, "create_list", `{"title":"Packliste","listType":"vacation"}`)
|
||||
listID := l["id"].(string)
|
||||
if l["listType"] != "vacation" || l["role"] != "owner" {
|
||||
t.Fatalf("create_list: %v", l)
|
||||
}
|
||||
item := call(7, "add_list_item", `{"listId":"`+listID+`","content":"Zahnbürste"}`)
|
||||
if item["agentMark"] != "created" {
|
||||
t.Fatalf("agent item should be marked created: %v", item)
|
||||
}
|
||||
items, _ := store.GetListItems(listID)
|
||||
if len(items) != 1 || items[0].AgentMark != "created" {
|
||||
t.Fatalf("stored item mark: %+v", items)
|
||||
}
|
||||
if _, err := srv.setListItemDone(claims.UserID, listID, items[0].ID, true, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if items, _ = store.GetListItems(listID); items[0].AgentMark != "" {
|
||||
t.Fatalf("ticking by the user should clear the mark: %q", items[0].AgentMark)
|
||||
}
|
||||
|
||||
// Other people's tasks cannot be deleted
|
||||
other, _ := store.CreateUser("other@example.com", "hash", "Other", "member")
|
||||
foreign, _ := store.CreateThoughtWithDetails(other.ID, "Fremd", "normal", "manual", "", "", "todo", "")
|
||||
_, res := rpc(t, router, key.RawKey, `{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"delete_task","arguments":{"id":"`+foreign.ID+`"}}}`)
|
||||
if _, isErr := callResult(t, res); !isErr {
|
||||
t.Fatalf("key must not delete foreign tasks")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user