list.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package note
  6
  7import (
  8	"context"
  9	"fmt"
 10	"strings"
 11
 12	"git.secluded.site/go-lunatask"
 13	"git.secluded.site/lune/internal/mcp/shared"
 14	"github.com/modelcontextprotocol/go-sdk/mcp"
 15)
 16
 17// ListToolName is the name of the list notes tool.
 18const ListToolName = "list_notes"
 19
 20// ListToolDescription describes the list notes tool for LLMs.
 21const ListToolDescription = `Lists notes from Lunatask.
 22
 23Optional:
 24- notebook_id: Filter by notebook UUID (from lunatask://notebooks)
 25- source: Filter by source identifier
 26- source_id: Filter by source-specific ID
 27
 28Returns note metadata (IDs, dates, pinned status).
 29Use lunatask://note/{id} resource for full note details.
 30
 31Note: Due to end-to-end encryption, note names and content
 32are not available in the list — only metadata is returned.`
 33
 34// ListInput is the input schema for listing notes.
 35type ListInput struct {
 36	NotebookID *string `json:"notebook_id,omitempty"`
 37	Source     *string `json:"source,omitempty"`
 38	SourceID   *string `json:"source_id,omitempty"`
 39}
 40
 41// ListNoteItem represents a note in the list output.
 42type ListNoteItem struct {
 43	DeepLink   string  `json:"deep_link"`
 44	NotebookID *string `json:"notebook_id,omitempty"`
 45	DateOn     *string `json:"date_on,omitempty"`
 46	Pinned     bool    `json:"pinned"`
 47	CreatedAt  string  `json:"created_at"`
 48}
 49
 50// ListOutput is the output schema for listing notes.
 51type ListOutput struct {
 52	Notes []ListNoteItem `json:"notes"`
 53	Count int            `json:"count"`
 54}
 55
 56// HandleList lists notes.
 57func (h *Handler) HandleList(
 58	ctx context.Context,
 59	_ *mcp.CallToolRequest,
 60	input ListInput,
 61) (*mcp.CallToolResult, ListOutput, error) {
 62	if input.NotebookID != nil {
 63		if err := lunatask.ValidateUUID(*input.NotebookID); err != nil {
 64			return shared.ErrorResult("invalid notebook_id: expected UUID"), ListOutput{}, nil
 65		}
 66	}
 67
 68	opts := buildListOptions(input)
 69
 70	notes, err := h.client.ListNotes(ctx, opts)
 71	if err != nil {
 72		return shared.ErrorResult(err.Error()), ListOutput{}, nil
 73	}
 74
 75	if input.NotebookID != nil {
 76		notes = filterByNotebook(notes, *input.NotebookID)
 77	}
 78
 79	items := make([]ListNoteItem, 0, len(notes))
 80
 81	for _, note := range notes {
 82		deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
 83
 84		item := ListNoteItem{
 85			DeepLink:   deepLink,
 86			NotebookID: note.NotebookID,
 87			Pinned:     note.Pinned,
 88			CreatedAt:  note.CreatedAt.Format("2006-01-02"),
 89		}
 90
 91		if note.DateOn != nil {
 92			dateStr := note.DateOn.Format("2006-01-02")
 93			item.DateOn = &dateStr
 94		}
 95
 96		items = append(items, item)
 97	}
 98
 99	output := ListOutput{
100		Notes: items,
101		Count: len(items),
102	}
103
104	text := formatListText(items, h.notebooks)
105
106	return &mcp.CallToolResult{
107		Content: []mcp.Content{&mcp.TextContent{Text: text}},
108	}, output, nil
109}
110
111func buildListOptions(input ListInput) *lunatask.ListNotesOptions {
112	if input.Source == nil && input.SourceID == nil {
113		return nil
114	}
115
116	opts := &lunatask.ListNotesOptions{}
117
118	if input.Source != nil {
119		opts.Source = input.Source
120	}
121
122	if input.SourceID != nil {
123		opts.SourceID = input.SourceID
124	}
125
126	return opts
127}
128
129func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
130	filtered := make([]lunatask.Note, 0, len(notes))
131
132	for _, note := range notes {
133		if note.NotebookID != nil && *note.NotebookID == notebookID {
134			filtered = append(filtered, note)
135		}
136	}
137
138	return filtered
139}
140
141func formatListText(items []ListNoteItem, notebooks []shared.NotebookProvider) string {
142	if len(items) == 0 {
143		return "No notes found"
144	}
145
146	var text strings.Builder
147
148	text.WriteString(fmt.Sprintf("Found %d note(s):\n", len(items)))
149
150	for _, item := range items {
151		text.WriteString("- ")
152		text.WriteString(item.DeepLink)
153
154		var details []string
155
156		if item.NotebookID != nil {
157			nbName := *item.NotebookID
158			for _, nb := range notebooks {
159				if nb.ID == *item.NotebookID {
160					nbName = nb.Key
161
162					break
163				}
164			}
165
166			details = append(details, "notebook: "+nbName)
167		}
168
169		if item.Pinned {
170			details = append(details, "pinned")
171		}
172
173		if len(details) > 0 {
174			text.WriteString(" (")
175			text.WriteString(strings.Join(details, ", "))
176			text.WriteString(")")
177		}
178
179		text.WriteString("\n")
180	}
181
182	text.WriteString("\nUse lunatask://note/{id} resource for full details.")
183
184	return text.String()
185}