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}