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 "time"
12
13 "git.secluded.site/go-lunatask"
14 "git.secluded.site/lune/internal/mcp/shared"
15 "github.com/modelcontextprotocol/go-sdk/mcp"
16)
17
18// ShowToolName is the name of the show note tool.
19const ShowToolName = "show_note"
20
21// ShowToolDescription describes the show note tool for LLMs.
22const ShowToolDescription = `Shows metadata for a specific note from Lunatask.
23
24Required:
25- id: Note UUID or lunatask:// deep link
26
27Note: Due to end-to-end encryption, note title and content are not available.
28Only metadata (notebook, date, pinned status, sources) is returned.`
29
30// ShowInput is the input schema for showing a note.
31type ShowInput struct {
32 ID string `json:"id" jsonschema:"required"`
33}
34
35// ShowSource represents a source reference in the output.
36type ShowSource struct {
37 Source string `json:"source"`
38 SourceID string `json:"source_id"`
39}
40
41// ShowOutput is the output schema for showing a note.
42type ShowOutput 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 Sources []ShowSource `json:"sources,omitempty"`
48 CreatedAt string `json:"created_at"`
49 UpdatedAt string `json:"updated_at"`
50}
51
52// HandleShow shows a note's details.
53func (h *Handler) HandleShow(
54 ctx context.Context,
55 _ *mcp.CallToolRequest,
56 input ShowInput,
57) (*mcp.CallToolResult, ShowOutput, error) {
58 _, id, err := lunatask.ParseReference(input.ID)
59 if err != nil {
60 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"), ShowOutput{}, nil
61 }
62
63 note, err := h.client.GetNote(ctx, id)
64 if err != nil {
65 return shared.ErrorResult(err.Error()), ShowOutput{}, nil
66 }
67
68 output := buildShowOutput(note)
69 text := formatNoteShowText(output, h.notebooks)
70
71 return &mcp.CallToolResult{
72 Content: []mcp.Content{&mcp.TextContent{Text: text}},
73 }, output, nil
74}
75
76func buildShowOutput(note *lunatask.Note) ShowOutput {
77 output := ShowOutput{
78 NotebookID: note.NotebookID,
79 Pinned: note.Pinned,
80 CreatedAt: note.CreatedAt.Format(time.RFC3339),
81 UpdatedAt: note.UpdatedAt.Format(time.RFC3339),
82 }
83
84 output.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
85
86 if note.DateOn != nil {
87 s := note.DateOn.Format("2006-01-02")
88 output.DateOn = &s
89 }
90
91 if len(note.Sources) > 0 {
92 output.Sources = make([]ShowSource, 0, len(note.Sources))
93 for _, src := range note.Sources {
94 output.Sources = append(output.Sources, ShowSource{
95 Source: src.Source,
96 SourceID: src.SourceID,
97 })
98 }
99 }
100
101 return output
102}
103
104func formatNoteShowText(output ShowOutput, notebooks []shared.NotebookProvider) string {
105 var builder strings.Builder
106
107 builder.WriteString(fmt.Sprintf("Note: %s\n", output.DeepLink))
108
109 if output.NotebookID != nil {
110 nbName := *output.NotebookID
111 for _, nb := range notebooks {
112 if nb.ID == *output.NotebookID {
113 nbName = nb.Key
114
115 break
116 }
117 }
118
119 builder.WriteString(fmt.Sprintf("Notebook: %s\n", nbName))
120 }
121
122 if output.DateOn != nil {
123 builder.WriteString(fmt.Sprintf("Date: %s\n", *output.DateOn))
124 }
125
126 if output.Pinned {
127 builder.WriteString("Pinned: yes\n")
128 }
129
130 if len(output.Sources) > 0 {
131 builder.WriteString("Sources:\n")
132
133 for _, src := range output.Sources {
134 builder.WriteString(fmt.Sprintf(" - %s: %s\n", src.Source, src.SourceID))
135 }
136 }
137
138 builder.WriteString(fmt.Sprintf("Created: %s\n", output.CreatedAt))
139 builder.WriteString("Updated: " + output.UpdatedAt)
140
141 return builder.String()
142}