1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package crud
6
7import (
8 "context"
9
10 "git.secluded.site/go-lunatask"
11 "git.secluded.site/lune/internal/mcp/shared"
12 "github.com/google/jsonschema-go/jsonschema"
13 "github.com/modelcontextprotocol/go-sdk/mcp"
14)
15
16// DeleteToolName is the name of the consolidated delete tool.
17const DeleteToolName = "delete"
18
19// DeleteToolDescription describes the delete tool for LLMs.
20const DeleteToolDescription = `Permanently delete an entity from Lunatask.
21This action cannot be undone. The entity and its associations are removed.`
22
23// DeleteToolAnnotations returns hints about tool behavior.
24func DeleteToolAnnotations() *mcp.ToolAnnotations {
25 return &mcp.ToolAnnotations{
26 DestructiveHint: ptr(true),
27 }
28}
29
30func ptr[T any](v T) *T { return &v }
31
32// toAnyStrings converts a slice of string-based types to []any for JSON schema enums.
33func toAnyStrings[T ~string](slice []T) []any {
34 result := make([]any, len(slice))
35 for i, v := range slice {
36 result[i] = string(v)
37 }
38
39 return result
40}
41
42// prioritiesToAny converts priorities to their string representations for JSON schema enums.
43func prioritiesToAny(priorities []lunatask.Priority) []any {
44 result := make([]any, len(priorities))
45 for i := range priorities {
46 result[i] = priorities[i].String()
47 }
48
49 return result
50}
51
52// DeleteInputSchema returns a custom schema with enum constraints.
53func DeleteInputSchema() *jsonschema.Schema {
54 schema, _ := jsonschema.For[DeleteInput](nil)
55
56 schema.Properties["entity"].Enum = []any{
57 EntityTask, EntityNote, EntityPerson,
58 }
59
60 return schema
61}
62
63// DeleteInput is the input schema for the consolidated delete tool.
64type DeleteInput struct {
65 Entity string `json:"entity" jsonschema:"Entity type to delete"`
66 ID string `json:"id" jsonschema:"UUID or lunatask:// deep link"`
67}
68
69// DeleteOutput is the output schema for the consolidated delete tool.
70type DeleteOutput struct {
71 Entity string `json:"entity"`
72 DeepLink string `json:"deep_link"`
73 Success bool `json:"success"`
74}
75
76// HandleDelete deletes an entity based on the entity type.
77func (h *Handler) HandleDelete(
78 ctx context.Context,
79 _ *mcp.CallToolRequest,
80 input DeleteInput,
81) (*mcp.CallToolResult, DeleteOutput, error) {
82 switch input.Entity {
83 case EntityTask:
84 return h.deleteTask(ctx, input)
85 case EntityNote:
86 return h.deleteNote(ctx, input)
87 case EntityPerson:
88 return h.deletePerson(ctx, input)
89 default:
90 return shared.ErrorResult("invalid entity: must be task, note, or person"),
91 DeleteOutput{Entity: input.Entity}, nil
92 }
93}
94
95func (h *Handler) deleteTask(
96 ctx context.Context,
97 input DeleteInput,
98) (*mcp.CallToolResult, DeleteOutput, error) {
99 _, id, err := lunatask.ParseReference(input.ID)
100 if err != nil {
101 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
102 DeleteOutput{Entity: input.Entity}, nil
103 }
104
105 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, id)
106
107 if _, err := h.client.DeleteTask(ctx, id); err != nil {
108 return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
109 }
110
111 return &mcp.CallToolResult{
112 Content: []mcp.Content{&mcp.TextContent{
113 Text: "Task deleted: " + deepLink,
114 }},
115 }, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
116}
117
118func (h *Handler) deleteNote(
119 ctx context.Context,
120 input DeleteInput,
121) (*mcp.CallToolResult, DeleteOutput, error) {
122 _, id, err := lunatask.ParseReference(input.ID)
123 if err != nil {
124 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
125 DeleteOutput{Entity: input.Entity}, nil
126 }
127
128 note, err := h.client.DeleteNote(ctx, id)
129 if err != nil {
130 return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
131 }
132
133 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
134
135 return &mcp.CallToolResult{
136 Content: []mcp.Content{&mcp.TextContent{
137 Text: "Note deleted: " + deepLink,
138 }},
139 }, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
140}
141
142func (h *Handler) deletePerson(
143 ctx context.Context,
144 input DeleteInput,
145) (*mcp.CallToolResult, DeleteOutput, error) {
146 _, id, err := lunatask.ParseReference(input.ID)
147 if err != nil {
148 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
149 DeleteOutput{Entity: input.Entity}, nil
150 }
151
152 person, err := h.client.DeletePerson(ctx, id)
153 if err != nil {
154 return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
155 }
156
157 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
158
159 return &mcp.CallToolResult{
160 Content: []mcp.Content{&mcp.TextContent{
161 Text: "Person deleted: " + deepLink,
162 }},
163 }, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
164}