refactor(mcp)!: consolidate 23 tools into 7

Amolith created

BREAKING CHANGE: Tool names have changed completely.

Before (23 tools): create_task, update_task, delete_task, list_tasks,
show_task, create_note, update_note, delete_note, list_notes, show_note,
create_person, update_person, delete_person, list_people, show_person,
add_timeline_note, track_habit, list_habits, list_areas, list_goals,
list_notebooks, add_journal_entry, get_timestamp

After (7 tools):
- create: entity={task, note, person, journal}
- update: entity={task, note, person}
- delete: entity={task, note, person}
- query: entity={task, note, person, area, goal, notebook, habit}
- track_habit: standalone action
- add_timeline_note: standalone action (extracted to timeline/)
- get_timestamp: standalone utility

Benefits:
- Clearer action-based semantics for LLM tool selection
- Reduced cognitive load (7 vs 23 tools)
- Consistent entity parameter across CRUD operations
- Config-based entities (area, goal, notebook, habit) now queryable

Also fixes track_habit to accept config keys, UUIDs, or deep links
(previously only accepted UUIDs).

Assisted-by: Claude Opus 4.5 via Crush

Change summary

AGENTS.md                              |  12 
cmd/mcp/mcp.go                         |  42 -
cmd/mcp/server.go                      | 217 ------
internal/config/config.go              |  66 -
internal/mcp/tools/area/list.go        |  96 --
internal/mcp/tools/crud/create.go      | 297 ++++++++
internal/mcp/tools/crud/delete.go      |  68 +
internal/mcp/tools/crud/query.go       | 912 +++++++++++++++++++++++++++
internal/mcp/tools/crud/update.go      | 277 ++++++++
internal/mcp/tools/goal/list.go        | 128 ---
internal/mcp/tools/habit/list.go       |  82 --
internal/mcp/tools/habit/track.go      |  29 
internal/mcp/tools/journal/create.go   | 102 ---
internal/mcp/tools/note/create.go      | 113 ---
internal/mcp/tools/note/delete.go      |  63 -
internal/mcp/tools/note/list.go        | 185 -----
internal/mcp/tools/note/show.go        | 142 ----
internal/mcp/tools/note/update.go      | 110 ---
internal/mcp/tools/notebook/list.go    |  94 --
internal/mcp/tools/person/create.go    |  99 ---
internal/mcp/tools/person/delete.go    |  63 -
internal/mcp/tools/person/list.go      | 138 ----
internal/mcp/tools/person/show.go      | 121 ---
internal/mcp/tools/person/update.go    |  98 ---
internal/mcp/tools/task/create.go      | 239 -------
internal/mcp/tools/task/delete.go      |  59 -
internal/mcp/tools/task/list.go        | 158 ----
internal/mcp/tools/task/show.go        | 162 ----
internal/mcp/tools/task/update.go      | 244 -------
internal/mcp/tools/timeline/handler.go |  50 
30 files changed, 1,593 insertions(+), 2,873 deletions(-)

Detailed changes

AGENTS.md 🔗

@@ -57,7 +57,7 @@ internal/
   validate/          → Input validation (delegates to go-lunatask Parse* functions)
   mcp/
     shared/          → Provider types (AreaProvider, HabitProvider) and error helpers
