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
+112
-11
@@ -128,7 +128,8 @@ func (s *Server) handleMCP(w http.ResponseWriter, r *http.Request) {
|
||||
"serverInfo": map[string]string{"name": "MindSift", "version": mcpServerVersion},
|
||||
"instructions": "MindSift is a personal notes, tasks and lists app. Use list_task_lists and list_tasks " +
|
||||
"to see open tasks, complete_task to check one off, list_lists/get_list for shopping and other " +
|
||||
"lists and check_list_item to tick items. IDs come from the list tools; never guess them.",
|
||||
"lists and check_list_item to tick items. IDs come from the list tools; never guess them. " +
|
||||
"Entries you create or change are marked for the user until they touch them.",
|
||||
}, nil)
|
||||
case "ping":
|
||||
writeRPC(w, req.ID, map[string]interface{}{}, nil)
|
||||
@@ -275,11 +276,31 @@ func toTaskView(t *domain.Thought) taskView {
|
||||
}
|
||||
|
||||
type taskListView struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Permission string `json:"permission"`
|
||||
OpenTasks int `json:"openTasks"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Permission string `json:"permission"`
|
||||
OpenTasks int `json:"openTasks"`
|
||||
Columns []columnView `json:"columns,omitempty"`
|
||||
}
|
||||
|
||||
// columnView: a board column, so an agent knows the valid kanbanStatus values of a task list
|
||||
type columnView struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDone bool `json:"isDone,omitempty"`
|
||||
}
|
||||
|
||||
func toTaskListView(l domain.TaskList) taskListView {
|
||||
cols := make([]columnView, 0, len(l.Columns))
|
||||
for _, c := range l.Columns {
|
||||
cols = append(cols, columnView{ID: c.ID, Name: c.Name, IsDone: c.IsDone})
|
||||
}
|
||||
permission := l.Permission
|
||||
if permission == "" && l.IsOwner {
|
||||
permission = "owner"
|
||||
}
|
||||
return taskListView{ID: l.ID, Title: l.Title, Icon: l.Icon, Permission: permission, OpenTasks: l.TasksCount, Columns: cols}
|
||||
}
|
||||
|
||||
type listView struct {
|
||||
@@ -309,7 +330,7 @@ func toListView(l domain.SharedList) listView {
|
||||
var mcpTools = []mcpTool{
|
||||
{
|
||||
Name: "list_task_lists",
|
||||
Description: "All task lists the user can see, with permission (owner/editor/viewer) and the number of open tasks. Tasks without a list live in the personal inbox list (taskListId empty).",
|
||||
Description: "All task lists the user can see, with permission (owner/editor/viewer), the number of open tasks and the board columns (valid kanbanStatus values). Tasks without a list live in the personal inbox list (taskListId empty).",
|
||||
Scope: storage.ScopeTasksRead,
|
||||
Schema: schema(nil, map[string]interface{}{}),
|
||||
run: func(s *Server, userID string, _ json.RawMessage) (interface{}, error) {
|
||||
@@ -319,11 +340,39 @@ var mcpTools = []mcpTool{
|
||||
}
|
||||
out := make([]taskListView, 0, len(lists))
|
||||
for _, l := range lists {
|
||||
out = append(out, taskListView{ID: l.ID, Title: l.Title, Icon: l.Icon, Permission: l.Permission, OpenTasks: l.TasksCount})
|
||||
out = append(out, toTaskListView(l))
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create_task_list",
|
||||
Description: "Create a personal task list (board with the default columns). Returns it with its id and columns.",
|
||||
Scope: storage.ScopeTasksWrite,
|
||||
Schema: schema([]string{"title"}, map[string]interface{}{
|
||||
"title": prop("string", "Name of the task list"),
|
||||
"icon": prop("string", "One emoji, optional (default 📋)"),
|
||||
}),
|
||||
run: func(s *Server, userID string, args json.RawMessage) (interface{}, error) {
|
||||
var a struct {
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
if err := decodeArgs(args, &a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(a.Title) == "" {
|
||||
return nil, errors.New("'title' darf nicht leer sein")
|
||||
}
|
||||
tl, err := s.store.CreateTaskList(userID, "", "", a.Title, a.Icon, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.broadcastTaskListChange(tl.ID, nil, "TASK_LIST_CREATED")
|
||||
tl.IsOwner = true
|
||||
return toTaskListView(*tl), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_tasks",
|
||||
Description: "Tasks the user can see. Defaults to open tasks across all lists; filter by taskListId, status (open|done|all) or dueBefore (YYYY-MM-DD, inclusive). Sorted: overdue and due first.",
|
||||
@@ -458,6 +507,7 @@ var mcpTools = []mcpTool{
|
||||
return nil, err
|
||||
}
|
||||
a.taskPatch.Assignees, a.taskPatch.Recurrence, a.taskPatch.Subtasks = nil, nil, nil
|
||||
a.taskPatch.byAgent = true
|
||||
t, err := s.updateTask(userID, a.ID, a.taskPatch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -482,13 +532,34 @@ var mcpTools = []mcpTool{
|
||||
return nil, err
|
||||
}
|
||||
done := a.Completed == nil || *a.Completed
|
||||
t, err := s.updateTask(userID, a.ID, taskPatch{IsCompleted: &done})
|
||||
t, err := s.updateTask(userID, a.ID, taskPatch{IsCompleted: &done, byAgent: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toTaskView(t), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete_task",
|
||||
Description: "Delete a task for good, e.g. one created by mistake. Prefer complete_task for work that is done.",
|
||||
Scope: storage.ScopeTasksWrite,
|
||||
Schema: schema([]string{"id"}, map[string]interface{}{"id": prop("string", "Task id")}),
|
||||
run: func(s *Server, userID string, args json.RawMessage) (interface{}, error) {
|
||||
var a struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := decodeArgs(args, &a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.store.GetThoughtByID(userID, a.ID); err != nil {
|
||||
return nil, errors.New("Aufgabe nicht gefunden")
|
||||
}
|
||||
if err := s.deleteTask(userID, a.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"id": a.ID, "deleted": true}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_lists",
|
||||
Description: "Shopping lists, checklists and other shared lists the user can see, with open/total item counts.",
|
||||
@@ -506,6 +577,35 @@ var mcpTools = []mcpTool{
|
||||
return out, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create_list",
|
||||
Description: "Create a list for the user: shopping (items grouped by category), checklist or vacation (packing list).",
|
||||
Scope: storage.ScopeListsWrite,
|
||||
Schema: schema([]string{"title"}, map[string]interface{}{
|
||||
"title": prop("string", "Name of the list"),
|
||||
"icon": prop("string", "One emoji, optional"),
|
||||
"listType": map[string]interface{}{"type": "string", "enum": []string{"shopping", "checklist", "vacation"}, "description": "Default shopping"},
|
||||
}),
|
||||
run: func(s *Server, userID string, args json.RawMessage) (interface{}, error) {
|
||||
var a struct {
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
ListType string `json:"listType"`
|
||||
}
|
||||
if err := decodeArgs(args, &a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(a.Title) == "" {
|
||||
return nil, errors.New("'title' darf nicht leer sein")
|
||||
}
|
||||
l, err := s.store.CreateList(userID, a.Title, a.Icon, a.ListType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.wsHub.BroadcastListEvent(l.ID, "LIST_CREATED", l)
|
||||
return toListView(*l), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_list",
|
||||
Description: "One list with all its items (id, content, category, quantity, isCompleted).",
|
||||
@@ -545,7 +645,7 @@ var mcpTools = []mcpTool{
|
||||
if strings.TrimSpace(a.Content) == "" {
|
||||
return nil, errors.New("'content' darf nicht leer sein")
|
||||
}
|
||||
return s.addListItem(userID, a.ListID, a.Content, a.Category, a.Quantity)
|
||||
return s.addListItem(userID, a.ListID, a.Content, a.Category, a.Quantity, true)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -566,7 +666,7 @@ var mcpTools = []mcpTool{
|
||||
if err := decodeArgs(args, &a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.setListItemDone(userID, a.ListID, a.ItemID, a.Completed == nil || *a.Completed)
|
||||
return s.setListItemDone(userID, a.ListID, a.ItemID, a.Completed == nil || *a.Completed, true)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -598,6 +698,7 @@ var mcpTools = []mcpTool{
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.AgentMark = s.store.SetListItemAgentMark(a.ItemID, true)
|
||||
s.wsHub.BroadcastListEvent(a.ListID, "ITEM_UPDATED", item)
|
||||
return item, nil
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user