@@ -167,6 +167,7 @@ var validToolNames = map[string]func(*config.ToolsConfig, bool){
note.UpdateToolName: func(t *config.ToolsConfig, v bool) { t.UpdateNote = v },
note.DeleteToolName: func(t *config.ToolsConfig, v bool) { t.DeleteNote = v },
note.ListToolName: func(t *config.ToolsConfig, v bool) { t.ListNotes = v },
+ note.ShowToolName: func(t *config.ToolsConfig, v bool) { t.ShowNote = v },
person.CreateToolName: func(t *config.ToolsConfig, v bool) { t.CreatePerson = v },
person.UpdateToolName: func(t *config.ToolsConfig, v bool) { t.UpdatePerson = v },
person.DeleteToolName: func(t *config.ToolsConfig, v bool) { t.DeletePerson = v },
@@ -175,7 +176,6 @@ var validToolNames = map[string]func(*config.ToolsConfig, bool){
habit.TrackToolName: func(t *config.ToolsConfig, v bool) { t.TrackHabit = v },
journal.CreateToolName: func(t *config.ToolsConfig, v bool) { t.CreateJournal = v },
// TODO: Add these once implemented:
- // - show_note (ShowNote)
// - show_person (ShowPerson)
// - list_habits (ListHabits)
// - list_areas (ListAreas) - needs config field
@@ -243,6 +243,13 @@ func registerNoteTools(
Description: notetool.ListToolDescription,
}, noteHandler.HandleList)
}
+
+ if tools.ShowNote {
+ mcp.AddTool(mcpServer, &mcp.Tool{
+ Name: notetool.ShowToolName,
+ Description: notetool.ShowToolDescription,
+ }, noteHandler.HandleShow)
+ }
}
func registerPersonTools(
@@ -84,10 +84,12 @@ func (c *MCPConfig) MCPDefaults() {
}
// ApplyDefaults enables all tools if none are explicitly configured.
+// Note: "show" tools and config-based "list" tools are default-disabled
+// because they are fallbacks for agents that don't support MCP resources.
//
//nolint:cyclop // Complexity from repetitive boolean checks; structurally simple.
func (t *ToolsConfig) ApplyDefaults() {
- // If all are false (zero value), enable everything
+ // If all are false (zero value), enable everything except resource fallbacks
if !t.GetTimestamp && !t.CreateTask && !t.UpdateTask && !t.DeleteTask &&
!t.ListTasks && !t.ShowTask && !t.CreateNote && !t.UpdateNote &&
!t.DeleteNote && !t.ListNotes && !t.ShowNote && !t.CreatePerson &&
@@ -98,20 +100,20 @@ func (t *ToolsConfig) ApplyDefaults() {
t.UpdateTask = true
t.DeleteTask = true
t.ListTasks = true
- t.ShowTask = true
+ // ShowTask: default-disabled (fallback for lunatask://task/{id} resource)
t.CreateNote = true
t.UpdateNote = true
t.DeleteNote = true
t.ListNotes = true
- t.ShowNote = true
+ // ShowNote: default-disabled (fallback for lunatask://note/{id} resource)
t.CreatePerson = true
t.UpdatePerson = true
t.DeletePerson = true
t.ListPeople = true
- t.ShowPerson = true
+ // ShowPerson: default-disabled (fallback for lunatask://person/{id} resource)
t.PersonTimeline = true
t.TrackHabit = true
- t.ListHabits = true
+ // ListHabits: default-disabled (fallback for lunatask://habits resource)
t.CreateJournal = true
}
}
@@ -0,0 +1,142 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package note
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "git.secluded.site/go-lunatask"
+ "git.secluded.site/lune/internal/mcp/shared"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// ShowToolName is the name of the show note tool.
+const ShowToolName = "show_note"
+
+// ShowToolDescription describes the show note tool for LLMs.
+const ShowToolDescription = `Shows metadata for a specific note from Lunatask.
+
+Required:
+- id: Note UUID or lunatask:// deep link
+
+Note: Due to end-to-end encryption, note title and content are not available.
+Only metadata (notebook, date, pinned status, sources) is returned.`
+
+// ShowInput is the input schema for showing a note.
+type ShowInput struct {
+ ID string `json:"id" jsonschema:"required"`
+}
+
+// ShowSource represents a source reference in the output.
+type ShowSource struct {
+ Source string `json:"source"`
+ SourceID string `json:"source_id"`
+}
+
+// ShowOutput is the output schema for showing a note.
+type ShowOutput struct {
+ DeepLink string `json:"deep_link"`
+ NotebookID *string `json:"notebook_id,omitempty"`
+ DateOn *string `json:"date_on,omitempty"`
+ Pinned bool `json:"pinned"`
+ Sources []ShowSource `json:"sources,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+// HandleShow shows a note's details.
+func (h *Handler) HandleShow(
+ ctx context.Context,
+ _ *mcp.CallToolRequest,
+ input ShowInput,
+) (*mcp.CallToolResult, ShowOutput, error) {
+ _, id, err := lunatask.ParseReference(input.ID)
+ if err != nil {
+ return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"), ShowOutput{}, nil
+ }
+
+ note, err := h.client.GetNote(ctx, id)
+ if err != nil {
+ return shared.ErrorResult(err.Error()), ShowOutput{}, nil
+ }
+
+ output := buildShowOutput(note)
+ text := formatNoteShowText(output, h.notebooks)
+
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{&mcp.TextContent{Text: text}},
+ }, output, nil
+}
+
+func buildShowOutput(note *lunatask.Note) ShowOutput {
+ output := ShowOutput{
+ NotebookID: note.NotebookID,
+ Pinned: note.Pinned,
+ CreatedAt: note.CreatedAt.Format(time.RFC3339),
+ UpdatedAt: note.UpdatedAt.Format(time.RFC3339),
+ }
+
+ output.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
+
+ if note.DateOn != nil {
+ s := note.DateOn.Format("2006-01-02")
+ output.DateOn = &s
+ }
+
+ if len(note.Sources) > 0 {
+ output.Sources = make([]ShowSource, 0, len(note.Sources))
+ for _, src := range note.Sources {
+ output.Sources = append(output.Sources, ShowSource{
+ Source: src.Source,
+ SourceID: src.SourceID,
+ })
+ }
+ }
+
+ return output
+}
+
+func formatNoteShowText(output ShowOutput, notebooks []shared.NotebookProvider) string {
+ var builder strings.Builder
+
+ builder.WriteString(fmt.Sprintf("Note: %s\n", output.DeepLink))
+
+ if output.NotebookID != nil {
+ nbName := *output.NotebookID
+ for _, nb := range notebooks {
+ if nb.ID == *output.NotebookID {
+ nbName = nb.Key
+
+ break
+ }
+ }
+
+ builder.WriteString(fmt.Sprintf("Notebook: %s\n", nbName))
+ }
+
+ if output.DateOn != nil {
+ builder.WriteString(fmt.Sprintf("Date: %s\n", *output.DateOn))
+ }
+
+ if output.Pinned {
+ builder.WriteString("Pinned: yes\n")
+ }
+
+ if len(output.Sources) > 0 {
+ builder.WriteString("Sources:\n")
+
+ for _, src := range output.Sources {
+ builder.WriteString(fmt.Sprintf(" - %s: %s\n", src.Source, src.SourceID))
+ }
+ }
+
+ builder.WriteString(fmt.Sprintf("Created: %s\n", output.CreatedAt))
+ builder.WriteString("Updated: " + output.UpdatedAt)
+
+ return builder.String()
+}