-    tools/           → MCP tool handlers (task/, note/, person/, habit/, journal/, timestamp/)
+    tools/           → MCP tool handlers (crud/, habit/, timeline/, timestamp/)
     resources/       → MCP resource handlers (areas/, habits/, notebooks/, task/, note/, person/)
 ```
 
@@ -103,13 +103,21 @@ integration. Architecture:
 - **Shared** (`internal/mcp/shared/`): Provider types decouple config from MCP
   handlers; `ErrorResult()` returns structured tool errors.
 
+**Consolidated tools** (7 total):
+
+- **CRUD tools** (`crud/`): `create`, `update`, `delete`, `query` — each accepts
+  an `entity` field to specify the type (task, note, person, journal, area, etc.)
+- **Action tools**: `track_habit` (habit/), `add_timeline_note` (timeline/),
+  `get_timestamp` (timestamp/)
+
 **Tool handler pattern**: Tools use `mcp.AddTool()` with typed input structs.
 Validation happens in `parse*Input()` functions returning `*mcp.CallToolResult`
 for errors (not Go errors). Parse functions call `lunatask.Parse*()` for enums
 and `dateutil.Parse()` for dates.
 
 **Config-driven tools**: `cfg.MCP.Tools.*` booleans enable/disable individual
-tools. All default to enabled via `cfg.MCP.MCPDefaults()`.
+tools. All default to enabled via `cfg.MCP.MCPDefaults()` (except `query`, which
+is a fallback for agents without MCP resource support).
 
 ## Core dependencies
 

cmd/mcp/mcp.go 🔗

@@ -11,15 +11,9 @@ import (
 
 	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/mcp/tools/area"
 	"git.secluded.site/lune/internal/mcp/tools/crud"
-	"git.secluded.site/lune/internal/mcp/tools/goal"
 	"git.secluded.site/lune/internal/mcp/tools/habit"
-	"git.secluded.site/lune/internal/mcp/tools/journal"
-	"git.secluded.site/lune/internal/mcp/tools/note"
-	"git.secluded.site/lune/internal/mcp/tools/notebook"
-	"git.secluded.site/lune/internal/mcp/tools/person"
-	"git.secluded.site/lune/internal/mcp/tools/task"
+	"git.secluded.site/lune/internal/mcp/tools/timeline"
 	"git.secluded.site/lune/internal/mcp/tools/timestamp"
 	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
 	"github.com/spf13/cobra"
@@ -161,33 +155,13 @@ func resolvePort(cfg *config.Config) int {
 
 // validToolNames maps MCP tool names to their ToolsConfig field setters.
 var validToolNames = map[string]func(*config.ToolsConfig, bool){
-	timestamp.ToolName:      func(t *config.ToolsConfig, v bool) { t.GetTimestamp = v },
-	task.CreateToolName:     func(t *config.ToolsConfig, v bool) { t.CreateTask = v },
-	task.UpdateToolName:     func(t *config.ToolsConfig, v bool) { t.UpdateTask = v },
-	task.DeleteToolName:     func(t *config.ToolsConfig, v bool) { t.DeleteTask = v },
-	task.ListToolName:       func(t *config.ToolsConfig, v bool) { t.ListTasks = v },
-	task.ShowToolName:       func(t *config.ToolsConfig, v bool) { t.ShowTask = v },
-	note.CreateToolName:     func(t *config.ToolsConfig, v bool) { t.CreateNote = v },
-	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 },
-	person.ListToolName:     func(t *config.ToolsConfig, v bool) { t.ListPeople = v },
-	person.TimelineToolName: func(t *config.ToolsConfig, v bool) { t.PersonTimeline = v },
-	person.ShowToolName:     func(t *config.ToolsConfig, v bool) { t.ShowPerson = v },
-	habit.TrackToolName:     func(t *config.ToolsConfig, v bool) { t.TrackHabit = v },
-	habit.ListToolName:      func(t *config.ToolsConfig, v bool) { t.ListHabits = v },
-	notebook.ListToolName:   func(t *config.ToolsConfig, v bool) { t.ListNotebooks = v },
-	area.ListToolName:       func(t *config.ToolsConfig, v bool) { t.ListAreas = v },
-	goal.ListToolName:       func(t *config.ToolsConfig, v bool) { t.ListGoals = v },
-	journal.CreateToolName:  func(t *config.ToolsConfig, v bool) { t.CreateJournal = v },
-	crud.CreateToolName:     func(t *config.ToolsConfig, v bool) { t.Create = v },
-	crud.UpdateToolName:     func(t *config.ToolsConfig, v bool) { t.Update = v },
-	crud.DeleteToolName:     func(t *config.ToolsConfig, v bool) { t.Delete = v },
-	crud.QueryToolName:      func(t *config.ToolsConfig, v bool) { t.Query = v },
+	timestamp.ToolName:  func(t *config.ToolsConfig, v bool) { t.GetTimestamp = v },
+	timeline.ToolName:   func(t *config.ToolsConfig, v bool) { t.AddTimelineNote = v },
+	habit.TrackToolName: func(t *config.ToolsConfig, v bool) { t.TrackHabit = v },
+	crud.CreateToolName: func(t *config.ToolsConfig, v bool) { t.Create = v },
+	crud.UpdateToolName: func(t *config.ToolsConfig, v bool) { t.Update = v },
+	crud.DeleteToolName: func(t *config.ToolsConfig, v bool) { t.Delete = v },
+	crud.QueryToolName:  func(t *config.ToolsConfig, v bool) { t.Query = v },
 }
 
 // resolveTools modifies cfg.MCP.Tools based on CLI flags.

cmd/mcp/server.go 🔗

@@ -22,15 +22,9 @@ import (
 	taskrs "git.secluded.site/lune/internal/mcp/resources/task"
 	"git.secluded.site/lune/internal/mcp/resources/tasks"
 	"git.secluded.site/lune/internal/mcp/shared"
-	areatool "git.secluded.site/lune/internal/mcp/tools/area"
 	"git.secluded.site/lune/internal/mcp/tools/crud"
-	goaltool "git.secluded.site/lune/internal/mcp/tools/goal"
 	"git.secluded.site/lune/internal/mcp/tools/habit"
-	"git.secluded.site/lune/internal/mcp/tools/journal"
-	notetool "git.secluded.site/lune/internal/mcp/tools/note"
-	"git.secluded.site/lune/internal/mcp/tools/notebook"
-	persontool "git.secluded.site/lune/internal/mcp/tools/person"
-	"git.secluded.site/lune/internal/mcp/tools/task"
+	"git.secluded.site/lune/internal/mcp/tools/timeline"
 	"git.secluded.site/lune/internal/mcp/tools/timestamp"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 	"github.com/spf13/cobra"
@@ -355,79 +349,23 @@ func registerTools(
 		}, tsHandler.Handle)
 	}
 
-	registerTaskTools(mcpServer, cfg, tools, accessToken, areaProviders)
-	registerNoteTools(mcpServer, tools, accessToken, notebookProviders)
-	registerPersonTools(mcpServer, tools, accessToken)
-	registerHabitTools(mcpServer, tools, accessToken, habitProviders)
-	registerConfigListTools(mcpServer, tools, areaProviders, notebookProviders)
-
-	if tools.CreateJournal {
-		journalHandler := journal.NewHandler(accessToken)
+	if tools.AddTimelineNote {
+		timelineHandler := timeline.NewHandler(accessToken)
 		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        journal.CreateToolName,
-			Description: journal.CreateToolDescription,
-		}, journalHandler.HandleCreate)
+			Name:        timeline.ToolName,
+			Description: timeline.ToolDescription,
+		}, timelineHandler.Handle)
 	}
 
-	registerCRUDTools(mcpServer, cfg, tools, accessToken, areaProviders, habitProviders, notebookProviders)
-}
-
-func registerHabitTools(
-	mcpServer *mcp.Server,
-	tools *config.ToolsConfig,
-	accessToken string,
-	habitProviders []shared.HabitProvider,
-) {
-	if !tools.TrackHabit && !tools.ListHabits {
-		return
-	}
-
-	habitHandler := habit.NewHandler(accessToken, habitProviders)
-
 	if tools.TrackHabit {
+		habitHandler := habit.NewHandler(accessToken, habitProviders)
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        habit.TrackToolName,
 			Description: habit.TrackToolDescription,
 		}, habitHandler.HandleTrack)
 	}
 
-	if tools.ListHabits {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        habit.ListToolName,
-			Description: habit.ListToolDescription,
-		}, habitHandler.HandleList)
-	}
-}
-
-func registerConfigListTools(
-	mcpServer *mcp.Server,
-	tools *config.ToolsConfig,
-	areaProviders []shared.AreaProvider,
-	notebookProviders []shared.NotebookProvider,
-) {
-	if tools.ListNotebooks {
-		notebookHandler := notebook.NewHandler(notebookProviders)
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        notebook.ListToolName,
-			Description: notebook.ListToolDescription,
-		}, notebookHandler.HandleList)
-	}
-
-	if tools.ListAreas {
-		areaHandler := areatool.NewHandler(areaProviders)
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        areatool.ListToolName,
-			Description: areatool.ListToolDescription,
-		}, areaHandler.HandleList)
-	}
-
-	if tools.ListGoals {
-		goalHandler := goaltool.NewHandler(areaProviders)
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        goaltool.ListToolName,
-			Description: goaltool.ListToolDescription,
-		}, goalHandler.HandleList)
-	}
+	registerCRUDTools(mcpServer, cfg, tools, accessToken, areaProviders, habitProviders, notebookProviders)
 }
 
 func registerCRUDTools(
@@ -474,145 +412,6 @@ func registerCRUDTools(
 	}
 }
 
-func registerTaskTools(
-	mcpServer *mcp.Server,
-	cfg *config.Config,
-	tools *config.ToolsConfig,
-	accessToken string,
-	areaProviders []shared.AreaProvider,
-) {
-	taskHandler := task.NewHandler(accessToken, cfg, areaProviders)
-
-	if tools.CreateTask {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        task.CreateToolName,
-			Description: task.CreateToolDescription,
-		}, taskHandler.HandleCreate)
-	}
-
-	if tools.UpdateTask {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        task.UpdateToolName,
-			Description: task.UpdateToolDescription,
-		}, taskHandler.HandleUpdate)
-	}
-
-	if tools.DeleteTask {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        task.DeleteToolName,
-			Description: task.DeleteToolDescription,
-		}, taskHandler.HandleDelete)
-	}
-
-	if tools.ListTasks {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        task.ListToolName,
-			Description: task.ListToolDescription,
-		}, taskHandler.HandleList)
-	}
-
-	if tools.ShowTask {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        task.ShowToolName,
-			Description: task.ShowToolDescription,
-		}, taskHandler.HandleShow)
-	}
-}
-
-func registerNoteTools(
-	mcpServer *mcp.Server,
-	tools *config.ToolsConfig,
-	accessToken string,
-	notebookProviders []shared.NotebookProvider,
-) {
-	noteHandler := notetool.NewHandler(accessToken, notebookProviders)
-
-	if tools.CreateNote {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        notetool.CreateToolName,
-			Description: notetool.CreateToolDescription,
-		}, noteHandler.HandleCreate)
-	}
-
-	if tools.UpdateNote {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        notetool.UpdateToolName,
-			Description: notetool.UpdateToolDescription,
-		}, noteHandler.HandleUpdate)
-	}
-
-	if tools.DeleteNote {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        notetool.DeleteToolName,
-			Description: notetool.DeleteToolDescription,
-		}, noteHandler.HandleDelete)
-	}
-
-	if tools.ListNotes {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        notetool.ListToolName,
-			Description: notetool.ListToolDescription,
-		}, noteHandler.HandleList)
-	}
-
-	if tools.ShowNote {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        notetool.ShowToolName,
-			Description: notetool.ShowToolDescription,
-		}, noteHandler.HandleShow)
-	}
-}
-
-func registerPersonTools(
-	mcpServer *mcp.Server,
-	tools *config.ToolsConfig,
-	accessToken string,
-) {
-	personHandler := persontool.NewHandler(accessToken)
-
-	if tools.CreatePerson {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        persontool.CreateToolName,
-			Description: persontool.CreateToolDescription,
-		}, personHandler.HandleCreate)
-	}
-
-	if tools.UpdatePerson {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        persontool.UpdateToolName,
-			Description: persontool.UpdateToolDescription,
-		}, personHandler.HandleUpdate)
-	}
-
-	if tools.DeletePerson {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        persontool.DeleteToolName,
-			Description: persontool.DeleteToolDescription,
-		}, personHandler.HandleDelete)
-	}
-
-	if tools.ListPeople {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        persontool.ListToolName,
-			Description: persontool.ListToolDescription,
-		}, personHandler.HandleList)
-	}
-
-	if tools.PersonTimeline {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        persontool.TimelineToolName,
-			Description: persontool.TimelineToolDescription,
-		}, personHandler.HandleTimeline)
-	}
-
-	if tools.ShowPerson {
-		mcp.AddTool(mcpServer, &mcp.Tool{
-			Name:        persontool.ShowToolName,
-			Description: persontool.ShowToolDescription,
-		}, personHandler.HandleShow)
-	}
-}
-
 func runStdio(mcpServer *mcp.Server) error {
 	if err := mcpServer.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
 		return fmt.Errorf("stdio server error: %w", err)

internal/config/config.go 🔗

@@ -39,34 +39,9 @@ type MCPConfig struct {
 // ToolsConfig controls which MCP tools are enabled.
 // All tools default to enabled when not explicitly set.
 type ToolsConfig struct {
-	GetTimestamp bool `toml:"get_timestamp"`
-
-	CreateTask bool `toml:"create_task"`
-	UpdateTask bool `toml:"update_task"`
-	DeleteTask bool `toml:"delete_task"`
-	ListTasks  bool `toml:"list_tasks"`
-	ShowTask   bool `toml:"show_task"`
-
-	CreateNote bool `toml:"create_note"`
-	UpdateNote bool `toml:"update_note"`
-	DeleteNote bool `toml:"delete_note"`
-	ListNotes  bool `toml:"list_notes"`
-	ShowNote   bool `toml:"show_note"`
-
-	CreatePerson   bool `toml:"create_person"`
-	UpdatePerson   bool `toml:"update_person"`
-	DeletePerson   bool `toml:"delete_person"`
-	ListPeople     bool `toml:"list_people"`
-	ShowPerson     bool `toml:"show_person"`
-	PersonTimeline bool `toml:"person_timeline"`
-
-	TrackHabit    bool `toml:"track_habit"`
-	ListHabits    bool `toml:"list_habits"`
-	ListNotebooks bool `toml:"list_notebooks"`
-	ListAreas     bool `toml:"list_areas"`
-	ListGoals     bool `toml:"list_goals"`
-
-	CreateJournal bool `toml:"create_journal"`
+	GetTimestamp    bool `toml:"get_timestamp"`
+	AddTimelineNote bool `toml:"add_timeline_note"`
+	TrackHabit      bool `toml:"track_habit"`
 
 	Create bool `toml:"create"` // task, note, person, journal
 	Update bool `toml:"update"` // task, note, person
@@ -92,42 +67,19 @@ 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.
+// Note: Query is default-disabled because it's a fallback for agents
+// that don't support MCP resources.
 func (t *ToolsConfig) ApplyDefaults() {
-	// 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 &&
-		!t.UpdatePerson && !t.DeletePerson && !t.ListPeople && !t.ShowPerson &&
-		!t.PersonTimeline && !t.TrackHabit && !t.ListHabits && !t.ListNotebooks &&
-		!t.ListAreas && !t.ListGoals && !t.CreateJournal &&
+	// If all are false (zero value), enable everything except Query
+	if !t.GetTimestamp && !t.AddTimelineNote && !t.TrackHabit &&
 		!t.Create && !t.Update && !t.Delete && !t.Query {
 		t.GetTimestamp = true
-		t.CreateTask = true
-		t.UpdateTask = true
-		t.DeleteTask = true
-		// ListTasks: default-disabled (fallback for lunatask://tasks/* resources)
-		// ShowTask: default-disabled (fallback for lunatask://task/{id} resource)
-		t.CreateNote = true
-		t.UpdateNote = true
-		t.DeleteNote = true
-		// ListNotes: default-disabled (fallback for lunatask://notes/* resources)
-		// ShowNote: default-disabled (fallback for lunatask://note/{id} resource)
-		t.CreatePerson = true
-		t.UpdatePerson = true
-		t.DeletePerson = true
-		// ListPeople: default-disabled (fallback for lunatask://people/* resources)
-		// ShowPerson: default-disabled (fallback for lunatask://person/{id} resource)
-		t.PersonTimeline = true
+		t.AddTimelineNote = true
 		t.TrackHabit = true
-		// ListHabits: default-disabled (fallback for lunatask://habits resource)
-		t.CreateJournal = true
 		t.Create = true
 		t.Update = true
 		t.Delete = true
+		// Query: default-disabled (fallback for agents without resource support)
 	}
 }
 

internal/mcp/tools/area/list.go 🔗

@@ -1,96 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package area provides MCP tools for area operations.
-package area
-
-import (
-	"context"
-	"fmt"
-	"strings"
-
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// ListToolName is the name of the list areas tool.
-const ListToolName = "list_areas"
-
-// ListToolDescription describes the list areas tool for LLMs.
-const ListToolDescription = `Lists configured areas. Fallback for lunatask://areas resource.
-
-Returns area metadata (id, name, key, workflow) from config.
-Use the lunatask://areas resource if your client supports MCP resources.`
-
-// ListInput is the input schema for listing areas.
-type ListInput struct{}
-
-// Summary represents an area in the list output.
-type Summary struct {
-	ID       string `json:"id"`
-	Name     string `json:"name"`
-	Key      string `json:"key"`
-	Workflow string `json:"workflow"`
-}
-
-// ListOutput is the output schema for listing areas.
-type ListOutput struct {
-	Areas []Summary `json:"areas"`
-	Count int       `json:"count"`
-}
-
-// Handler handles area tool requests.
-type Handler struct {
-	areas []shared.AreaProvider
-}
-
-// NewHandler creates a new area tool handler.
-func NewHandler(areas []shared.AreaProvider) *Handler {
-	return &Handler{areas: areas}
-}
-
-// HandleList lists configured areas.
-func (h *Handler) HandleList(
-	_ context.Context,
-	_ *mcp.CallToolRequest,
-	_ ListInput,
-) (*mcp.CallToolResult, ListOutput, error) {
-	summaries := make([]Summary, 0, len(h.areas))
-
-	for _, area := range h.areas {
-		summaries = append(summaries, Summary{
-			ID:       area.ID,
-			Name:     area.Name,
-			Key:      area.Key,
-			Workflow: string(area.Workflow),
-		})
-	}
-
-	output := ListOutput{
-		Areas: summaries,
-		Count: len(summaries),
-	}
-
-	text := formatListText(summaries)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-func formatListText(areas []Summary) string {
-	if len(areas) == 0 {
-		return "No areas configured"
-	}
-
-	var text strings.Builder
-
-	text.WriteString(fmt.Sprintf("Found %d area(s):\n", len(areas)))
-
-	for _, a := range areas {
-		text.WriteString(fmt.Sprintf("- %s: %s (%s, workflow: %s)\n", a.Key, a.Name, a.ID, a.Workflow))
-	}
-
-	return text.String()
-}

internal/mcp/tools/crud/create.go 🔗

@@ -10,7 +10,9 @@ import (
 
 	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/config"
+	"git.secluded.site/lune/internal/dateutil"
 	"git.secluded.site/lune/internal/mcp/shared"
+	"git.secluded.site/lune/internal/validate"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
@@ -74,7 +76,7 @@ Returns the created entity's ID and deep link.`
 
 // CreateInput is the input schema for the consolidated create tool.
 type CreateInput struct {
-	Entity string `json:"entity" jsonschema:"required,enum=task,enum=note,enum=person,enum=journal"`
+	Entity string `json:"entity" jsonschema:"required"`
 
 	// Common fields
 	Name     *string `json:"name,omitempty"`
@@ -160,34 +162,303 @@ func (h *Handler) HandleCreate(
 	}
 }
 
+// parsedTaskCreateInput holds validated and parsed task create input fields.
+type parsedTaskCreateInput struct {
+	Name        string
+	AreaID      string
+	GoalID      *string
+	Status      *lunatask.TaskStatus
+	Note        *string
+	Priority    *lunatask.Priority
+	Estimate    *int
+	Motivation  *lunatask.Motivation
+	Important   *bool
+	Urgent      *bool
+	ScheduledOn *lunatask.Date
+}
+
 func (h *Handler) createTask(
-	_ context.Context,
+	ctx context.Context,
 	input CreateInput,
 ) (*mcp.CallToolResult, CreateOutput, error) {
-	return shared.ErrorResult("task creation not yet implemented"),
-		CreateOutput{Entity: input.Entity}, nil
+	parsed, errResult := h.parseTaskCreateInput(input)
+	if errResult != nil {
+		return errResult, CreateOutput{Entity: input.Entity}, nil
+	}
+
+	builder := h.client.NewTask(parsed.Name)
+	applyToTaskBuilder(builder, parsed)
+
+	task, err := builder.Create(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), CreateOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Task created: " + deepLink,
+		}},
+	}, CreateOutput{Entity: input.Entity, DeepLink: deepLink, ID: task.ID}, nil
+}
+
+//nolint:cyclop,funlen
+func (h *Handler) parseTaskCreateInput(input CreateInput) (*parsedTaskCreateInput, *mcp.CallToolResult) {
+	if input.Name == nil || *input.Name == "" {
+		return nil, shared.ErrorResult("name is required for task creation")
+	}
+
+	if input.AreaID == nil || *input.AreaID == "" {
+		return nil, shared.ErrorResult("area_id is required for task creation")
+	}
+
+	parsed := &parsedTaskCreateInput{
+		Name:      *input.Name,
+		Note:      input.Note,
+		Estimate:  input.Estimate,
+		Important: input.Important,
+		Urgent:    input.Urgent,
+	}
+
+	areaID, err := validate.AreaRef(h.cfg, *input.AreaID)
+	if err != nil {
+		return nil, shared.ErrorResult(err.Error())
+	}
+
+	parsed.AreaID = areaID
+
+	if input.GoalID != nil {
+		goalID, err := validate.GoalRef(h.cfg, parsed.AreaID, *input.GoalID)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.GoalID = &goalID
+	}
+
+	if input.Estimate != nil {
+		if err := shared.ValidateEstimate(*input.Estimate); err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+	}
+
+	if input.Status != nil {
+		status, err := lunatask.ParseTaskStatus(*input.Status)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.Status = &status
+	}
+
+	if input.Priority != nil {
+		priority, err := lunatask.ParsePriority(*input.Priority)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.Priority = &priority
+	}
+
+	if input.Motivation != nil {
+		motivation, err := lunatask.ParseMotivation(*input.Motivation)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.Motivation = &motivation
+	}
+
+	if input.ScheduledOn != nil {
+		date, err := dateutil.Parse(*input.ScheduledOn)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.ScheduledOn = &date
+	}
+
+	return parsed, nil
+}
+
+//nolint:cyclop
+func applyToTaskBuilder(builder *lunatask.TaskBuilder, parsed *parsedTaskCreateInput) {
+	builder.InArea(parsed.AreaID)
+
+	if parsed.GoalID != nil {
+		builder.InGoal(*parsed.GoalID)
+	}
+
+	if parsed.Status != nil {
+		builder.WithStatus(*parsed.Status)
+	}
+
+	if parsed.Note != nil {
+		builder.WithNote(*parsed.Note)
+	}
+
+	if parsed.Priority != nil {
+		builder.Priority(*parsed.Priority)
+	}
+
+	if parsed.Estimate != nil {
+		builder.WithEstimate(*parsed.Estimate)
+	}
+
+	if parsed.Motivation != nil {
+		builder.WithMotivation(*parsed.Motivation)
+	}
+
+	if parsed.Important != nil {
+		if *parsed.Important {
+			builder.Important()
+		} else {
+			builder.NotImportant()
+		}
+	}
+
+	if parsed.Urgent != nil {
+		if *parsed.Urgent {
+			builder.Urgent()
+		} else {
+			builder.NotUrgent()
+		}
+	}
+
+	if parsed.ScheduledOn != nil {
+		builder.ScheduledOn(*parsed.ScheduledOn)
+	}
 }
 
 func (h *Handler) createNote(
-	_ context.Context,
+	ctx context.Context,
 	input CreateInput,
 ) (*mcp.CallToolResult, CreateOutput, error) {
-	return shared.ErrorResult("note creation not yet implemented"),
-		CreateOutput{Entity: input.Entity}, nil
+	if input.NotebookID != nil {
+		if err := lunatask.ValidateUUID(*input.NotebookID); err != nil {
+			return shared.ErrorResult("invalid notebook_id: expected UUID"),
+				CreateOutput{Entity: input.Entity}, nil
+		}
+	}
+
+	builder := h.client.NewNote()
+
+	if input.Name != nil {
+		builder.WithName(*input.Name)
+	}
+
+	if input.NotebookID != nil {
+		builder.InNotebook(*input.NotebookID)
+	}
+
+	if input.Content != nil {
+		builder.WithContent(*input.Content)
+	}
+
+	if input.Source != nil && input.SourceID != nil {
+		builder.FromSource(*input.Source, *input.SourceID)
+	}
+
+	note, err := builder.Create(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), CreateOutput{Entity: input.Entity}, nil
+	}
+
+	if note == nil {
+		return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{
+				Text: "Note already exists (duplicate source)",
+			}},
+		}, CreateOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Note created: " + deepLink,
+		}},
+	}, CreateOutput{Entity: input.Entity, DeepLink: deepLink, ID: note.ID}, nil
 }
 
 func (h *Handler) createPerson(
-	_ context.Context,
+	ctx context.Context,
 	input CreateInput,
 ) (*mcp.CallToolResult, CreateOutput, error) {
-	return shared.ErrorResult("person creation not yet implemented"),
-		CreateOutput{Entity: input.Entity}, nil
+	if input.FirstName == nil || *input.FirstName == "" {
+		return shared.ErrorResult("first_name is required for person creation"),
+			CreateOutput{Entity: input.Entity}, nil
+	}
+
+	lastName := ""
+	if input.LastName != nil {
+		lastName = *input.LastName
+	}
+
+	builder := h.client.NewPerson(*input.FirstName, lastName)
+
+	if input.Relationship != nil {
+		rel, err := lunatask.ParseRelationshipStrength(*input.Relationship)
+		if err != nil {
+			return shared.ErrorResult(err.Error()), CreateOutput{Entity: input.Entity}, nil
+		}
+
+		builder.WithRelationshipStrength(rel)
+	}
+
+	if input.Source != nil && input.SourceID != nil {
+		builder.FromSource(*input.Source, *input.SourceID)
+	}
+
+	person, err := builder.Create(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), CreateOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Person created: " + deepLink,
+		}},
+	}, CreateOutput{Entity: input.Entity, DeepLink: deepLink, ID: person.ID}, nil
 }
 
 func (h *Handler) createJournal(
-	_ context.Context,
+	ctx context.Context,
 	input CreateInput,
 ) (*mcp.CallToolResult, CreateOutput, error) {
-	return shared.ErrorResult("journal creation not yet implemented"),
-		CreateOutput{Entity: input.Entity}, nil
+	dateStr := ""
+	if input.Date != nil {
+		dateStr = *input.Date
+	}
+
+	date, err := dateutil.Parse(dateStr)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), CreateOutput{Entity: input.Entity}, nil
+	}
+
+	builder := h.client.NewJournalEntry(date)
+
+	if input.Content != nil {
+		builder.WithContent(*input.Content)
+	}
+
+	if input.Name != nil {
+		builder.WithName(*input.Name)
+	}
+
+	entry, err := builder.Create(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), CreateOutput{Entity: input.Entity}, nil
+	}
+
+	formattedDate := date.Format("2006-01-02")
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Journal entry created for " + formattedDate,
+		}},
+	}, CreateOutput{Entity: input.Entity, ID: entry.ID}, nil
 }

internal/mcp/tools/crud/delete.go 🔗

@@ -7,6 +7,7 @@ package crud
 import (
 	"context"
 
+	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/mcp/shared"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
@@ -27,7 +28,7 @@ Returns confirmation of deletion with the entity's deep link.`
 
 // DeleteInput is the input schema for the consolidated delete tool.
 type DeleteInput struct {
-	Entity string `json:"entity" jsonschema:"required,enum=task,enum=note,enum=person"`
+	Entity string `json:"entity" jsonschema:"required"`
 	ID     string `json:"id"     jsonschema:"required"`
 }
 
@@ -58,25 +59,72 @@ func (h *Handler) HandleDelete(
 }
 
 func (h *Handler) deleteTask(
-	_ context.Context,
+	ctx context.Context,
 	input DeleteInput,
 ) (*mcp.CallToolResult, DeleteOutput, error) {
-	return shared.ErrorResult("task deletion not yet implemented"),
-		DeleteOutput{Entity: input.Entity}, nil
+	_, id, err := lunatask.ParseReference(input.ID)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			DeleteOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, id)
+
+	if _, err := h.client.DeleteTask(ctx, id); err != nil {
+		return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
+	}
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Task deleted: " + deepLink,
+		}},
+	}, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
 }
 
 func (h *Handler) deleteNote(
-	_ context.Context,
+	ctx context.Context,
 	input DeleteInput,
 ) (*mcp.CallToolResult, DeleteOutput, error) {
-	return shared.ErrorResult("note deletion not yet implemented"),
-		DeleteOutput{Entity: input.Entity}, nil
+	_, id, err := lunatask.ParseReference(input.ID)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			DeleteOutput{Entity: input.Entity}, nil
+	}
+
+	note, err := h.client.DeleteNote(ctx, id)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Note deleted: " + deepLink,
+		}},
+	}, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
 }
 
 func (h *Handler) deletePerson(
-	_ context.Context,
+	ctx context.Context,
 	input DeleteInput,
 ) (*mcp.CallToolResult, DeleteOutput, error) {
-	return shared.ErrorResult("person deletion not yet implemented"),
-		DeleteOutput{Entity: input.Entity}, nil
+	_, id, err := lunatask.ParseReference(input.ID)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			DeleteOutput{Entity: input.Entity}, nil
+	}
+
+	person, err := h.client.DeletePerson(ctx, id)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), DeleteOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Person deleted: " + deepLink,
+		}},
+	}, DeleteOutput{Entity: input.Entity, DeepLink: deepLink, Success: true}, nil
 }

