delete.go

  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}