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 OpenWorldHint: ptr(true),
28 Title: "Delete entity",
29 }
30}
31
32func ptr[T any](v T) *T { return &v }
33
34// toAnyStrings converts a slice of string-based types to []any for JSON schema enums.
35func toAnyStrings[T ~string](slice []T) []any {
36 result := make([]any, len(slice))
37 for i, v := range slice {
38 result[i] = string(v)
39 }
40
41 return result
42}
43
44// prioritiesToAny converts priorities to their string representations for JSON schema enums.
45func prioritiesToAny(priorities []lunatask.Priority) []any {
46 result := make([]any, len(priorities))
47 for i := range priorities {
48 result[i] = priorities[i].String()
49 }
50
51 return result
52}
53
54// DeleteInputSchema returns a custom schema with enum constraints.
55func DeleteInputSchema() *jsonschema.Schema {
56 schema, _ := jsonschema.For[DeleteInput](nil)
57
58 schema.Properties["entity"].Enum = []any{
59 EntityTask, EntityNote, EntityPerson,
60 }
61
62 return schema
63}
64
65// DeleteInput is the input schema for the consolidated delete tool.
66type DeleteInput struct {
67 Entity string `json:"entity" jsonschema:"Entity type to delete"`
68 ID string `json:"id" jsonschema:"UUID or lunatask:// deep link"`
69}
70
71// DeleteOutput is the output schema for the consolidated delete tool.
72type DeleteOutput struct {
73 Entity string `json:"entity"`
74 DeepLink string `json:"deep_link"`
75 Success bool `json:"success"`
76}
77
78// HandleDelete deletes an entity based on the entity type.
79func (h *Handler) HandleDelete(
80 ctx context.Context,
81 _ *mcp.CallToolRequest,
82 input DeleteInput,
83) (*mcp.CallToolResult, DeleteOutput, error) {
84 switch input.Entity {
85 case EntityTask:
86 return h.deleteTask(ctx, input)
87 case EntityNote:
88 return h.deleteNote(ctx, input)
89 case EntityPerson:
90 return h.deletePerson(ctx, input)
91 default:
92 return shared.ErrorResult("invalid entity: must be task, note, or person"),
93 DeleteOutput{Entity: input.Entity}, nil
94 }
95}
96
97func (h *Handler) deleteTask(
98 ctx context.Context,
99 input DeleteInput,
100) (*mcp.CallToolResult, DeleteOutput, error) {
101 _, id, err := lunatask.ParseReference(input.ID)
102 if err != nil {
103 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
104 DeleteOutput{Entity: input.Entity}, nil
105 }
106
107 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, id)
108
109 if _, err := h.client.DeleteTask(ctx, id); err != nil {
110 return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
111 }
112
113 return &mcp.CallToolResult{
114 Content: []mcp.Content{&mcp.TextContent{
115 Text: "Task deleted: " + deepLink,
116 }},
117 }, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
118}
119
120func (h *Handler) deleteNote(
121 ctx context.Context,
122 input DeleteInput,
123) (*mcp.CallToolResult, DeleteOutput, error) {
124 _, id, err := lunatask.ParseReference(input.ID)
125 if err != nil {
126 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
127 DeleteOutput{Entity: input.Entity}, nil
128 }
129
130 note, err := h.client.DeleteNote(ctx, id)
131 if err != nil {
132 return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
133 }
134
135 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
136
137 return &mcp.CallToolResult{
138 Content: []mcp.Content{&mcp.TextContent{
139 Text: "Note deleted: " + deepLink,
140 }},
141 }, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
142}
143
144func (h *Handler) deletePerson(
145 ctx context.Context,
146 input DeleteInput,
147) (*mcp.CallToolResult, DeleteOutput, error) {
148 _, id, err := lunatask.ParseReference(input.ID)
149 if err != nil {
150 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
151 DeleteOutput{Entity: input.Entity}, nil
152 }
153
154 person, err := h.client.DeletePerson(ctx, id)
155 if err != nil {
156 return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
157 }
158
159 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
160
161 return &mcp.CallToolResult{
162 Content: []mcp.Content{&mcp.TextContent{
163 Text: "Person deleted: " + deepLink,
164 }},
165 }, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
166}