internal/mcp/tools/crud/query.go 🔗

@@ -6,7 +6,11 @@ package crud
 
 import (
 	"context"
+	"fmt"
+	"strings"
+	"time"
 
+	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/mcp/shared"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
@@ -45,11 +49,11 @@ When id is omitted, returns a list with optional filters:
 **area, notebook, habit**: No filters (returns all from config)
 
 Note: Due to end-to-end encryption, names and content are not available
-for list operations — only metadata is returned. Use id parameter for details.`
+for list operations. Only metadata is returned. Use id parameter for details.`
 
 // QueryInput is the input schema for the consolidated query tool.
 type QueryInput struct {
-	Entity string  `json:"entity"       jsonschema:"required,enum=task,enum=note,enum=person,enum=area,enum=goal,enum=notebook,enum=habit"` //nolint:lll // JSON schema enum list
+	Entity string  `json:"entity"       jsonschema:"required"`
 	ID     *string `json:"id,omitempty"`
 
 	// Task/Goal filters
@@ -104,28 +108,703 @@ func (h *Handler) HandleQuery(
 	}
 }
 
+// TaskSummary represents a task in list output.
+type TaskSummary struct {
+	DeepLink    string  `json:"deep_link"`
+	Status      *string `json:"status,omitempty"`
+	Priority    *int    `json:"priority,omitempty"`
+	ScheduledOn *string `json:"scheduled_on,omitempty"`
+	CreatedAt   string  `json:"created_at"`
+	AreaID      *string `json:"area_id,omitempty"`
+	GoalID      *string `json:"goal_id,omitempty"`
+}
+
+// TaskDetail represents detailed task information.
+type TaskDetail struct {
+	DeepLink    string  `json:"deep_link"`
+	Status      *string `json:"status,omitempty"`
+	Priority    *int    `json:"priority,omitempty"`
+	Estimate    *int    `json:"estimate,omitempty"`
+	ScheduledOn *string `json:"scheduled_on,omitempty"`
+	CompletedAt *string `json:"completed_at,omitempty"`
+	CreatedAt   string  `json:"created_at"`
+	UpdatedAt   string  `json:"updated_at"`
+	AreaID      *string `json:"area_id,omitempty"`
+	GoalID      *string `json:"goal_id,omitempty"`
+	Important   *bool   `json:"important,omitempty"`
+	Urgent      *bool   `json:"urgent,omitempty"`
+}
+
 func (h *Handler) queryTask(
-	_ context.Context,
+	ctx context.Context,
 	input QueryInput,
 ) (*mcp.CallToolResult, QueryOutput, error) {
-	return shared.ErrorResult("task query not yet implemented"),
-		QueryOutput{Entity: input.Entity}, nil
+	if input.ID != nil {
+		return h.showTask(ctx, *input.ID)
+	}
+
+	return h.listTasks(ctx, input)
+}
+
+func (h *Handler) showTask(
+	ctx context.Context,
+	id string,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	_, taskID, err := lunatask.ParseReference(id)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			QueryOutput{Entity: EntityTask}, nil
+	}
+
+	task, err := h.client.GetTask(ctx, taskID)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), QueryOutput{Entity: EntityTask}, nil
+	}
+
+	detail := TaskDetail{
+		CreatedAt: task.CreatedAt.Format(time.RFC3339),
+		UpdatedAt: task.UpdatedAt.Format(time.RFC3339),
+		AreaID:    task.AreaID,
+		GoalID:    task.GoalID,
+	}
+
+	detail.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
+
+	if task.Status != nil {
+		s := string(*task.Status)
+		detail.Status = &s
+	}
+
+	if task.Priority != nil {
+		p := int(*task.Priority)
+		detail.Priority = &p
+	}
+
+	if task.Estimate != nil {
+		detail.Estimate = task.Estimate
+	}
+
+	if task.ScheduledOn != nil {
+		s := task.ScheduledOn.Format("2006-01-02")
+		detail.ScheduledOn = &s
+	}
+
+	if task.CompletedAt != nil {
+		s := task.CompletedAt.Format(time.RFC3339)
+		detail.CompletedAt = &s
+	}
+
+	if task.Eisenhower != nil {
+		important := task.Eisenhower.IsImportant()
+		urgent := task.Eisenhower.IsUrgent()
+		detail.Important = &important
+		detail.Urgent = &urgent
+	}
+
+	text := formatTaskShowText(detail)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{Text: text}},
+	}, QueryOutput{Entity: EntityTask, DeepLink: detail.DeepLink, Items: detail}, nil
+}
+
+func (h *Handler) listTasks(
+	ctx context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	if input.AreaID != nil {
+		if err := lunatask.ValidateUUID(*input.AreaID); err != nil {
+			return shared.ErrorResult("invalid area_id: expected UUID"),
+				QueryOutput{Entity: EntityTask}, nil
+		}
+	}
+
+	if input.Status != nil {
+		if _, err := lunatask.ParseTaskStatus(*input.Status); err != nil {
+			return shared.ErrorResult("invalid status: must be later, next, started, waiting, or completed"),
+				QueryOutput{Entity: EntityTask}, nil
+		}
+	}
+
+	tasks, err := h.client.ListTasks(ctx, nil)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), QueryOutput{Entity: EntityTask}, nil
+	}
+
+	opts := &lunatask.TaskFilterOptions{
+		AreaID:           input.AreaID,
+		IncludeCompleted: input.IncludeCompleted != nil && *input.IncludeCompleted,
+		Today:            time.Now(),
+	}
+
+	if input.Status != nil {
+		s := lunatask.TaskStatus(*input.Status)
+		opts.Status = &s
+	}
+
+	filtered := lunatask.FilterTasks(tasks, opts)
+	summaries := buildTaskSummaries(filtered)
+	text := formatTaskListText(summaries)
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{Text: text}},
+		}, QueryOutput{
+			Entity: EntityTask,
+			Items:  summaries,
+			Count:  len(summaries),
+		}, nil
+}
+
+func buildTaskSummaries(tasks []lunatask.Task) []TaskSummary {
+	summaries := make([]TaskSummary, 0, len(tasks))
+
+	for _, task := range tasks {
+		summary := TaskSummary{
+			CreatedAt: task.CreatedAt.Format(time.RFC3339),
+			AreaID:    task.AreaID,
+			GoalID:    task.GoalID,
+		}
+
+		summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
+
+		if task.Status != nil {
+			s := string(*task.Status)
+			summary.Status = &s
+		}
+
+		if task.Priority != nil {
+			p := int(*task.Priority)
+			summary.Priority = &p
+		}
+
+		if task.ScheduledOn != nil {
+			s := task.ScheduledOn.Format("2006-01-02")
+			summary.ScheduledOn = &s
+		}
+
+		summaries = append(summaries, summary)
+	}
+
+	return summaries
+}
+
+func formatTaskListText(summaries []TaskSummary) string {
+	if len(summaries) == 0 {
+		return "No tasks found."
+	}
+
+	var builder strings.Builder
+
+	builder.WriteString(fmt.Sprintf("Found %d task(s):\n", len(summaries)))
+
+	for _, summary := range summaries {
+		status := "unknown"
+		if summary.Status != nil {
+			status = *summary.Status
+		}
+
+		builder.WriteString(fmt.Sprintf("- %s (%s)\n", summary.DeepLink, status))
+	}
+
+	builder.WriteString("\nUse query with id for full details.")
+
+	return builder.String()
+}
+
+func formatTaskShowText(detail TaskDetail) string {
+	var builder strings.Builder
+
+	builder.WriteString(fmt.Sprintf("Task: %s\n", detail.DeepLink))
+	writeOptionalField(&builder, "Status", detail.Status)
+	writeOptionalIntField(&builder, "Priority", detail.Priority)
+	writeOptionalField(&builder, "Scheduled", detail.ScheduledOn)
+	writeOptionalMinutesField(&builder, "Estimate", detail.Estimate)
+	writeEisenhowerField(&builder, detail.Important, detail.Urgent)
+	builder.WriteString(fmt.Sprintf("Created: %s\n", detail.CreatedAt))
+	builder.WriteString("Updated: " + detail.UpdatedAt)
+	writeOptionalField(&builder, "\nCompleted", detail.CompletedAt)
+
+	return builder.String()
+}
+
+func writeOptionalField(builder *strings.Builder, label string, value *string) {
+	if value != nil {
+		fmt.Fprintf(builder, "%s: %s\n", label, *value)
+	}
+}
+
+func writeOptionalIntField(builder *strings.Builder, label string, value *int) {
+	if value != nil {
+		fmt.Fprintf(builder, "%s: %d\n", label, *value)
+	}
+}
+
+func writeOptionalMinutesField(builder *strings.Builder, label string, value *int) {
+	if value != nil {
+		fmt.Fprintf(builder, "%s: %d min\n", label, *value)
+	}
+}
+
+func writeEisenhowerField(builder *strings.Builder, important, urgent *bool) {
+	var parts []string
+
+	if important != nil && *important {
+		parts = append(parts, "important")
+	}
+
+	if urgent != nil && *urgent {
+		parts = append(parts, "urgent")
+	}
+
+	if len(parts) > 0 {
+		fmt.Fprintf(builder, "Eisenhower: %s\n", strings.Join(parts, ", "))
+	}
+}
+
+// NoteSummary represents a note in list output.
+type NoteSummary struct {
+	DeepLink   string  `json:"deep_link"`
+	NotebookID *string `json:"notebook_id,omitempty"`
+	DateOn     *string `json:"date_on,omitempty"`
+	Pinned     bool    `json:"pinned"`
+	CreatedAt  string  `json:"created_at"`
+}
+
+// NoteSource represents a source reference in note output.
+type NoteSource struct {
+	Source   string `json:"source"`
+	SourceID string `json:"source_id"`
+}
+
+// NoteDetail represents detailed note information.
+type NoteDetail struct {
+	DeepLink   string       `json:"deep_link"`
+	NotebookID *string      `json:"notebook_id,omitempty"`
+	DateOn     *string      `json:"date_on,omitempty"`
+	Pinned     bool         `json:"pinned"`
+	Sources    []NoteSource `json:"sources,omitempty"`
+	CreatedAt  string       `json:"created_at"`
+	UpdatedAt  string       `json:"updated_at"`
 }
 
 func (h *Handler) queryNote(
-	_ context.Context,
+	ctx context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	if input.ID != nil {
+		return h.showNote(ctx, *input.ID)
+	}
+
+	return h.listNotes(ctx, input)
+}
+
+func (h *Handler) showNote(
+	ctx context.Context,
+	id string,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	_, noteID, err := lunatask.ParseReference(id)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			QueryOutput{Entity: EntityNote}, nil
+	}
+
+	note, err := h.client.GetNote(ctx, noteID)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), QueryOutput{Entity: EntityNote}, nil
+	}
+
+	detail := NoteDetail{
+		NotebookID: note.NotebookID,
+		Pinned:     note.Pinned,
+		CreatedAt:  note.CreatedAt.Format(time.RFC3339),
+		UpdatedAt:  note.UpdatedAt.Format(time.RFC3339),
+	}
+
+	detail.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
+
+	if note.DateOn != nil {
+		s := note.DateOn.Format("2006-01-02")
+		detail.DateOn = &s
+	}
+
+	if len(note.Sources) > 0 {
+		detail.Sources = make([]NoteSource, 0, len(note.Sources))
+		for _, src := range note.Sources {
+			detail.Sources = append(detail.Sources, NoteSource{
+				Source:   src.Source,
+				SourceID: src.SourceID,
+			})
+		}
+	}
+
+	text := formatNoteShowText(detail, h.notebooks)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{Text: text}},
+	}, QueryOutput{Entity: EntityNote, DeepLink: detail.DeepLink, Items: detail}, nil
+}
+
+func (h *Handler) listNotes(
+	ctx context.Context,
 	input QueryInput,
 ) (*mcp.CallToolResult, QueryOutput, error) {
-	return shared.ErrorResult("note query not yet implemented"),
-		QueryOutput{Entity: input.Entity}, nil
+	if input.NotebookID != nil {
+		if err := lunatask.ValidateUUID(*input.NotebookID); err != nil {
+			return shared.ErrorResult("invalid notebook_id: expected UUID"),
+				QueryOutput{Entity: EntityNote}, nil
+		}
+	}
+
+	opts := buildNoteListOptions(input)
+
+	notes, err := h.client.ListNotes(ctx, opts)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), QueryOutput{Entity: EntityNote}, nil
+	}
+
+	if input.NotebookID != nil {
+		notes = filterNotesByNotebook(notes, *input.NotebookID)
+	}
+
+	summaries := buildNoteSummaries(notes)
+	text := formatNoteListText(summaries, h.notebooks)
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{Text: text}},
+		}, QueryOutput{
+			Entity: EntityNote,
+			Items:  summaries,
+			Count:  len(summaries),
+		}, nil
+}
+
+func buildNoteListOptions(input QueryInput) *lunatask.ListNotesOptions {
+	if input.Source == nil && input.SourceID == nil {
+		return nil
+	}
+
+	opts := &lunatask.ListNotesOptions{}
+
+	if input.Source != nil {
+		opts.Source = input.Source
+	}
+
+	if input.SourceID != nil {
+		opts.SourceID = input.SourceID
+	}
+
+	return opts
+}
+
+func filterNotesByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
+	filtered := make([]lunatask.Note, 0, len(notes))
+
+	for _, note := range notes {
+		if note.NotebookID != nil && *note.NotebookID == notebookID {
+			filtered = append(filtered, note)
+		}
+	}
+
+	return filtered
+}
+
+func buildNoteSummaries(notes []lunatask.Note) []NoteSummary {
+	summaries := make([]NoteSummary, 0, len(notes))
+
+	for _, note := range notes {
+		summary := NoteSummary{
+			NotebookID: note.NotebookID,
+			Pinned:     note.Pinned,
+			CreatedAt:  note.CreatedAt.Format("2006-01-02"),
+		}
+
+		summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
+
+		if note.DateOn != nil {
+			dateStr := note.DateOn.Format("2006-01-02")
+			summary.DateOn = &dateStr
+		}
+
+		summaries = append(summaries, summary)
+	}
+
+	return summaries
+}
+
+func formatNoteListText(summaries []NoteSummary, notebooks []shared.NotebookProvider) string {
+	if len(summaries) == 0 {
+		return "No notes found"
+	}
+
+	var text strings.Builder
+
+	text.WriteString(fmt.Sprintf("Found %d note(s):\n", len(summaries)))
+
+	for _, item := range summaries {
+		text.WriteString("- ")
+		text.WriteString(item.DeepLink)
+
+		var details []string
+
+		if item.NotebookID != nil {
+			nbName := *item.NotebookID
+			for _, nb := range notebooks {
+				if nb.ID == *item.NotebookID {
+					nbName = nb.Key
+
+					break
+				}
+			}
+
+			details = append(details, "notebook: "+nbName)
+		}
+
+		if item.Pinned {
+			details = append(details, "pinned")
+		}
+
+		if len(details) > 0 {
+			text.WriteString(" (")
+			text.WriteString(strings.Join(details, ", "))
+			text.WriteString(")")
+		}
+
+		text.WriteString("\n")
+	}
+
+	text.WriteString("\nUse query with id for full details.")
+
+	return text.String()
+}
+
+func formatNoteShowText(detail NoteDetail, notebooks []shared.NotebookProvider) string {
+	var builder strings.Builder
+
+	builder.WriteString(fmt.Sprintf("Note: %s\n", detail.DeepLink))
+
+	if detail.NotebookID != nil {
+		nbName := *detail.NotebookID
+		for _, nb := range notebooks {
+			if nb.ID == *detail.NotebookID {
+				nbName = nb.Key
+
+				break
+			}
+		}
+
+		builder.WriteString(fmt.Sprintf("Notebook: %s\n", nbName))
+	}
+
+	if detail.DateOn != nil {
+		builder.WriteString(fmt.Sprintf("Date: %s\n", *detail.DateOn))
+	}
+
+	if detail.Pinned {
+		builder.WriteString("Pinned: yes\n")
+	}
+
+	if len(detail.Sources) > 0 {
+		builder.WriteString("Sources:\n")
+
+		for _, src := range detail.Sources {
+			builder.WriteString(fmt.Sprintf("  - %s: %s\n", src.Source, src.SourceID))
+		}
+	}
+
+	builder.WriteString(fmt.Sprintf("Created: %s\n", detail.CreatedAt))
+	builder.WriteString("Updated: " + detail.UpdatedAt)
+
+	return builder.String()
 }
 
 func (h *Handler) queryPerson(
-	_ context.Context,
+	ctx context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	if input.ID != nil {
+		return h.showPerson(ctx, *input.ID)
+	}
+
+	return h.listPeople(ctx, input)
+}
+
+// PersonSummary represents a person in list output.
+type PersonSummary struct {
+	DeepLink             string  `json:"deep_link"`
+	RelationshipStrength *string `json:"relationship_strength,omitempty"`
+	CreatedAt            string  `json:"created_at"`
+}
+
+// PersonSource represents a source reference in person output.
+type PersonSource struct {
+	Source   string `json:"source"`
+	SourceID string `json:"source_id"`
+}
+
+// PersonDetail represents detailed person information.
+type PersonDetail struct {
+	DeepLink             string         `json:"deep_link"`
+	RelationshipStrength *string        `json:"relationship_strength,omitempty"`
+	Sources              []PersonSource `json:"sources,omitempty"`
+	CreatedAt            string         `json:"created_at"`
+	UpdatedAt            string         `json:"updated_at"`
+}
+
+func (h *Handler) showPerson(
+	ctx context.Context,
+	id string,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	_, personID, err := lunatask.ParseReference(id)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			QueryOutput{Entity: EntityPerson}, nil
+	}
+
+	person, err := h.client.GetPerson(ctx, personID)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), QueryOutput{Entity: EntityPerson}, nil
+	}
+
+	detail := PersonDetail{
+		CreatedAt: person.CreatedAt.Format(time.RFC3339),
+		UpdatedAt: person.UpdatedAt.Format(time.RFC3339),
+	}
+
+	detail.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
+
+	if person.RelationshipStrength != nil {
+		s := string(*person.RelationshipStrength)
+		detail.RelationshipStrength = &s
+	}
+
+	if len(person.Sources) > 0 {
+		detail.Sources = make([]PersonSource, 0, len(person.Sources))
+		for _, src := range person.Sources {
+			detail.Sources = append(detail.Sources, PersonSource{
+				Source:   src.Source,
+				SourceID: src.SourceID,
+			})
+		}
+	}
+
+	text := formatPersonShowText(detail)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{Text: text}},
+	}, QueryOutput{Entity: EntityPerson, DeepLink: detail.DeepLink, Items: detail}, nil
+}
+
+func (h *Handler) listPeople(
+	ctx context.Context,
 	input QueryInput,
 ) (*mcp.CallToolResult, QueryOutput, error) {
-	return shared.ErrorResult("person query not yet implemented"),
-		QueryOutput{Entity: input.Entity}, nil
+	opts := buildPeopleListOptions(input)
+
+	people, err := h.client.ListPeople(ctx, opts)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), QueryOutput{Entity: EntityPerson}, nil
+	}
+
+	summaries := buildPersonSummaries(people)
+	text := formatPeopleListText(summaries)
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{Text: text}},
+		}, QueryOutput{
+			Entity: EntityPerson,
+			Items:  summaries,
+			Count:  len(summaries),
+		}, nil
+}
+
+func buildPeopleListOptions(input QueryInput) *lunatask.ListPeopleOptions {
+	if input.Source == nil && input.SourceID == nil {
+		return nil
+	}
+
+	opts := &lunatask.ListPeopleOptions{}
+
+	if input.Source != nil {
+		opts.Source = input.Source
+	}
+
+	if input.SourceID != nil {
+		opts.SourceID = input.SourceID
+	}
+
+	return opts
+}
+
+func buildPersonSummaries(people []lunatask.Person) []PersonSummary {
+	summaries := make([]PersonSummary, 0, len(people))
+
+	for _, person := range people {
+		deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
+
+		summary := PersonSummary{
+			DeepLink:  deepLink,
+			CreatedAt: person.CreatedAt.Format("2006-01-02"),
+		}
+
+		if person.RelationshipStrength != nil {
+			rel := string(*person.RelationshipStrength)
+			summary.RelationshipStrength = &rel
+		}
+
+		summaries = append(summaries, summary)
+	}
+
+	return summaries
+}
+
+func formatPeopleListText(summaries []PersonSummary) string {
+	if len(summaries) == 0 {
+		return "No people found"
+	}
+
+	var text strings.Builder
+
+	text.WriteString(fmt.Sprintf("Found %d person(s):\n", len(summaries)))
+
+	for _, item := range summaries {
+		text.WriteString("- ")
+		text.WriteString(item.DeepLink)
+
+		if item.RelationshipStrength != nil {
+			text.WriteString(" (")
+			text.WriteString(*item.RelationshipStrength)
+			text.WriteString(")")
+		}
+
+		text.WriteString("\n")
+	}
+
+	text.WriteString("\nUse query with id for full details.")
+
+	return text.String()
+}
+
+func formatPersonShowText(detail PersonDetail) string {
+	var builder strings.Builder
+
+	builder.WriteString(fmt.Sprintf("Person: %s\n", detail.DeepLink))
+
+	if detail.RelationshipStrength != nil {
+		builder.WriteString(fmt.Sprintf("Relationship: %s\n", *detail.RelationshipStrength))
+	}
+
+	if len(detail.Sources) > 0 {
+		builder.WriteString("Sources:\n")
+
+		for _, src := range detail.Sources {
+			builder.WriteString(fmt.Sprintf("  - %s: %s\n", src.Source, src.SourceID))
+		}
+	}
+
+	builder.WriteString(fmt.Sprintf("Created: %s\n", detail.CreatedAt))
+	builder.WriteString("Updated: " + detail.UpdatedAt)
+
+	return builder.String()
 }
 
 func (h *Handler) queryArea(
@@ -137,8 +816,50 @@ func (h *Handler) queryArea(
 			QueryOutput{Entity: input.Entity}, nil
 	}
 
-	return shared.ErrorResult("area query not yet implemented"),
-		QueryOutput{Entity: input.Entity}, nil
+	summaries := make([]AreaSummary, 0, len(h.areas))
+
+	for _, area := range h.areas {
+		summaries = append(summaries, AreaSummary{
+			ID:       area.ID,
+			Name:     area.Name,
+			Key:      area.Key,
+			Workflow: string(area.Workflow),
+		})
+	}
+
+	text := formatAreaListText(summaries)
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{Text: text}},
+		}, QueryOutput{
+			Entity: EntityArea,
+			Items:  summaries,
+			Count:  len(summaries),
+		}, nil
+}
+
+// AreaSummary represents an area in list output.
+type AreaSummary struct {
+	ID       string `json:"id"`
+	Name     string `json:"name"`
+	Key      string `json:"key"`
+	Workflow string `json:"workflow"`
+}
+
+func formatAreaListText(areas []AreaSummary) string {
+	if len(areas) == 0 {
+		return "No areas configured"
+	}
+
+	var text strings.Builder
+
+	text.WriteString(fmt.Sprintf("Found %d area(s):\n", len(areas)))
+
+	for _, a := range areas {
+		text.WriteString(fmt.Sprintf("- %s: %s (%s, workflow: %s)\n", a.Key, a.Name, a.ID, a.Workflow))
+	}
+
+	return text.String()
 }
 
 func (h *Handler) queryGoal(
@@ -150,8 +871,81 @@ func (h *Handler) queryGoal(
 			QueryOutput{Entity: input.Entity}, nil
 	}
 
-	return shared.ErrorResult("goal query not yet implemented"),
-		QueryOutput{Entity: input.Entity}, nil
+	if input.AreaID == nil {
+		return shared.ErrorResult("area_id is required for goal query"),
+			QueryOutput{Entity: input.Entity}, nil
+	}
+
+	area := h.resolveAreaRef(*input.AreaID)
+	if area == nil {
+		return shared.ErrorResult("unknown area: " + *input.AreaID),
+			QueryOutput{Entity: input.Entity}, nil
+	}
+
+	summaries := make([]GoalSummary, 0, len(area.Goals))
+
+	for _, goal := range area.Goals {
+		summaries = append(summaries, GoalSummary{
+			ID:   goal.ID,
+			Name: goal.Name,
+			Key:  goal.Key,
+		})
+	}
+
+	text := formatGoalListText(summaries, area.Name)
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{Text: text}},
+		}, QueryOutput{
+			Entity: EntityGoal,
+			Items:  summaries,
+			Count:  len(summaries),
+		}, nil
+}
+
+// GoalSummary represents a goal in list output.
+type GoalSummary struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+	Key  string `json:"key"`
+}
+
+// resolveAreaRef resolves an area reference to an AreaProvider.
+// Accepts config key, UUID, or deep link.
+func (h *Handler) resolveAreaRef(input string) *shared.AreaProvider {
+	// Try UUID or deep link first
+	if _, id, err := lunatask.ParseReference(input); err == nil {
+		for i := range h.areas {
+			if h.areas[i].ID == id {
+				return &h.areas[i]
+			}
+		}
+	}
+
+	// Try config key lookup
+	for i := range h.areas {
+		if h.areas[i].Key == input {
+			return &h.areas[i]
+		}
+	}
+
+	return nil
+}
+
+func formatGoalListText(goals []GoalSummary, areaName string) string {
+	if len(goals) == 0 {
+		return fmt.Sprintf("No goals configured for area %q", areaName)
+	}
+
+	var text strings.Builder
+
+	text.WriteString(fmt.Sprintf("Found %d goal(s) in %q:\n", len(goals), areaName))
+
+	for _, g := range goals {
+		text.WriteString(fmt.Sprintf("- %s: %s (%s)\n", g.Key, g.Name, g.ID))
+	}
+
+	return text.String()
 }
 
 func (h *Handler) queryNotebook(
@@ -163,8 +957,48 @@ func (h *Handler) queryNotebook(
 			QueryOutput{Entity: input.Entity}, nil
 	}
 
-	return shared.ErrorResult("notebook query not yet implemented"),
-		QueryOutput{Entity: input.Entity}, nil
+	summaries := make([]NotebookSummary, 0, len(h.notebooks))
+
+	for _, nb := range h.notebooks {
+		summaries = append(summaries, NotebookSummary{
+			ID:   nb.ID,
+			Name: nb.Name,
+			Key:  nb.Key,
+		})
+	}
+
+	text := formatNotebookListText(summaries)
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{Text: text}},
+		}, QueryOutput{
+			Entity: EntityNotebook,
+			Items:  summaries,
+			Count:  len(summaries),
+		}, nil
+}
+
+// NotebookSummary represents a notebook in list output.
+type NotebookSummary struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+	Key  string `json:"key"`
+}
+
+func formatNotebookListText(notebooks []NotebookSummary) string {
+	if len(notebooks) == 0 {
+		return "No notebooks configured"
+	}
+
+	var text strings.Builder
+
+	text.WriteString(fmt.Sprintf("Found %d notebook(s):\n", len(notebooks)))
+
+	for _, nb := range notebooks {
+		text.WriteString(fmt.Sprintf("- %s (key: %s, id: %s)\n", nb.Name, nb.Key, nb.ID))
+	}
+
+	return text.String()
 }
 
 func (h *Handler) queryHabit(
@@ -176,6 +1010,46 @@ func (h *Handler) queryHabit(
 			QueryOutput{Entity: input.Entity}, nil
 	}
 
-	return shared.ErrorResult("habit query not yet implemented"),
-		QueryOutput{Entity: input.Entity}, nil
+	summaries := make([]HabitSummary, 0, len(h.habits))
+
+	for _, habit := range h.habits {
+		summaries = append(summaries, HabitSummary{
+			ID:   habit.ID,
+			Name: habit.Name,
+			Key:  habit.Key,
+		})
+	}
+
+	text := formatHabitListText(summaries)
+
+	return &mcp.CallToolResult{
+			Content: []mcp.Content{&mcp.TextContent{Text: text}},
+		}, QueryOutput{
+			Entity: EntityHabit,
+			Items:  summaries,
+			Count:  len(summaries),
+		}, nil
+}
+
+// HabitSummary represents a habit in list output.
+type HabitSummary struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+	Key  string `json:"key"`
+}
+
+func formatHabitListText(habits []HabitSummary) string {
+	if len(habits) == 0 {
+		return "No habits configured"
+	}
+
+	var text strings.Builder
+
+	text.WriteString(fmt.Sprintf("Found %d habit(s):\n", len(habits)))
+
+	for _, h := range habits {
+		text.WriteString(fmt.Sprintf("- %s: %s (%s)\n", h.Key, h.Name, h.ID))
+	}
+
+	return text.String()
 }

