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		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}