internal/mcp/tools/crud/update.go 🔗

@@ -7,7 +7,10 @@ package crud
 import (
 	"context"
 
+	"git.secluded.site/go-lunatask"
+	"git.secluded.site/lune/internal/dateutil"
 	"git.secluded.site/lune/internal/mcp/shared"
+	"git.secluded.site/lune/internal/validate"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
@@ -52,7 +55,7 @@ Returns the updated entity's deep link.`
 
 // UpdateInput is the input schema for the consolidated update tool.
 type UpdateInput struct {
-	Entity string `json:"entity" jsonschema:"required,enum=task,enum=note,enum=person"`
+	Entity string `json:"entity" jsonschema:"required"`
 	ID     string `json:"id"     jsonschema:"required"`
 
 	// Common fields
@@ -106,26 +109,280 @@ func (h *Handler) HandleUpdate(
 	}
 }
 
+// parsedTaskUpdateInput holds validated and parsed task update input fields.
+type parsedTaskUpdateInput struct {
+	ID          string
+	Name        *string
+	AreaID      *string
+	GoalID      *string
+	Status      *lunatask.TaskStatus
+	Note        *string
+	Priority    *lunatask.Priority
+	Estimate    *int
+	Motivation  *lunatask.Motivation
+	Important   *bool
+	Urgent      *bool
+	ScheduledOn *lunatask.Date
+}
+
 func (h *Handler) updateTask(
-	_ context.Context,
+	ctx context.Context,
 	input UpdateInput,
 ) (*mcp.CallToolResult, UpdateOutput, error) {
-	return shared.ErrorResult("task update not yet implemented"),
-		UpdateOutput{Entity: input.Entity}, nil
+	parsed, errResult := h.parseTaskUpdateInput(input)
+	if errResult != nil {
+		return errResult, UpdateOutput{Entity: input.Entity}, nil
+	}
+
+	builder := h.client.NewTaskUpdate(parsed.ID)
+	applyToTaskUpdateBuilder(builder, parsed)
+
+	task, err := builder.Update(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), UpdateOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Task updated: " + deepLink,
+		}},
+	}, UpdateOutput{Entity: input.Entity, DeepLink: deepLink}, nil
+}
+
+//nolint:cyclop,funlen
+func (h *Handler) parseTaskUpdateInput(input UpdateInput) (*parsedTaskUpdateInput, *mcp.CallToolResult) {
+	_, id, err := lunatask.ParseReference(input.ID)
+	if err != nil {
+		return nil, shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link")
+	}
+
+	parsed := &parsedTaskUpdateInput{
+		ID:        id,
+		Name:      input.Name,
+		Note:      input.Note,
+		Estimate:  input.Estimate,
+		Important: input.Important,
+		Urgent:    input.Urgent,
+	}
+
+	if input.AreaID != nil {
+		areaID, err := validate.AreaRef(h.cfg, *input.AreaID)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.AreaID = &areaID
+	}
+
+	if input.GoalID != nil {
+		areaID := ""
+		if parsed.AreaID != nil {
+			areaID = *parsed.AreaID
+		}
+
+		goalID, err := validate.GoalRef(h.cfg, areaID, *input.GoalID)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.GoalID = &goalID
+	}
+
+	if input.Estimate != nil {
+		if err := shared.ValidateEstimate(*input.Estimate); err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+	}
+
+	if input.Status != nil {
+		status, err := lunatask.ParseTaskStatus(*input.Status)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.Status = &status
+	}
+
+	if input.Priority != nil {
+		priority, err := lunatask.ParsePriority(*input.Priority)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.Priority = &priority
+	}
+
+	if input.Motivation != nil {
+		motivation, err := lunatask.ParseMotivation(*input.Motivation)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.Motivation = &motivation
+	}
+
+	if input.ScheduledOn != nil {
+		date, err := dateutil.Parse(*input.ScheduledOn)
+		if err != nil {
+			return nil, shared.ErrorResult(err.Error())
+		}
+
+		parsed.ScheduledOn = &date
+	}
+
+	return parsed, nil
+}
+
+//nolint:cyclop
+func applyToTaskUpdateBuilder(builder *lunatask.TaskUpdateBuilder, parsed *parsedTaskUpdateInput) {
+	if parsed.Name != nil {
+		builder.Name(*parsed.Name)
+	}
+
+	if parsed.AreaID != nil {
+		builder.InArea(*parsed.AreaID)
+	}
+
+	if parsed.GoalID != nil {
+		builder.InGoal(*parsed.GoalID)
+	}
+
+	if parsed.Status != nil {
+		builder.WithStatus(*parsed.Status)
+	}
+
+	if parsed.Note != nil {
+		builder.WithNote(*parsed.Note)
+	}
+
+	if parsed.Priority != nil {
+		builder.Priority(*parsed.Priority)
+	}
+
+	if parsed.Estimate != nil {
+		builder.WithEstimate(*parsed.Estimate)
+	}
+
+	if parsed.Motivation != nil {
+		builder.WithMotivation(*parsed.Motivation)
+	}
+
+	if parsed.Important != nil {
+		if *parsed.Important {
+			builder.Important()
+		} else {
+			builder.NotImportant()
+		}
+	}
+
+	if parsed.Urgent != nil {
+		if *parsed.Urgent {
+			builder.Urgent()
+		} else {
+			builder.NotUrgent()
+		}
+	}
+
+	if parsed.ScheduledOn != nil {
+		builder.ScheduledOn(*parsed.ScheduledOn)
+	}
 }
 
 func (h *Handler) updateNote(
-	_ context.Context,
+	ctx context.Context,
 	input UpdateInput,
 ) (*mcp.CallToolResult, UpdateOutput, error) {
-	return shared.ErrorResult("note update not yet implemented"),
-		UpdateOutput{Entity: input.Entity}, nil
+	_, id, err := lunatask.ParseReference(input.ID)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			UpdateOutput{Entity: input.Entity}, nil
+	}
+
+	if input.NotebookID != nil {
+		if err := lunatask.ValidateUUID(*input.NotebookID); err != nil {
+			return shared.ErrorResult("invalid notebook_id: expected UUID"),
+				UpdateOutput{Entity: input.Entity}, nil
+		}
+	}
+
+	builder := h.client.NewNoteUpdate(id)
+
+	if input.Name != nil {
+		builder.WithName(*input.Name)
+	}
+
+	if input.NotebookID != nil {
+		builder.InNotebook(*input.NotebookID)
+	}
+
+	if input.Content != nil {
+		builder.WithContent(*input.Content)
+	}
+
+	if input.Date != nil {
+		date, err := dateutil.Parse(*input.Date)
+		if err != nil {
+			return shared.ErrorResult(err.Error()), UpdateOutput{Entity: input.Entity}, nil
+		}
+
+		builder.OnDate(date)
+	}
+
+	note, err := builder.Update(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), UpdateOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Note updated: " + deepLink,
+		}},
+	}, UpdateOutput{Entity: input.Entity, DeepLink: deepLink}, nil
 }
 
 func (h *Handler) updatePerson(
-	_ context.Context,
+	ctx context.Context,
 	input UpdateInput,
 ) (*mcp.CallToolResult, UpdateOutput, error) {
-	return shared.ErrorResult("person update not yet implemented"),
-		UpdateOutput{Entity: input.Entity}, nil
+	_, id, err := lunatask.ParseReference(input.ID)
+	if err != nil {
+		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"),
+			UpdateOutput{Entity: input.Entity}, nil
+	}
+
+	builder := h.client.NewPersonUpdate(id)
+
+	if input.FirstName != nil {
+		builder.FirstName(*input.FirstName)
+	}
+
+	if input.LastName != nil {
+		builder.LastName(*input.LastName)
+	}
+
+	if input.Relationship != nil {
+		rel, err := lunatask.ParseRelationshipStrength(*input.Relationship)
+		if err != nil {
+			return shared.ErrorResult(err.Error()), UpdateOutput{Entity: input.Entity}, nil
+		}
+
+		builder.WithRelationshipStrength(rel)
+	}
+
+	person, err := builder.Update(ctx)
+	if err != nil {
+		return shared.ErrorResult(err.Error()), UpdateOutput{Entity: input.Entity}, nil
+	}
+
+	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
+
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{
+			Text: "Person updated: " + deepLink,
+		}},
+	}, UpdateOutput{Entity: input.Entity, DeepLink: deepLink}, nil
 }

internal/mcp/tools/goal/list.go 🔗

@@ -1,128 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package goal provides MCP tools for goal operations.
-package goal
-
-import (
-	"context"
-	"fmt"
-	"strings"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// ListToolName is the name of the list goals tool.
-const ListToolName = "list_goals"
-
-// ListToolDescription describes the list goals tool for LLMs.
-const ListToolDescription = `Lists goals for an area. Use list_areas to find area IDs first.
-
-Required:
-- area_id: Area UUID, deep link, or config key
-
-Returns goal metadata (id, name, key) for the specified area.`
-
-// ListInput is the input schema for listing goals.
-type ListInput struct {
-	AreaID string `json:"area_id" jsonschema:"required"`
-}
-
-// Summary represents a goal in the list output.
-type Summary struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-	Key  string `json:"key"`
-}
-
-// ListOutput is the output schema for listing goals.
-type ListOutput struct {
-	Goals  []Summary `json:"goals"`
-	Count  int       `json:"count"`
-	AreaID string    `json:"area_id"`
-}
-
-// Handler handles goal tool requests.
-type Handler struct {
-	areas []shared.AreaProvider
-}
-
-// NewHandler creates a new goal tool handler.
-func NewHandler(areas []shared.AreaProvider) *Handler {
-	return &Handler{areas: areas}
-}
-
-// HandleList lists goals for an area.
-func (h *Handler) HandleList(
-	_ context.Context,
-	_ *mcp.CallToolRequest,
-	input ListInput,
-) (*mcp.CallToolResult, ListOutput, error) {
-	area := h.resolveAreaRef(input.AreaID)
-	if area == nil {
-		return shared.ErrorResult("unknown area: " + input.AreaID), ListOutput{}, nil
-	}
-
-	summaries := make([]Summary, 0, len(area.Goals))
-
-	for _, goal := range area.Goals {
-		summaries = append(summaries, Summary{
-			ID:   goal.ID,
-			Name: goal.Name,
-			Key:  goal.Key,
-		})
-	}
-
-	output := ListOutput{
-		Goals:  summaries,
-		Count:  len(summaries),
-		AreaID: area.ID,
-	}
-
-	text := formatListText(summaries, area.Name)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-// resolveAreaRef resolves an area reference to an AreaProvider.
-// Accepts config key, UUID, or deep link.
-func (h *Handler) resolveAreaRef(input string) *shared.AreaProvider {
-	// Try UUID or deep link first
-	if _, id, err := lunatask.ParseReference(input); err == nil {
-		for i := range h.areas {
-			if h.areas[i].ID == id {
-				return &h.areas[i]
-			}
-		}
-	}
-
-	// Try config key lookup
-	for i := range h.areas {
-		if h.areas[i].Key == input {
-			return &h.areas[i]
-		}
-	}
-
-	return nil
-}
-
-func formatListText(goals []Summary, areaName string) string {
-	if len(goals) == 0 {
-		return fmt.Sprintf("No goals configured for area %q", areaName)
-	}
-
-	var text strings.Builder
-
-	text.WriteString(fmt.Sprintf("Found %d goal(s) in %q:\n", len(goals), areaName))
-
-	for _, g := range goals {
-		text.WriteString(fmt.Sprintf("- %s: %s (%s)\n", g.Key, g.Name, g.ID))
-	}
-
-	return text.String()
-}

internal/mcp/tools/habit/list.go 🔗

@@ -1,82 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package habit
-
-import (
-	"context"
-	"fmt"
-	"strings"
-
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// ListToolName is the name of the list habits tool.
-const ListToolName = "list_habits"
-
-// ListToolDescription describes the list habits tool for LLMs.
-const ListToolDescription = `Lists configured habits. Fallback for lunatask://habits resource.
-
-Returns habit metadata (id, name, key) from config.
-Use the lunatask://habits resource if your client supports MCP resources.`
-
-// ListInput is the input schema for listing habits.
-type ListInput struct{}
-
-// Summary represents a habit in the list output.
-type Summary struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-	Key  string `json:"key"`
-}
-
-// ListOutput is the output schema for listing habits.
-type ListOutput struct {
-	Habits []Summary `json:"habits"`
-	Count  int       `json:"count"`
-}
-
-// HandleList lists configured habits.
-func (h *Handler) HandleList(
-	_ context.Context,
-	_ *mcp.CallToolRequest,
-	_ ListInput,
-) (*mcp.CallToolResult, ListOutput, error) {
-	summaries := make([]Summary, 0, len(h.habits))
-
-	for _, habit := range h.habits {
-		summaries = append(summaries, Summary{
-			ID:   habit.ID,
-			Name: habit.Name,
-			Key:  habit.Key,
-		})
-	}
-
-	output := ListOutput{
-		Habits: summaries,
-		Count:  len(summaries),
-	}
-
-	text := formatListText(summaries)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-func formatListText(habits []Summary) string {
-	if len(habits) == 0 {
-		return "No habits configured"
-	}
-
-	var text strings.Builder
-
-	text.WriteString(fmt.Sprintf("Found %d habit(s):\n", len(habits)))
-
-	for _, h := range habits {
-		text.WriteString(fmt.Sprintf("- %s: %s (%s)\n", h.Key, h.Name, h.ID))
-	}
-
-	return text.String()
-}

internal/mcp/tools/habit/track.go 🔗

@@ -21,7 +21,7 @@ const TrackToolName = "track_habit"
 const TrackToolDescription = `Records that a habit was performed on a specific date.
 
 Required:
-- habit_id: Habit UUID (get from lunatask://habits resource)
+- habit_id: Habit UUID, deep link, or config key
 
 Optional:
 - performed_on: Date performed (YYYY-MM-DD or natural language, default: today)
@@ -61,8 +61,9 @@ func (h *Handler) HandleTrack(
 	_ *mcp.CallToolRequest,
 	input TrackInput,
 ) (*mcp.CallToolResult, TrackOutput, error) {
-	if err := lunatask.ValidateUUID(input.HabitID); err != nil {
-		return shared.ErrorResult("invalid habit_id: expected UUID"), TrackOutput{}, nil
+	habitID := h.resolveHabitRef(input.HabitID)
+	if habitID == "" {
+		return shared.ErrorResult("unknown habit: " + input.HabitID), TrackOutput{}, nil
 	}
 
 	dateStr := ""
@@ -79,14 +80,32 @@ func (h *Handler) HandleTrack(
 		PerformedOn: performedOn,
 	}
 
-	_, err = h.client.TrackHabitActivity(ctx, input.HabitID, req)
+	_, err = h.client.TrackHabitActivity(ctx, habitID, req)
 	if err != nil {
 		return shared.ErrorResult(err.Error()), TrackOutput{}, nil
 	}
 
 	return nil, TrackOutput{
 		Success:     true,
-		HabitID:     input.HabitID,
+		HabitID:     habitID,
 		PerformedOn: performedOn.Format("2006-01-02"),
 	}, nil
 }
+
+// resolveHabitRef resolves a habit reference to a UUID.
+// Accepts config key, UUID, or deep link.
+func (h *Handler) resolveHabitRef(input string) string {
+	// Try UUID or deep link first
+	if _, id, err := lunatask.ParseReference(input); err == nil {
+		return id
+	}
+
+	// Try config key lookup
+	for _, habit := range h.habits {
+		if habit.Key == input {
+			return habit.ID
+		}
+	}
+
+	return ""
+}

internal/mcp/tools/journal/create.go 🔗

@@ -1,102 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package journal provides MCP tools for Lunatask journal operations.
-package journal
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/dateutil"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// CreateToolName is the name of the create journal entry tool.
-const CreateToolName = "add_journal_entry"
-
-// CreateToolDescription describes the create journal entry tool for LLMs.
-const CreateToolDescription = `Creates a journal entry in Lunatask for daily reflection.
-
-Optional:
-- content: Markdown content for the entry
-- name: Entry title (defaults to weekday name if omitted)
-- date: Entry date (YYYY-MM-DD or natural language, default: today)
-
-Entries are date-keyed. If no date is provided, uses today.
-Content supports Markdown formatting.
-
-Common uses:
-- End-of-day summaries
-- Reflection on completed work
-- Recording thoughts or learnings`
-
-// CreateInput is the input schema for creating a journal entry.
-type CreateInput struct {
-	Content *string `json:"content,omitempty"`
-	Name    *string `json:"name,omitempty"`
-	Date    *string `json:"date,omitempty"`
-}
-
-// CreateOutput is the output schema for creating a journal entry.
-type CreateOutput struct {
-	ID   string `json:"id"`
-	Date string `json:"date"`
-}
-
-// Handler handles journal-related MCP tool requests.
-type Handler struct {
-	client *lunatask.Client
-}
-
-// NewHandler creates a new journal handler.
-func NewHandler(accessToken string) *Handler {
-	return &Handler{
-		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
-	}
-}
-
-// HandleCreate creates a new journal entry.
-func (h *Handler) HandleCreate(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input CreateInput,
-) (*mcp.CallToolResult, CreateOutput, error) {
-	dateStr := ""
-	if input.Date != nil {
-		dateStr = *input.Date
-	}
-
-	date, err := dateutil.Parse(dateStr)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
-	}
-
-	builder := h.client.NewJournalEntry(date)
-
-	if input.Content != nil {
-		builder.WithContent(*input.Content)
-	}
-
-	if input.Name != nil {
-		builder.WithName(*input.Name)
-	}
-
-	entry, err := builder.Create(ctx)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
-	}
-
-	formattedDate := date.Format("2006-01-02")
-
-	return &mcp.CallToolResult{
-			Content: []mcp.Content{&mcp.TextContent{
-				Text: "Journal entry created for " + formattedDate,
-			}},
-		}, CreateOutput{
-			ID:   entry.ID,
-			Date: formattedDate,
-		}, nil
-}

internal/mcp/tools/note/create.go 🔗

@@ -1,113 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package note provides MCP tools for Lunatask note operations.
-package note
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// CreateToolName is the name of the create note tool.
-const CreateToolName = "create_note"
-
-// CreateToolDescription describes the create note tool for LLMs.
-const CreateToolDescription = `Creates a new note in Lunatask.
-
-Optional:
-- name: Note title
-- notebook_id: Notebook UUID (get from lunatask://notebooks resource)
-- content: Markdown content
-- source: Origin identifier for integrations
-- source_id: Source-specific ID (requires source)
-
-All fields are optional — can create an empty note.
-Returns the deep link to the created note.
-
-Note: If a note with the same source/source_id already exists,
-the API returns a duplicate warning instead of creating a new note.`
-
-// CreateInput is the input schema for creating a note.
-type CreateInput struct {
-	Name       *string `json:"name,omitempty"`
-	NotebookID *string `json:"notebook_id,omitempty"`
-	Content    *string `json:"content,omitempty"`
-	Source     *string `json:"source,omitempty"`
-	SourceID   *string `json:"source_id,omitempty"`
-}
-
-// CreateOutput is the output schema for creating a note.
-type CreateOutput struct {
-	DeepLink string `json:"deep_link"`
-}
-
-// Handler handles note-related MCP tool requests.
-type Handler struct {
-	client    *lunatask.Client
-	notebooks []shared.NotebookProvider
-}
-
-// NewHandler creates a new note handler.
-func NewHandler(accessToken string, notebooks []shared.NotebookProvider) *Handler {
-	return &Handler{
-		client:    lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
-		notebooks: notebooks,
-	}
-}
-
-// HandleCreate creates a new note.
-func (h *Handler) HandleCreate(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input CreateInput,
-) (*mcp.CallToolResult, CreateOutput, error) {
-	if input.NotebookID != nil {
-		if err := lunatask.ValidateUUID(*input.NotebookID); err != nil {
-			return shared.ErrorResult("invalid notebook_id: expected UUID"), CreateOutput{}, nil
-		}
-	}
-
-	builder := h.client.NewNote()
-
-	if input.Name != nil {
-		builder.WithName(*input.Name)
-	}
-
-	if input.NotebookID != nil {
-		builder.InNotebook(*input.NotebookID)
-	}
-
-	if input.Content != nil {
-		builder.WithContent(*input.Content)
-	}
-
-	if input.Source != nil && input.SourceID != nil {
-		builder.FromSource(*input.Source, *input.SourceID)
-	}
-
-	note, err := builder.Create(ctx)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
-	}
-
-	if note == nil {
-		return &mcp.CallToolResult{
-			Content: []mcp.Content{&mcp.TextContent{
-				Text: "Note already exists (duplicate source)",
-			}},
-		}, CreateOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{
-			Text: "Note created: " + deepLink,
-		}},
-	}, CreateOutput{DeepLink: deepLink}, nil
-}

internal/mcp/tools/note/delete.go 🔗

@@ -1,63 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package note
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// DeleteToolName is the name of the delete note tool.
-const DeleteToolName = "delete_note"
-
-// DeleteToolDescription describes the delete note tool for LLMs.
-const DeleteToolDescription = `Deletes a note from Lunatask.
-
-Required:
-- id: Note UUID or lunatask://note/... deep link
-
-This action is permanent and cannot be undone.`
-
-// DeleteInput is the input schema for deleting a note.
-type DeleteInput struct {
-	ID string `json:"id" jsonschema:"required"`
-}
-
-// DeleteOutput is the output schema for deleting a note.
-type DeleteOutput struct {
-	Success  bool   `json:"success"`
-	DeepLink string `json:"deep_link"`
-}
-
-// HandleDelete deletes a note.
-func (h *Handler) HandleDelete(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input DeleteInput,
-) (*mcp.CallToolResult, DeleteOutput, error) {
-	id, err := parseReference(input.ID)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), DeleteOutput{}, nil
-	}
-
-	note, err := h.client.DeleteNote(ctx, id)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), DeleteOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
-
-	return &mcp.CallToolResult{
-			Content: []mcp.Content{&mcp.TextContent{
-				Text: "Note deleted: " + deepLink,
-			}},
-		}, DeleteOutput{
-			Success:  true,
-			DeepLink: deepLink,
-		}, nil
-}

internal/mcp/tools/note/list.go 🔗

@@ -1,185 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package note
-
-import (
-	"context"
-	"fmt"
-	"strings"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// ListToolName is the name of the list notes tool.
-const ListToolName = "list_notes"
-
-// ListToolDescription describes the list notes tool for LLMs.
-const ListToolDescription = `Lists notes from Lunatask.
-
-Optional:
-- notebook_id: Filter by notebook UUID (from lunatask://notebooks)
-- source: Filter by source identifier
-- source_id: Filter by source-specific ID
-
-Returns note metadata (IDs, dates, pinned status).
-Use lunatask://note/{id} resource for full note details.
-
-Note: Due to end-to-end encryption, note names and content
-are not available in the list — only metadata is returned.`
-
-// ListInput is the input schema for listing notes.
-type ListInput struct {
-	NotebookID *string `json:"notebook_id,omitempty"`
-	Source     *string `json:"source,omitempty"`
-	SourceID   *string `json:"source_id,omitempty"`
-}
-
-// ListNoteItem represents a note in the list output.
-type ListNoteItem struct {
-	DeepLink   string  `json:"deep_link"`
-	NotebookID *string `json:"notebook_id,omitempty"`
-	DateOn     *string `json:"date_on,omitempty"`
-	Pinned     bool    `json:"pinned"`
-	CreatedAt  string  `json:"created_at"`
-}
-
-// ListOutput is the output schema for listing notes.
-type ListOutput struct {
-	Notes []ListNoteItem `json:"notes"`
-	Count int            `json:"count"`
-}
-
-// HandleList lists notes.
-func (h *Handler) HandleList(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input ListInput,
-) (*mcp.CallToolResult, ListOutput, error) {
-	if input.NotebookID != nil {
-		if err := lunatask.ValidateUUID(*input.NotebookID); err != nil {
-			return shared.ErrorResult("invalid notebook_id: expected UUID"), ListOutput{}, nil
-		}
-	}
-
-	opts := buildListOptions(input)
-
-	notes, err := h.client.ListNotes(ctx, opts)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), ListOutput{}, nil
-	}
-
-	if input.NotebookID != nil {
-		notes = filterByNotebook(notes, *input.NotebookID)
-	}
-
-	items := make([]ListNoteItem, 0, len(notes))
-
-	for _, note := range notes {
-		deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
-
-		item := ListNoteItem{
-			DeepLink:   deepLink,
-			NotebookID: note.NotebookID,
-			Pinned:     note.Pinned,
-			CreatedAt:  note.CreatedAt.Format("2006-01-02"),
-		}
-
-		if note.DateOn != nil {
-			dateStr := note.DateOn.Format("2006-01-02")
-			item.DateOn = &dateStr
-		}
-
-		items = append(items, item)
-	}
-
-	output := ListOutput{
-		Notes: items,
-		Count: len(items),
-	}
-
-	text := formatListText(items, h.notebooks)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-func buildListOptions(input ListInput) *lunatask.ListNotesOptions {
-	if input.Source == nil && input.SourceID == nil {
-		return nil
-	}
-
-	opts := &lunatask.ListNotesOptions{}
-
-	if input.Source != nil {
-		opts.Source = input.Source
-	}
-
-	if input.SourceID != nil {
-		opts.SourceID = input.SourceID
-	}
-
-	return opts
-}
-
-func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
-	filtered := make([]lunatask.Note, 0, len(notes))
-
-	for _, note := range notes {
-		if note.NotebookID != nil && *note.NotebookID == notebookID {
-			filtered = append(filtered, note)
-		}
-	}
-
-	return filtered
-}
-
-func formatListText(items []ListNoteItem, notebooks []shared.NotebookProvider) string {
-	if len(items) == 0 {
-		return "No notes found"
-	}
-
-	var text strings.Builder
-
-	text.WriteString(fmt.Sprintf("Found %d note(s):\n", len(items)))
-
-	for _, item := range items {
-		text.WriteString("- ")
-		text.WriteString(item.DeepLink)
-
-		var details []string
-
-		if item.NotebookID != nil {
-			nbName := *item.NotebookID
-			for _, nb := range notebooks {
-				if nb.ID == *item.NotebookID {
-					nbName = nb.Key
-
-					break
-				}
-			}
-
-			details = append(details, "notebook: "+nbName)
-		}
-
-		if item.Pinned {
-			details = append(details, "pinned")
-		}
-
-		if len(details) > 0 {
-			text.WriteString(" (")
-			text.WriteString(strings.Join(details, ", "))
-			text.WriteString(")")
-		}
-
-		text.WriteString("\n")
-	}
-
-	text.WriteString("\nUse lunatask://note/{id} resource for full details.")
-
-	return text.String()
-}

internal/mcp/tools/note/show.go 🔗

@@ -1,142 +0,0 @@
-// 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()
-}

internal/mcp/tools/note/update.go 🔗

@@ -1,110 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package note
-
-import (
-	"context"
-	"fmt"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/dateutil"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// UpdateToolName is the name of the update note tool.
-const UpdateToolName = "update_note"
-
-// UpdateToolDescription describes the update note tool for LLMs.
-const UpdateToolDescription = `Updates an existing note in Lunatask.
-
-Required:
-- id: Note UUID or lunatask://note/... deep link
-
-Optional:
-- name: New note title
-- notebook_id: Move to notebook (UUID from lunatask://notebooks)
-- content: Replace content (Markdown)
-- date: Note date (YYYY-MM-DD or natural language)
-
-Only provided fields are modified; other fields remain unchanged.`
-
-// UpdateInput is the input schema for updating a note.
-type UpdateInput struct {
-	ID         string  `json:"id"                    jsonschema:"required"`
-	Name       *string `json:"name,omitempty"`
-	NotebookID *string `json:"notebook_id,omitempty"`
-	Content    *string `json:"content,omitempty"`
-	Date       *string `json:"date,omitempty"`
-}
-
-// UpdateOutput is the output schema for updating a note.
-type UpdateOutput struct {
-	DeepLink string `json:"deep_link"`
-}
-
-// HandleUpdate updates an existing note.
-func (h *Handler) HandleUpdate(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input UpdateInput,
-) (*mcp.CallToolResult, UpdateOutput, error) {
-	id, err := parseReference(input.ID)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
-	}
-
-	if input.NotebookID != nil {
-		if err := lunatask.ValidateUUID(*input.NotebookID); err != nil {
-			return shared.ErrorResult("invalid notebook_id: expected UUID"), UpdateOutput{}, nil
-		}
-	}
-
-	builder := h.client.NewNoteUpdate(id)
-
-	if input.Name != nil {
-		builder.WithName(*input.Name)
-	}
-
-	if input.NotebookID != nil {
-		builder.InNotebook(*input.NotebookID)
-	}
-
-	if input.Content != nil {
-		builder.WithContent(*input.Content)
-	}
-
-	if input.Date != nil {
-		date, err := dateutil.Parse(*input.Date)
-		if err != nil {
-			return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
-		}
-
-		builder.OnDate(date)
-	}
-
-	note, err := builder.Update(ctx)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{
-			Text: "Note updated: " + deepLink,
-		}},
-	}, UpdateOutput{DeepLink: deepLink}, nil
-}
-
-// parseReference extracts UUID from either a raw UUID or a lunatask:// deep link.
-func parseReference(ref string) (string, error) {
-	_, id, err := lunatask.ParseReference(ref)
-	if err != nil {
-		return "", fmt.Errorf("invalid ID: %w", err)
-	}
-
-	return id, nil
-}

internal/mcp/tools/notebook/list.go 🔗

@@ -1,94 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package notebook provides MCP tools for notebook operations.
-package notebook
-
-import (
-	"context"
-	"fmt"
-	"strings"
-
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// ListToolName is the name of the list notebooks tool.
-const ListToolName = "list_notebooks"
-
-// ListToolDescription describes the list notebooks tool for LLMs.
-const ListToolDescription = `Lists configured notebooks. Fallback for lunatask://notebooks resource.
-
-Returns notebook metadata (IDs, names, keys).
-Use notebook IDs when creating or updating notes.`
-
-// ListInput is the input schema for listing notebooks.
-type ListInput struct{}
-
-// Summary represents a notebook in the list output.
-type Summary struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-	Key  string `json:"key"`
-}
-
-// ListOutput is the output schema for listing notebooks.
-type ListOutput struct {
-	Notebooks []Summary `json:"notebooks"`
-	Count     int       `json:"count"`
-}
-
-// Handler handles notebook tool requests.
-type Handler struct {
-	notebooks []shared.NotebookProvider
-}
-
-// NewHandler creates a new notebook tool handler.
-func NewHandler(notebooks []shared.NotebookProvider) *Handler {
-	return &Handler{notebooks: notebooks}
-}
-
-// HandleList lists configured notebooks.
-func (h *Handler) HandleList(
-	_ context.Context,
-	_ *mcp.CallToolRequest,
-	_ ListInput,
-) (*mcp.CallToolResult, ListOutput, error) {
-	summaries := make([]Summary, 0, len(h.notebooks))
-
-	for _, nb := range h.notebooks {
-		summaries = append(summaries, Summary{
-			ID:   nb.ID,
-			Name: nb.Name,
-			Key:  nb.Key,
-		})
-	}
-
-	output := ListOutput{
-		Notebooks: summaries,
-		Count:     len(summaries),
-	}
-
-	text := formatListText(summaries)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-func formatListText(notebooks []Summary) string {
-	if len(notebooks) == 0 {
-		return "No notebooks configured"
-	}
-
-	var text strings.Builder
-
-	text.WriteString(fmt.Sprintf("Found %d notebook(s):\n", len(notebooks)))
-
-	for _, nb := range notebooks {
-		text.WriteString(fmt.Sprintf("- %s (key: %s, id: %s)\n", nb.Name, nb.Key, nb.ID))
-	}
-
-	return text.String()
-}

internal/mcp/tools/person/create.go 🔗

@@ -1,99 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package person provides MCP tools for Lunatask person/relationship operations.
-package person
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// CreateToolName is the name of the create person tool.
-const CreateToolName = "create_person"
-
-// CreateToolDescription describes the create person tool for LLMs.
-const CreateToolDescription = `Creates a new person in Lunatask's relationship tracker.
-
-Required:
-- first_name: First name
-
-Optional:
-- last_name: Last name
-- relationship: Relationship strength (family, intimate-friends, close-friends,
-  casual-friends, acquaintances, business-contacts, almost-strangers)
-  Default: casual-friends
-- source: Origin identifier for integrations
-- source_id: Source-specific ID (requires source)
-
-Returns the deep link to the created person.`
-
-// CreateInput is the input schema for creating a person.
-type CreateInput struct {
-	FirstName    string  `json:"first_name"             jsonschema:"required"`
-	LastName     *string `json:"last_name,omitempty"`
-	Relationship *string `json:"relationship,omitempty"`
-	Source       *string `json:"source,omitempty"`
-	SourceID     *string `json:"source_id,omitempty"`
-}
-
-// CreateOutput is the output schema for creating a person.
-type CreateOutput struct {
-	DeepLink string `json:"deep_link"`
-}
-
-// Handler handles person-related MCP tool requests.
-type Handler struct {
-	client *lunatask.Client
-}
-
-// NewHandler creates a new person handler.
-func NewHandler(accessToken string) *Handler {
-	return &Handler{
-		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
-	}
-}
-
-// HandleCreate creates a new person.
-func (h *Handler) HandleCreate(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input CreateInput,
-) (*mcp.CallToolResult, CreateOutput, error) {
-	lastName := ""
-	if input.LastName != nil {
-		lastName = *input.LastName
-	}
-
-	builder := h.client.NewPerson(input.FirstName, lastName)
-
-	if input.Relationship != nil {
-		rel, err := lunatask.ParseRelationshipStrength(*input.Relationship)
-		if err != nil {
-			return shared.ErrorResult(err.Error()), CreateOutput{}, nil
-		}
-
-		builder.WithRelationshipStrength(rel)
-	}
-
-	if input.Source != nil && input.SourceID != nil {
-		builder.FromSource(*input.Source, *input.SourceID)
-	}
-
-	person, err := builder.Create(ctx)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{
-			Text: "Person created: " + deepLink,
-		}},
-	}, CreateOutput{DeepLink: deepLink}, nil
-}

internal/mcp/tools/person/delete.go 🔗

@@ -1,63 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package person
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// DeleteToolName is the name of the delete person tool.
-const DeleteToolName = "delete_person"
-
-// DeleteToolDescription describes the delete person tool for LLMs.
-const DeleteToolDescription = `Deletes a person from Lunatask.
-
-Required:
-- id: Person UUID or lunatask://person/... deep link
-
-This action is permanent and cannot be undone.`
-
-// DeleteInput is the input schema for deleting a person.
-type DeleteInput struct {
-	ID string `json:"id" jsonschema:"required"`
-}
-
-// DeleteOutput is the output schema for deleting a person.
-type DeleteOutput struct {
-	Success  bool   `json:"success"`
-	DeepLink string `json:"deep_link"`
-}
-
-// HandleDelete deletes a person.
-func (h *Handler) HandleDelete(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input DeleteInput,
-) (*mcp.CallToolResult, DeleteOutput, error) {
-	id, err := parseReference(input.ID)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), DeleteOutput{}, nil
-	}
-
-	person, err := h.client.DeletePerson(ctx, id)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), DeleteOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
-
-	return &mcp.CallToolResult{
-			Content: []mcp.Content{&mcp.TextContent{
-				Text: "Person deleted: " + deepLink,
-			}},
-		}, DeleteOutput{
-			Success:  true,
-			DeepLink: deepLink,
-		}, nil
-}

internal/mcp/tools/person/list.go 🔗

@@ -1,138 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package person
-
-import (
-	"context"
-	"fmt"
-	"strings"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// ListToolName is the name of the list people tool.
-const ListToolName = "list_people"
-
-// ListToolDescription describes the list people tool for LLMs.
-const ListToolDescription = `Lists people from Lunatask's relationship tracker.
-
-Optional:
-- source: Filter by source identifier
-- source_id: Filter by source-specific ID
-
-Returns person metadata (IDs, relationship strength, created dates).
-Use lunatask://person/{id} resource for full person details.
-
-Note: Due to end-to-end encryption, names are not available
-in the list — only metadata is returned.`
-
-// ListInput is the input schema for listing people.
-type ListInput struct {
-	Source   *string `json:"source,omitempty"`
-	SourceID *string `json:"source_id,omitempty"`
-}
-
-// ListPersonItem represents a person in the list output.
-type ListPersonItem struct {
-	DeepLink             string  `json:"deep_link"`
-	RelationshipStrength *string `json:"relationship_strength,omitempty"`
-	CreatedAt            string  `json:"created_at"`
-}
-
-// ListOutput is the output schema for listing people.
-type ListOutput struct {
-	People []ListPersonItem `json:"people"`
-	Count  int              `json:"count"`
-}
-
-// HandleList lists people.
-func (h *Handler) HandleList(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input ListInput,
-) (*mcp.CallToolResult, ListOutput, error) {
-	opts := buildListOptions(input)
-
-	people, err := h.client.ListPeople(ctx, opts)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), ListOutput{}, nil
-	}
-
-	items := make([]ListPersonItem, 0, len(people))
-
-	for _, person := range people {
-		deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
-
-		item := ListPersonItem{
-			DeepLink:  deepLink,
-			CreatedAt: person.CreatedAt.Format("2006-01-02"),
-		}
-
-		if person.RelationshipStrength != nil {
-			rel := string(*person.RelationshipStrength)
-			item.RelationshipStrength = &rel
-		}
-
-		items = append(items, item)
-	}
-
-	output := ListOutput{
-		People: items,
-		Count:  len(items),
-	}
-
-	text := formatListText(items)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-func buildListOptions(input ListInput) *lunatask.ListPeopleOptions {
-	if input.Source == nil && input.SourceID == nil {
-		return nil
-	}
-
-	opts := &lunatask.ListPeopleOptions{}
-
-	if input.Source != nil {
-		opts.Source = input.Source
-	}
-
-	if input.SourceID != nil {
-		opts.SourceID = input.SourceID
-	}
-
-	return opts
-}
-
-func formatListText(items []ListPersonItem) string {
-	if len(items) == 0 {
-		return "No people found"
-	}
-
-	var text strings.Builder
-
-	text.WriteString(fmt.Sprintf("Found %d person(s):\n", len(items)))
-
-	for _, item := range items {
-		text.WriteString("- ")
-		text.WriteString(item.DeepLink)
-
-		if item.RelationshipStrength != nil {
-			text.WriteString(" (")
-			text.WriteString(*item.RelationshipStrength)
-			text.WriteString(")")
-		}
-
-		text.WriteString("\n")
-	}
-
-	text.WriteString("\nUse lunatask://person/{id} resource for full details.")
-
-	return text.String()
-}

internal/mcp/tools/person/show.go 🔗

@@ -1,121 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package person
-
-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 person tool.
-const ShowToolName = "show_person"
-
-// ShowToolDescription describes the show person tool for LLMs.
-const ShowToolDescription = `Shows metadata for a specific person from Lunatask.
-
-Required:
-- id: Person UUID or lunatask:// deep link
-
-Note: Due to end-to-end encryption, person name is not available.
-Only metadata (relationship strength, sources) is returned.`
-
-// ShowInput is the input schema for showing a person.
-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 person.
-type ShowOutput struct {
-	DeepLink             string       `json:"deep_link"`
-	RelationshipStrength *string      `json:"relationship_strength,omitempty"`
-	Sources              []ShowSource `json:"sources,omitempty"`
-	CreatedAt            string       `json:"created_at"`
-	UpdatedAt            string       `json:"updated_at"`
-}
-
-// HandleShow shows a person'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
-	}
-
-	person, err := h.client.GetPerson(ctx, id)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), ShowOutput{}, nil
-	}
-
-	output := buildShowOutput(person)
-	text := formatPersonShowText(output)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-func buildShowOutput(person *lunatask.Person) ShowOutput {
-	output := ShowOutput{
-		CreatedAt: person.CreatedAt.Format(time.RFC3339),
-		UpdatedAt: person.UpdatedAt.Format(time.RFC3339),
-	}
-
-	output.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
-
-	if person.RelationshipStrength != nil {
-		s := string(*person.RelationshipStrength)
-		output.RelationshipStrength = &s
-	}
-
-	if len(person.Sources) > 0 {
-		output.Sources = make([]ShowSource, 0, len(person.Sources))
-		for _, src := range person.Sources {
-			output.Sources = append(output.Sources, ShowSource{
-				Source:   src.Source,
-				SourceID: src.SourceID,
-			})
-		}
-	}
-
-	return output
-}
-
-func formatPersonShowText(output ShowOutput) string {
-	var builder strings.Builder
-
-	builder.WriteString(fmt.Sprintf("Person: %s\n", output.DeepLink))
-
-	if output.RelationshipStrength != nil {
-		builder.WriteString(fmt.Sprintf("Relationship: %s\n", *output.RelationshipStrength))
-	}
-
-	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()
-}

internal/mcp/tools/person/update.go 🔗

@@ -1,98 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package person
-
-import (
-	"context"
-	"fmt"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// UpdateToolName is the name of the update person tool.
-const UpdateToolName = "update_person"
-
-// UpdateToolDescription describes the update person tool for LLMs.
-const UpdateToolDescription = `Updates an existing person in Lunatask.
-
-Required:
-- id: Person UUID or lunatask://person/... deep link
-
-Optional:
-- first_name: New first name
-- last_name: New last name
-- relationship: New relationship strength (family, intimate-friends,
-  close-friends, casual-friends, acquaintances, business-contacts, almost-strangers)
-
-Only provided fields are modified; other fields remain unchanged.`
-
-// UpdateInput is the input schema for updating a person.
-type UpdateInput struct {
-	ID           string  `json:"id"                     jsonschema:"required"`
-	FirstName    *string `json:"first_name,omitempty"`
-	LastName     *string `json:"last_name,omitempty"`
-	Relationship *string `json:"relationship,omitempty"`
-}
-
-// UpdateOutput is the output schema for updating a person.
-type UpdateOutput struct {
-	DeepLink string `json:"deep_link"`
-}
-
-// HandleUpdate updates an existing person.
-func (h *Handler) HandleUpdate(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input UpdateInput,
-) (*mcp.CallToolResult, UpdateOutput, error) {
-	id, err := parseReference(input.ID)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
-	}
-
-	builder := h.client.NewPersonUpdate(id)
-
-	if input.FirstName != nil {
-		builder.FirstName(*input.FirstName)
-	}
-
-	if input.LastName != nil {
-		builder.LastName(*input.LastName)
-	}
-
-	if input.Relationship != nil {
-		rel, err := lunatask.ParseRelationshipStrength(*input.Relationship)
-		if err != nil {
-			return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
-		}
-
-		builder.WithRelationshipStrength(rel)
-	}
-
-	person, err := builder.Update(ctx)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{
-			Text: "Person updated: " + deepLink,
-		}},
-	}, UpdateOutput{DeepLink: deepLink}, nil
-}
-
-// parseReference extracts UUID from either a raw UUID or a lunatask:// deep link.
-func parseReference(ref string) (string, error) {
-	_, id, err := lunatask.ParseReference(ref)
-	if err != nil {
-		return "", fmt.Errorf("invalid ID: %w", err)
-	}
-
-	return id, nil
-}

internal/mcp/tools/task/create.go 🔗

@@ -1,239 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package task provides MCP tools for Lunatask task operations.
-package task
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/dateutil"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"git.secluded.site/lune/internal/validate"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// CreateToolName is the name of the create task tool.
-const CreateToolName = "create_task"
-
-// CreateToolDescription describes the create task tool for LLMs.
-const CreateToolDescription = `Creates a new task in Lunatask.
-
-Required:
-- name: Task title
-- area_id: Area UUID, lunatask:// deep link, or config key
-
-Optional:
-- goal_id: Goal UUID, lunatask:// deep link, or config key (requires area_id)
-- status: later, next, started, waiting (default: later)
-- note: Markdown note/description for the task
-- priority: lowest, low, normal, high, highest
-- estimate: Time estimate in minutes (0-720)
-- motivation: must, should, want
-- important: true/false for Eisenhower matrix
-- urgent: true/false for Eisenhower matrix
-- scheduled_on: Date to schedule (YYYY-MM-DD or natural language)
-
-Returns the created task's ID and deep link.`
-
-// CreateInput is the input schema for creating a task.
-type CreateInput struct {
-	Name        string  `json:"name"                   jsonschema:"required"`
-	AreaID      string  `json:"area_id"                jsonschema:"required"`
-	GoalID      *string `json:"goal_id,omitempty"`
-	Status      *string `json:"status,omitempty"`
-	Note        *string `json:"note,omitempty"`
-	Priority    *string `json:"priority,omitempty"`
-	Estimate    *int    `json:"estimate,omitempty"`
-	Motivation  *string `json:"motivation,omitempty"`
-	Important   *bool   `json:"important,omitempty"`
-	Urgent      *bool   `json:"urgent,omitempty"`
-	ScheduledOn *string `json:"scheduled_on,omitempty"`
-}
-
-// CreateOutput is the output schema for creating a task.
-type CreateOutput struct {
-	DeepLink string `json:"deep_link"`
-}
-
-// parsedCreateInput holds validated and parsed create input fields.
-type parsedCreateInput struct {
-	Name        string
-	AreaID      string
-	GoalID      *string
-	Status      *lunatask.TaskStatus
-	Note        *string
-	Priority    *lunatask.Priority
-	Estimate    *int
-	Motivation  *lunatask.Motivation
-	Important   *bool
-	Urgent      *bool
-	ScheduledOn *lunatask.Date
-}
-
-// Handler handles task-related MCP tool requests.
-type Handler struct {
-	client *lunatask.Client
-	cfg    *config.Config
-	areas  []shared.AreaProvider
-}
-
-// NewHandler creates a new task handler.
-func NewHandler(accessToken string, cfg *config.Config, areas []shared.AreaProvider) *Handler {
-	return &Handler{
-		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
-		cfg:    cfg,
-		areas:  areas,
-	}
-}
-
-// HandleCreate creates a new task.
-func (h *Handler) HandleCreate(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input CreateInput,
-) (*mcp.CallToolResult, CreateOutput, error) {
-	parsed, errResult := parseCreateInput(h.cfg, input)
-	if errResult != nil {
-		return errResult, CreateOutput{}, nil
-	}
-
-	builder := h.client.NewTask(parsed.Name)
-	applyToTaskBuilder(builder, parsed)
-
-	task, err := builder.Create(ctx)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{
-			Text: "Task created: " + deepLink,
-		}},
-	}, CreateOutput{DeepLink: deepLink}, nil
-}
-
-//nolint:cyclop,funlen
-func parseCreateInput(cfg *config.Config, input CreateInput) (*parsedCreateInput, *mcp.CallToolResult) {
-	parsed := &parsedCreateInput{
-		Name:      input.Name,
-		Note:      input.Note,
-		Estimate:  input.Estimate,
-		Important: input.Important,
-		Urgent:    input.Urgent,
-	}
-
-	areaID, err := validate.AreaRef(cfg, input.AreaID)
-	if err != nil {
-		return nil, shared.ErrorResult(err.Error())
-	}
-
-	parsed.AreaID = areaID
-
-	if input.GoalID != nil {
-		goalID, err := validate.GoalRef(cfg, parsed.AreaID, *input.GoalID)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.GoalID = &goalID
-	}
-
-	if input.Estimate != nil {
-		if err := shared.ValidateEstimate(*input.Estimate); err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-	}
-
-	if input.Status != nil {
-		status, err := lunatask.ParseTaskStatus(*input.Status)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.Status = &status
-	}
-
-	if input.Priority != nil {
-		priority, err := lunatask.ParsePriority(*input.Priority)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.Priority = &priority
-	}
-
-	if input.Motivation != nil {
-		motivation, err := lunatask.ParseMotivation(*input.Motivation)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.Motivation = &motivation
-	}
-
-	if input.ScheduledOn != nil {
-		date, err := dateutil.Parse(*input.ScheduledOn)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.ScheduledOn = &date
-	}
-
-	return parsed, nil
-}
-
-//nolint:cyclop
-func applyToTaskBuilder(builder *lunatask.TaskBuilder, parsed *parsedCreateInput) {
-	builder.InArea(parsed.AreaID)
-
-	if parsed.GoalID != nil {
-		builder.InGoal(*parsed.GoalID)
-	}
-
-	if parsed.Status != nil {
-		builder.WithStatus(*parsed.Status)
-	}
-
-	if parsed.Note != nil {
-		builder.WithNote(*parsed.Note)
-	}
-
-	if parsed.Priority != nil {
-		builder.Priority(*parsed.Priority)
-	}
-
-	if parsed.Estimate != nil {
-		builder.WithEstimate(*parsed.Estimate)
-	}
-
-	if parsed.Motivation != nil {
-		builder.WithMotivation(*parsed.Motivation)
-	}
-
-	if parsed.Important != nil {
-		if *parsed.Important {
-			builder.Important()
-		} else {
-			builder.NotImportant()
-		}
-	}
-
-	if parsed.Urgent != nil {
-		if *parsed.Urgent {
-			builder.Urgent()
-		} else {
-			builder.NotUrgent()
-		}
-	}
-
-	if parsed.ScheduledOn != nil {
-		builder.ScheduledOn(*parsed.ScheduledOn)
-	}
-}

internal/mcp/tools/task/delete.go 🔗

@@ -1,59 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package task
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// DeleteToolName is the name of the delete task tool.
-const DeleteToolName = "delete_task"
-
-// DeleteToolDescription describes the delete task tool for LLMs.
-const DeleteToolDescription = `Deletes a task from Lunatask.
-
-Required:
-- id: Task UUID or lunatask:// deep link
-
-This action is permanent and cannot be undone.`
-
-// DeleteInput is the input schema for deleting a task.
-type DeleteInput struct {
-	ID string `json:"id" jsonschema:"required"`
-}
-
-// DeleteOutput is the output schema for deleting a task.
-type DeleteOutput struct {
-	Success  bool   `json:"success"`
-	DeepLink string `json:"deep_link"`
-}
-
-// HandleDelete deletes a task.
-func (h *Handler) HandleDelete(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input DeleteInput,
-) (*mcp.CallToolResult, DeleteOutput, error) {
-	_, id, err := lunatask.ParseReference(input.ID)
-	if err != nil {
-		return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"), DeleteOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, id)
-
-	if _, err := h.client.DeleteTask(ctx, id); err != nil {
-		return shared.ErrorResult(err.Error()), DeleteOutput{}, nil
-	}
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{
-			Text: "Task deleted: " + deepLink,
-		}},
-	}, DeleteOutput{Success: true, DeepLink: deepLink}, nil
-}

internal/mcp/tools/task/list.go 🔗

@@ -1,158 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package task
-
-import (
-	"context"
-	"fmt"
-	"strings"
-	"time"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// ListToolName is the name of the list tasks tool.
-const ListToolName = "list_tasks"
-
-// ListToolDescription describes the list tasks tool for LLMs.
-const ListToolDescription = `Lists tasks from Lunatask.
-
-Optional filters:
-- area_id: Filter by area UUID
-- status: Filter by status (later, next, started, waiting, completed)
-- include_completed: Include completed tasks (default: false, only shows today's)
-
-Note: Due to end-to-end encryption, task names and notes are not available.
-Only metadata (ID, status, dates, priority, etc.) is returned.
-
-Returns a list of tasks with their metadata and deep links.`
-
-// ListInput is the input schema for listing tasks.
-type ListInput struct {
-	AreaID           *string `json:"area_id,omitempty"`
-	Status           *string `json:"status,omitempty"`
-	IncludeCompleted *bool   `json:"include_completed,omitempty"`
-}
-
-// ListOutput is the output schema for listing tasks.
-type ListOutput struct {
-	Tasks []Summary `json:"tasks"`
-	Count int       `json:"count"`
-}
-
-// Summary represents a task in list output.
-type Summary struct {
-	DeepLink    string  `json:"deep_link"`
-	Status      *string `json:"status,omitempty"`
-	Priority    *int    `json:"priority,omitempty"`
-	ScheduledOn *string `json:"scheduled_on,omitempty"`
-	CreatedAt   string  `json:"created_at"`
-	AreaID      *string `json:"area_id,omitempty"`
-	GoalID      *string `json:"goal_id,omitempty"`
-}
-
-// HandleList lists tasks.
-func (h *Handler) HandleList(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input ListInput,
-) (*mcp.CallToolResult, ListOutput, error) {
-	if input.AreaID != nil {
-		if err := lunatask.ValidateUUID(*input.AreaID); err != nil {
-			return shared.ErrorResult("invalid area_id: expected UUID"), ListOutput{}, nil
-		}
-	}
-
-	if input.Status != nil {
-		if _, err := lunatask.ParseTaskStatus(*input.Status); err != nil {
-			return shared.ErrorResult("invalid status: must be later, next, started, waiting, or completed"), ListOutput{}, nil
-		}
-	}
-
-	tasks, err := h.client.ListTasks(ctx, nil)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), ListOutput{}, nil
-	}
-
-	opts := &lunatask.TaskFilterOptions{
-		AreaID:           input.AreaID,
-		IncludeCompleted: input.IncludeCompleted != nil && *input.IncludeCompleted,
-		Today:            time.Now(),
-	}
-
-	if input.Status != nil {
-		s := lunatask.TaskStatus(*input.Status)
-		opts.Status = &s
-	}
-
-	filtered := lunatask.FilterTasks(tasks, opts)
-	summaries := buildSummaries(filtered)
-	text := formatListText(summaries)
-
-	return &mcp.CallToolResult{
-			Content: []mcp.Content{&mcp.TextContent{Text: text}},
-		}, ListOutput{
-			Tasks: summaries,
-			Count: len(summaries),
-		}, nil
-}
-
-func buildSummaries(tasks []lunatask.Task) []Summary {
-	summaries := make([]Summary, 0, len(tasks))
-
-	for _, task := range tasks {
-		summary := Summary{
-			CreatedAt: task.CreatedAt.Format(time.RFC3339),
-			AreaID:    task.AreaID,
-			GoalID:    task.GoalID,
-		}
-
-		summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
-
-		if task.Status != nil {
-			s := string(*task.Status)
-			summary.Status = &s
-		}
-
-		if task.Priority != nil {
-			p := int(*task.Priority)
-			summary.Priority = &p
-		}
-
-		if task.ScheduledOn != nil {
-			s := task.ScheduledOn.Format("2006-01-02")
-			summary.ScheduledOn = &s
-		}
-
-		summaries = append(summaries, summary)
-	}
-
-	return summaries
-}
-
-func formatListText(summaries []Summary) string {
-	if len(summaries) == 0 {
-		return "No tasks found."
-	}
-
-	var builder strings.Builder
-
-	builder.WriteString(fmt.Sprintf("Found %d task(s):\n", len(summaries)))
-
-	for _, summary := range summaries {
-		status := "unknown"
-		if summary.Status != nil {
-			status = *summary.Status
-		}
-
-		builder.WriteString(fmt.Sprintf("- %s (%s)\n", summary.DeepLink, status))
-	}
-
-	builder.WriteString("\nUse show_task for full details.")
-
-	return builder.String()
-}

internal/mcp/tools/task/show.go 🔗

@@ -1,162 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package task
-
-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 task tool.
-const ShowToolName = "show_task"
-
-// ShowToolDescription describes the show task tool for LLMs.
-const ShowToolDescription = `Shows details of a specific task from Lunatask.
-
-Required:
-- id: Task UUID or lunatask:// deep link
-
-Note: Due to end-to-end encryption, task name and note content are not available.
-Only metadata (ID, status, dates, priority, etc.) is returned.`
-
-// ShowInput is the input schema for showing a task.
-type ShowInput struct {
-	ID string `json:"id" jsonschema:"required"`
-}
-
-// ShowOutput is the output schema for showing a task.
-type ShowOutput struct {
-	DeepLink    string  `json:"deep_link"`
-	Status      *string `json:"status,omitempty"`
-	Priority    *int    `json:"priority,omitempty"`
-	Estimate    *int    `json:"estimate,omitempty"`
-	ScheduledOn *string `json:"scheduled_on,omitempty"`
-	CompletedAt *string `json:"completed_at,omitempty"`
-	CreatedAt   string  `json:"created_at"`
-	UpdatedAt   string  `json:"updated_at"`
-	AreaID      *string `json:"area_id,omitempty"`
-	GoalID      *string `json:"goal_id,omitempty"`
-	Important   *bool   `json:"important,omitempty"`
-	Urgent      *bool   `json:"urgent,omitempty"`
-}
-
-// HandleShow shows a task'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
-	}
-
-	task, err := h.client.GetTask(ctx, id)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), ShowOutput{}, nil
-	}
-
-	output := ShowOutput{
-		CreatedAt: task.CreatedAt.Format(time.RFC3339),
-		UpdatedAt: task.UpdatedAt.Format(time.RFC3339),
-		AreaID:    task.AreaID,
-		GoalID:    task.GoalID,
-	}
-
-	output.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
-
-	if task.Status != nil {
-		s := string(*task.Status)
-		output.Status = &s
-	}
-
-	if task.Priority != nil {
-		p := int(*task.Priority)
-		output.Priority = &p
-	}
-
-	if task.Estimate != nil {
-		output.Estimate = task.Estimate
-	}
-
-	if task.ScheduledOn != nil {
-		s := task.ScheduledOn.Format("2006-01-02")
-		output.ScheduledOn = &s
-	}
-
-	if task.CompletedAt != nil {
-		s := task.CompletedAt.Format(time.RFC3339)
-		output.CompletedAt = &s
-	}
-
-	if task.Eisenhower != nil {
-		important := task.Eisenhower.IsImportant()
-		urgent := task.Eisenhower.IsUrgent()
-		output.Important = &important
-		output.Urgent = &urgent
-	}
-
-	text := formatShowText(output)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{Text: text}},
-	}, output, nil
-}
-
-func formatShowText(output ShowOutput) string {
-	var builder strings.Builder
-
-	builder.WriteString(fmt.Sprintf("Task: %s\n", output.DeepLink))
-	writeOptionalField(&builder, "Status", output.Status)
-	writeOptionalIntField(&builder, "Priority", output.Priority)
-	writeOptionalField(&builder, "Scheduled", output.ScheduledOn)
-	writeOptionalMinutesField(&builder, "Estimate", output.Estimate)
-	writeEisenhowerField(&builder, output.Important, output.Urgent)
-	builder.WriteString(fmt.Sprintf("Created: %s\n", output.CreatedAt))
-	builder.WriteString("Updated: " + output.UpdatedAt)
-	writeOptionalField(&builder, "\nCompleted", output.CompletedAt)
-
-	return builder.String()
-}
-
-func writeOptionalField(builder *strings.Builder, label string, value *string) {
-	if value != nil {
-		fmt.Fprintf(builder, "%s: %s\n", label, *value)
-	}
-}
-
-func writeOptionalIntField(builder *strings.Builder, label string, value *int) {
-	if value != nil {
-		fmt.Fprintf(builder, "%s: %d\n", label, *value)
-	}
-}
-
-func writeOptionalMinutesField(builder *strings.Builder, label string, value *int) {
-	if value != nil {
-		fmt.Fprintf(builder, "%s: %d min\n", label, *value)
-	}
-}
-
-func writeEisenhowerField(builder *strings.Builder, important, urgent *bool) {
-	var parts []string
-
-	if important != nil && *important {
-		parts = append(parts, "important")
-	}
-
-	if urgent != nil && *urgent {
-		parts = append(parts, "urgent")
-	}
-
-	if len(parts) > 0 {
-		fmt.Fprintf(builder, "Eisenhower: %s\n", strings.Join(parts, ", "))
-	}
-}

internal/mcp/tools/task/update.go 🔗

@@ -1,244 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package task
-
-import (
-	"context"
-
-	"git.secluded.site/go-lunatask"
-	"git.secluded.site/lune/internal/config"
-	"git.secluded.site/lune/internal/dateutil"
-	"git.secluded.site/lune/internal/mcp/shared"
-	"git.secluded.site/lune/internal/validate"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
-)
-
-// UpdateToolName is the name of the update task tool.
-const UpdateToolName = "update_task"
-
-// UpdateToolDescription describes the update task tool for LLMs.
-const UpdateToolDescription = `Updates an existing task in Lunatask.
-
-Required:
-- id: Task UUID or lunatask:// deep link
-
-Optional (only specified fields are updated):
-- name: New task title
-- area_id: Move to area (UUID, lunatask:// deep link, or config key)
-- goal_id: Move to goal (UUID, lunatask:// deep link, or config key; requires area_id)
-- status: later, next, started, waiting, completed
-- note: New markdown note (replaces existing)
-- priority: lowest, low, normal, high, highest
-- estimate: Time estimate in minutes (0-720)
-- motivation: must, should, want
-- important: true/false for Eisenhower matrix
-- urgent: true/false for Eisenhower matrix
-- scheduled_on: Date to schedule (YYYY-MM-DD)
-
-Returns the updated task's ID and deep link.`
-
-// UpdateInput is the input schema for updating a task.
-type UpdateInput struct {
-	ID          string  `json:"id"                     jsonschema:"required"`
-	Name        *string `json:"name,omitempty"`
-	AreaID      *string `json:"area_id,omitempty"`
-	GoalID      *string `json:"goal_id,omitempty"`
-	Status      *string `json:"status,omitempty"`
-	Note        *string `json:"note,omitempty"`
-	Priority    *string `json:"priority,omitempty"`
-	Estimate    *int    `json:"estimate,omitempty"`
-	Motivation  *string `json:"motivation,omitempty"`
-	Important   *bool   `json:"important,omitempty"`
-	Urgent      *bool   `json:"urgent,omitempty"`
-	ScheduledOn *string `json:"scheduled_on,omitempty"`
-}
-
-// UpdateOutput is the output schema for updating a task.
-type UpdateOutput struct {
-	DeepLink string `json:"deep_link"`
-}
-
-// parsedUpdateInput holds validated and parsed update input fields.
-type parsedUpdateInput struct {
-	ID          string
-	Name        *string
-	AreaID      *string
-	GoalID      *string
-	Status      *lunatask.TaskStatus
-	Note        *string
-	Priority    *lunatask.Priority
-	Estimate    *int
-	Motivation  *lunatask.Motivation
-	Important   *bool
-	Urgent      *bool
-	ScheduledOn *lunatask.Date
-}
-
-// HandleUpdate updates an existing task.
-func (h *Handler) HandleUpdate(
-	ctx context.Context,
-	_ *mcp.CallToolRequest,
-	input UpdateInput,
-) (*mcp.CallToolResult, UpdateOutput, error) {
-	parsed, errResult := parseUpdateInput(h.cfg, input)
-	if errResult != nil {
-		return errResult, UpdateOutput{}, nil
-	}
-
-	builder := h.client.NewTaskUpdate(parsed.ID)
-	applyToTaskUpdateBuilder(builder, parsed)
-
-	task, err := builder.Update(ctx)
-	if err != nil {
-		return shared.ErrorResult(err.Error()), UpdateOutput{}, nil
-	}
-
-	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
-
-	return &mcp.CallToolResult{
-		Content: []mcp.Content{&mcp.TextContent{
-			Text: "Task updated: " + deepLink,
-		}},
-	}, UpdateOutput{DeepLink: deepLink}, nil
-}
-
-//nolint:cyclop,funlen
-func parseUpdateInput(cfg *config.Config, input UpdateInput) (*parsedUpdateInput, *mcp.CallToolResult) {
-	_, id, err := lunatask.ParseReference(input.ID)
-	if err != nil {
-		return nil, shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link")
-	}
-
-	parsed := &parsedUpdateInput{
-		ID:        id,
-		Name:      input.Name,
-		Note:      input.Note,
-		Estimate:  input.Estimate,
-		Important: input.Important,
-		Urgent:    input.Urgent,
-	}
-
-	if input.AreaID != nil {
-		areaID, err := validate.AreaRef(cfg, *input.AreaID)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.AreaID = &areaID
-	}
-
-	if input.GoalID != nil {
-		areaID := ""
-		if parsed.AreaID != nil {
-			areaID = *parsed.AreaID
-		}
-
-		goalID, err := validate.GoalRef(cfg, areaID, *input.GoalID)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.GoalID = &goalID
-	}
-
-	if input.Estimate != nil {
-		if err := shared.ValidateEstimate(*input.Estimate); err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-	}
-
-	if input.Status != nil {
-		status, err := lunatask.ParseTaskStatus(*input.Status)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.Status = &status
-	}
-
-	if input.Priority != nil {
-		priority, err := lunatask.ParsePriority(*input.Priority)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.Priority = &priority
-	}
-
-	if input.Motivation != nil {
-		motivation, err := lunatask.ParseMotivation(*input.Motivation)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.Motivation = &motivation
-	}
-
-	if input.ScheduledOn != nil {
-		date, err := dateutil.Parse(*input.ScheduledOn)
-		if err != nil {
-			return nil, shared.ErrorResult(err.Error())
-		}
-
-		parsed.ScheduledOn = &date
-	}
-
-	return parsed, nil
-}
-
-//nolint:cyclop
-func applyToTaskUpdateBuilder(builder *lunatask.TaskUpdateBuilder, parsed *parsedUpdateInput) {
-	if parsed.Name != nil {
-		builder.Name(*parsed.Name)
-	}
-
-	if parsed.AreaID != nil {
-		builder.InArea(*parsed.AreaID)
-	}
-
-	if parsed.GoalID != nil {
-		builder.InGoal(*parsed.GoalID)
-	}
-
-	if parsed.Status != nil {
-		builder.WithStatus(*parsed.Status)
-	}
-
-	if parsed.Note != nil {
-		builder.WithNote(*parsed.Note)
-	}
-
-	if parsed.Priority != nil {
-		builder.Priority(*parsed.Priority)
-	}
-
-	if parsed.Estimate != nil {
-		builder.WithEstimate(*parsed.Estimate)
-	}
-
-	if parsed.Motivation != nil {
-		builder.WithMotivation(*parsed.Motivation)
-	}
-
-	if parsed.Important != nil {
-		if *parsed.Important {
-			builder.Important()
-		} else {
-			builder.NotImportant()
-		}
-	}
-
-	if parsed.Urgent != nil {
-		if *parsed.Urgent {
-			builder.Urgent()
-		} else {
-			builder.NotUrgent()
-		}
-	}
-
-	if parsed.ScheduledOn != nil {
-		builder.ScheduledOn(*parsed.ScheduledOn)
-	}
-}

internal/mcp/tools/person/timeline.go → internal/mcp/tools/timeline/handler.go 🔗

@@ -2,7 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0-or-later
 
-package person
+// Package timeline provides the MCP tool for adding timeline notes to people.
+package timeline
 
 import (
 	"context"
@@ -13,11 +14,11 @@ import (
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
-// TimelineToolName is the name of the add timeline note tool.
-const TimelineToolName = "add_timeline_note"
+// ToolName is the name of the add timeline note tool.
+const ToolName = "add_timeline_note"
 
-// TimelineToolDescription describes the add timeline note tool for LLMs.
-const TimelineToolDescription = `Adds a timeline note to a person's memory timeline in Lunatask.
+// ToolDescription describes the add timeline note tool for LLMs.
+const ToolDescription = `Adds a timeline note to a person's memory timeline in Lunatask.
 
 Required:
 - person_id: Person UUID or lunatask://person/... deep link
@@ -29,29 +30,42 @@ Optional:
 This is append-only — adds to the person's memory timeline.
 Great for tracking when you last interacted with someone.`
 
-// TimelineInput is the input schema for adding a timeline note.
-type TimelineInput struct {
+// Input is the input schema for adding a timeline note.
+type Input struct {
 	PersonID string  `json:"person_id"         jsonschema:"required"`
 	Content  *string `json:"content,omitempty"`
 	Date     *string `json:"date,omitempty"`
 }
 
-// TimelineOutput is the output schema for adding a timeline note.
-type TimelineOutput struct {
+// Output is the output schema for adding a timeline note.
+type Output struct {
 	Success        bool   `json:"success"`
 	PersonDeepLink string `json:"person_deep_link"`
 	NoteID         string `json:"note_id"`
 }
 
-// HandleTimeline adds a timeline note to a person.
-func (h *Handler) HandleTimeline(
+// Handler handles timeline note MCP tool requests.
+type Handler struct {
+	client *lunatask.Client
+}
+
+// NewHandler creates a new timeline handler.
+func NewHandler(accessToken string) *Handler {
+	return &Handler{
+		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
+	}
+}
+
+// Handle adds a timeline note to a person.
+func (h *Handler) Handle(
 	ctx context.Context,
 	_ *mcp.CallToolRequest,
-	input TimelineInput,
-) (*mcp.CallToolResult, TimelineOutput, error) {
-	personID, err := parseReference(input.PersonID)
+	input Input,
+) (*mcp.CallToolResult, Output, error) {
+	_, personID, err := lunatask.ParseReference(input.PersonID)
 	if err != nil {
-		return shared.ErrorResult(err.Error()), TimelineOutput{}, nil
+		return shared.ErrorResult("invalid person_id: expected UUID or lunatask:// deep link"),
+			Output{}, nil
 	}
 
 	builder := h.client.NewTimelineNote(personID)
@@ -63,7 +77,7 @@ func (h *Handler) HandleTimeline(
 	if input.Date != nil {
 		date, err := dateutil.Parse(*input.Date)
 		if err != nil {
-			return shared.ErrorResult(err.Error()), TimelineOutput{}, nil
+			return shared.ErrorResult(err.Error()), Output{}, nil
 		}
 
 		builder.OnDate(date)
@@ -71,7 +85,7 @@ func (h *Handler) HandleTimeline(
 
 	note, err := builder.Create(ctx)
 	if err != nil {
-		return shared.ErrorResult(err.Error()), TimelineOutput{}, nil
+		return shared.ErrorResult(err.Error()), Output{}, nil
 	}
 
 	personDeepLink, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, personID)
@@ -87,7 +101,7 @@ func (h *Handler) HandleTimeline(
 			Content: []mcp.Content{&mcp.TextContent{
 				Text: "Timeline note added for " + personDeepLink + " on " + dateStr,
 			}},
-		}, TimelineOutput{
+		}, Output{
 			Success:        true,
 			PersonDeepLink: personDeepLink,
 			NoteID:         note.ID,