refactor(mcp): add schemas and annotations

Amolith created

- Add InputSchema() with enum constraints for CRUD tools
- Add ToolAnnotations() with behavioral hints (ReadOnlyHint,
DestructiveHint, IdempotentHint)
- Replace verbose descriptions with concise ones (schema conveys
  details)
- Use jsonschema:"description" tags instead of jsonschema:"required"
- Use go-lunatask All*() functions for type-safe enum values
- Bump go-lunatask to v0.1.0-rc9.3

Assisted-by: Claude Opus 4.5 via Crush

Change summary

cmd/mcp/server.go                       |  11 ++
go.mod                                  |   4 
go.sum                                  |   6 
internal/mcp/tools/crud/create.go       | 110 ++++++++++++--------------
internal/mcp/tools/crud/delete.go       |  50 ++++++++++-
internal/mcp/tools/crud/query.go        |  62 ++++++--------
internal/mcp/tools/crud/update.go       |  98 +++++++++++-------------
internal/mcp/tools/habit/track.go       |  20 ++--
internal/mcp/tools/timeline/handler.go  |  24 +++--
internal/mcp/tools/timestamp/handler.go |  29 +++---
10 files changed, 216 insertions(+), 198 deletions(-)

Detailed changes

cmd/mcp/server.go 🔗

@@ -347,6 +347,7 @@ func registerTools(
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        timestamp.ToolName,
 			Description: timestamp.ToolDescription,
+			Annotations: timestamp.ToolAnnotations(),
 		}, tsHandler.Handle)
 	}
 
@@ -355,6 +356,7 @@ func registerTools(
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        timeline.ToolName,
 			Description: timeline.ToolDescription,
+			Annotations: timeline.ToolAnnotations(),
 		}, timelineHandler.Handle)
 	}
 
@@ -363,6 +365,7 @@ func registerTools(
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        habit.TrackToolName,
 			Description: habit.TrackToolDescription,
+			Annotations: habit.TrackToolAnnotations(),
 		}, habitHandler.HandleTrack)
 	}
 
@@ -388,6 +391,8 @@ func registerCRUDTools(
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        crud.CreateToolName,
 			Description: crud.CreateToolDescription,
+			InputSchema: crud.CreateInputSchema(),
+			Annotations: crud.CreateToolAnnotations(),
 		}, crudHandler.HandleCreate)
 	}
 
@@ -395,6 +400,8 @@ func registerCRUDTools(
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        crud.UpdateToolName,
 			Description: crud.UpdateToolDescription,
+			InputSchema: crud.UpdateInputSchema(),
+			Annotations: crud.UpdateToolAnnotations(),
 		}, crudHandler.HandleUpdate)
 	}
 
@@ -402,6 +409,8 @@ func registerCRUDTools(
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        crud.DeleteToolName,
 			Description: crud.DeleteToolDescription,
+			InputSchema: crud.DeleteInputSchema(),
+			Annotations: crud.DeleteToolAnnotations(),
 		}, crudHandler.HandleDelete)
 	}
 
@@ -409,6 +418,8 @@ func registerCRUDTools(
 		mcp.AddTool(mcpServer, &mcp.Tool{
 			Name:        crud.QueryToolName,
 			Description: crud.QueryToolDescription,
+			InputSchema: crud.QueryInputSchema(),
+			Annotations: crud.QueryToolAnnotations(),
 		}, crudHandler.HandleQuery)
 	}
 }

go.mod 🔗

@@ -7,13 +7,14 @@ module git.secluded.site/lune
 go 1.25.5
 
 require (
-	git.secluded.site/go-lunatask v0.1.0-rc9.2
+	git.secluded.site/go-lunatask v0.1.0-rc9.3
 	github.com/BurntSushi/toml v1.6.0
 	github.com/KarpelesLab/strtotime v0.0.1
 	github.com/charmbracelet/fang v0.4.4
 	github.com/charmbracelet/huh v0.8.0
 	github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
 	github.com/charmbracelet/lipgloss v1.1.0
+	github.com/google/jsonschema-go v0.4.2
 	github.com/klauspost/lctime v0.1.0
 	github.com/mattn/go-isatty v0.0.20
 	github.com/modelcontextprotocol/go-sdk v1.2.0
@@ -46,7 +47,6 @@ require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/godbus/dbus/v5 v5.1.0 // indirect
-	github.com/google/jsonschema-go v0.4.2 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect

go.sum 🔗

@@ -2,8 +2,8 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXy
 al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
 charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
-git.secluded.site/go-lunatask v0.1.0-rc9.2 h1:fk5fCGdHmKpwz5HPy/n/LURBNweJoRN2xSay36VgA7g=
-git.secluded.site/go-lunatask v0.1.0-rc9.2/go.mod h1:rxps7BBqF+BkY8VN5E7J9zSOzSbtZ1hDmLEOHxjTHZQ=
+git.secluded.site/go-lunatask v0.1.0-rc9.3 h1:BrX6S7oQMylWAglSBXy7JjjwuSNYcPTGUlQTdwmdmFc=
+git.secluded.site/go-lunatask v0.1.0-rc9.3/go.mod h1:rxps7BBqF+BkY8VN5E7J9zSOzSbtZ1hDmLEOHxjTHZQ=
 github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
 github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/KarpelesLab/strtotime v0.0.1 h1:Af8vDb5RzwHhLP7ctSs7Y4CeiE+qlMXWWMqNd8xGOWY=
@@ -79,8 +79,6 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD
 github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
-github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
 github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
 github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=

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

@@ -13,6 +13,7 @@ import (
 	"git.secluded.site/lune/internal/dateutil"
 	"git.secluded.site/lune/internal/mcp/shared"
 	"git.secluded.site/lune/internal/validate"
+	"github.com/google/jsonschema-go/jsonschema"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
@@ -32,80 +33,67 @@ const (
 const CreateToolName = "create"
 
 // CreateToolDescription describes the create tool for LLMs.
-const CreateToolDescription = `Creates a new entity in Lunatask.
-
-Required:
-- entity: Type to create (task, note, person, journal)
-
-Entity-specific fields:
-
-**task** (requires name, area_id):
-- name: Task title
-- area_id: Area UUID, lunatask:// deep link, or config key
-- goal_id: Goal UUID, deep link, or config key (optional)
-- status: later, next, started, waiting (default: later)
-- note: Markdown note/description
-- 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)
-
-**note** (all fields optional):
-- name: Note title
-- notebook_id: Notebook UUID
-- content: Markdown content
-- source: Origin identifier for integrations
-- source_id: Source-specific ID (requires source)
-
-**person** (requires first_name):
-- first_name: First name
-- last_name: Last name
-- relationship: Relationship strength (family, intimate-friends, close-friends,
-  casual-friends, acquaintances, business-contacts, almost-strangers)
-- source: Origin identifier
-- source_id: Source-specific ID (requires source)
-
-**journal** (all fields optional):
-- name: Entry title (defaults to weekday name)
-- content: Markdown content
-- date: Entry date (YYYY-MM-DD or natural language, default: today)
-
-Returns the created entity's ID and deep link.`
+const CreateToolDescription = `Create a new entity in Lunatask.
+Required fields depend on entity type:
+- task: name, area_id
+- person: first_name
+- note, journal: all fields optional
+Returns the new entity's ID and deep link.`
+
+// CreateToolAnnotations returns hints about tool behavior.
+func CreateToolAnnotations() *mcp.ToolAnnotations {
+	return &mcp.ToolAnnotations{
+		DestructiveHint: ptr(false),
+	}
+}
+
+// CreateInputSchema returns a custom schema with enum constraints.
+func CreateInputSchema() *jsonschema.Schema {
+	schema, _ := jsonschema.For[CreateInput](nil)
+
+	schema.Properties["entity"].Enum = []any{
+		EntityTask, EntityNote, EntityPerson, EntityJournal,
+	}
+	schema.Properties["status"].Enum = toAnyStrings(lunatask.AllTaskStatuses())
+	schema.Properties["priority"].Enum = prioritiesToAny(lunatask.AllPriorities())
+	schema.Properties["motivation"].Enum = toAnyStrings(lunatask.AllMotivations())
+	schema.Properties["relationship"].Enum = toAnyStrings(lunatask.AllRelationshipStrengths())
+
+	return schema
+}
 
 // CreateInput is the input schema for the consolidated create tool.
 type CreateInput struct {
-	Entity string `json:"entity" jsonschema:"required"`
+	Entity string `json:"entity" jsonschema:"Entity type to create"`
 
 	// Common fields
-	Name     *string `json:"name,omitempty"`
-	Content  *string `json:"content,omitempty"`
-	Source   *string `json:"source,omitempty"`
-	SourceID *string `json:"source_id,omitempty"`
+	Name     *string `json:"name,omitempty"      jsonschema:"Title/name (required for task/person)"`
+	Content  *string `json:"content,omitempty"   jsonschema:"Markdown content"`
+	Source   *string `json:"source,omitempty"    jsonschema:"Origin identifier for integrations"`
+	SourceID *string `json:"source_id,omitempty" jsonschema:"Source-specific ID (requires source)"`
 
 	// Task-specific fields
-	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"`
+	AreaID      *string `json:"area_id,omitempty"      jsonschema:"Area (UUID, deep link, or key) - required for task"`
+	GoalID      *string `json:"goal_id,omitempty"      jsonschema:"Goal (UUID, deep link, or config key)"`
+	Status      *string `json:"status,omitempty"       jsonschema:"Initial status (default: later)"`
+	Note        *string `json:"note,omitempty"         jsonschema:"Task note (Markdown)"`
+	Priority    *string `json:"priority,omitempty"     jsonschema:"Priority level"`
+	Estimate    *int    `json:"estimate,omitempty"     jsonschema:"Time estimate in minutes (0-720)"`
+	Motivation  *string `json:"motivation,omitempty"   jsonschema:"Task motivation"`
+	Important   *bool   `json:"important,omitempty"    jsonschema:"Eisenhower matrix: important"`
+	Urgent      *bool   `json:"urgent,omitempty"       jsonschema:"Eisenhower matrix: urgent"`
+	ScheduledOn *string `json:"scheduled_on,omitempty" jsonschema:"Schedule date (strtotime syntax)"`
 
 	// Note-specific fields
-	NotebookID *string `json:"notebook_id,omitempty"`
+	NotebookID *string `json:"notebook_id,omitempty" jsonschema:"Notebook UUID"`
 
 	// Person-specific fields
-	FirstName    *string `json:"first_name,omitempty"`
-	LastName     *string `json:"last_name,omitempty"`
-	Relationship *string `json:"relationship,omitempty"`
+	FirstName    *string `json:"first_name,omitempty"   jsonschema:"First name (required for person)"`
+	LastName     *string `json:"last_name,omitempty"    jsonschema:"Last name"`
+	Relationship *string `json:"relationship,omitempty" jsonschema:"Relationship strength"`
 
 	// Journal-specific fields
-	Date *string `json:"date,omitempty"`
+	Date *string `json:"date,omitempty" jsonschema:"Entry date (strtotime syntax, default: today)"`
 }
 
 // CreateOutput is the output schema for the consolidated create tool.

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

@@ -9,6 +9,7 @@ import (
 
 	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/google/jsonschema-go/jsonschema"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
@@ -16,20 +17,53 @@ import (
 const DeleteToolName = "delete"
 
 // DeleteToolDescription describes the delete tool for LLMs.
-const DeleteToolDescription = `Deletes an entity from Lunatask.
+const DeleteToolDescription = `Permanently delete an entity from Lunatask.
+This action cannot be undone. The entity and its associations are removed.`
 
-Required:
-- entity: Type to delete (task, note, person)
-- id: Entity UUID or lunatask:// deep link
+// DeleteToolAnnotations returns hints about tool behavior.
+func DeleteToolAnnotations() *mcp.ToolAnnotations {
+	return &mcp.ToolAnnotations{
+		DestructiveHint: ptr(true),
+	}
+}
+
+func ptr[T any](v T) *T { return &v }
+
+// toAnyStrings converts a slice of string-based types to []any for JSON schema enums.
+func toAnyStrings[T ~string](slice []T) []any {
+	result := make([]any, len(slice))
+	for i, v := range slice {
+		result[i] = string(v)
+	}
+
+	return result
+}
 
-This action is permanent and cannot be undone.
+// prioritiesToAny converts priorities to their string representations for JSON schema enums.
+func prioritiesToAny(priorities []lunatask.Priority) []any {
+	result := make([]any, len(priorities))
+	for i := range priorities {
+		result[i] = priorities[i].String()
+	}
+
+	return result
+}
+
+// DeleteInputSchema returns a custom schema with enum constraints.
+func DeleteInputSchema() *jsonschema.Schema {
+	schema, _ := jsonschema.For[DeleteInput](nil)
 
-Returns confirmation of deletion with the entity's deep link.`
+	schema.Properties["entity"].Enum = []any{
+		EntityTask, EntityNote, EntityPerson,
+	}
+
+	return schema
+}
 
 // DeleteInput is the input schema for the consolidated delete tool.
 type DeleteInput struct {
-	Entity string `json:"entity" jsonschema:"required"`
-	ID     string `json:"id"     jsonschema:"required"`
+	Entity string `json:"entity" jsonschema:"Entity type to delete"`
+	ID     string `json:"id"     jsonschema:"UUID or lunatask:// deep link"`
 }
 
 // DeleteOutput is the output schema for the consolidated delete tool.

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

@@ -12,6 +12,7 @@ import (
 
 	"git.secluded.site/go-lunatask"
 	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/google/jsonschema-go/jsonschema"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
@@ -19,56 +20,49 @@ import (
 const QueryToolName = "query"
 
 // QueryToolDescription describes the query tool for LLMs.
-const QueryToolDescription = `Queries entities from Lunatask. Fallback for agents without MCP resource support.
+const QueryToolDescription = `Query Lunatask entities. Fallback for agents without MCP resource support.
 
-Required:
-- entity: Type to query (task, note, person, area, goal, notebook, habit)
+Provide id for single entity details, omit for filtered list.
+E2E encryption means names/content unavailable in lists—only metadata.
+Prefer MCP resources (lunatask://tasks/today, lunatask://areas) when available.`
 
-Optional:
-- id: Entity UUID or lunatask:// deep link (if provided, returns single entity details)
-
-When id is omitted, returns a list with optional filters:
-
-**task** filters:
-- area_id: Filter by area UUID
-- status: Filter by status (later, next, started, waiting, completed)
-- include_completed: Include completed tasks (default: false)
-
-**note** filters:
-- notebook_id: Filter by notebook UUID
-- source: Filter by source identifier
-- source_id: Filter by source-specific ID
-
-**person** filters:
-- source: Filter by source identifier
-- source_id: Filter by source-specific ID
+// QueryToolAnnotations returns hints about tool behavior.
+func QueryToolAnnotations() *mcp.ToolAnnotations {
+	return &mcp.ToolAnnotations{
+		ReadOnlyHint: true,
+	}
+}
 
-**goal** filters:
-- area_id: Required - area UUID, deep link, or config key
+// QueryInputSchema returns a custom schema with enum constraints.
+func QueryInputSchema() *jsonschema.Schema {
+	schema, _ := jsonschema.For[QueryInput](nil)
 
-**area, notebook, habit**: No filters (returns all from config)
+	schema.Properties["entity"].Enum = []any{
+		EntityTask, EntityNote, EntityPerson, EntityArea, EntityGoal, EntityNotebook, EntityHabit,
+	}
+	schema.Properties["status"].Enum = toAnyStrings(lunatask.AllTaskStatuses())
 
-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.`
+	return schema
+}
 
 // QueryInput is the input schema for the consolidated query tool.
 type QueryInput struct {
-	Entity string  `json:"entity"       jsonschema:"required"`
-	ID     *string `json:"id,omitempty"`
+	Entity string  `json:"entity"       jsonschema:"Entity type to query"`
+	ID     *string `json:"id,omitempty" jsonschema:"UUID or deep link for single entity lookup"`
 
 	// Task/Goal filters
-	AreaID *string `json:"area_id,omitempty"`
+	AreaID *string `json:"area_id,omitempty" jsonschema:"Filter by area (UUID, deep link, or config key)"`
 
 	// Task filters
-	Status           *string `json:"status,omitempty"`
-	IncludeCompleted *bool   `json:"include_completed,omitempty"`
+	Status           *string `json:"status,omitempty"            jsonschema:"Filter by task status"`
+	IncludeCompleted *bool   `json:"include_completed,omitempty" jsonschema:"Include completed tasks (default: false)"`
 
 	// Note filters
-	NotebookID *string `json:"notebook_id,omitempty"`
+	NotebookID *string `json:"notebook_id,omitempty" jsonschema:"Filter by notebook UUID"`
 
 	// Note/Person filters
-	Source   *string `json:"source,omitempty"`
-	SourceID *string `json:"source_id,omitempty"`
+	Source   *string `json:"source,omitempty"    jsonschema:"Filter by source identifier"`
+	SourceID *string `json:"source_id,omitempty" jsonschema:"Filter by source-specific ID"`
 }
 
 // QueryOutput is the output schema for the consolidated query tool.

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

@@ -11,6 +11,7 @@ import (
 	"git.secluded.site/lune/internal/dateutil"
 	"git.secluded.site/lune/internal/mcp/shared"
 	"git.secluded.site/lune/internal/validate"
+	"github.com/google/jsonschema-go/jsonschema"
 	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
@@ -18,70 +19,61 @@ import (
 const UpdateToolName = "update"
 
 // UpdateToolDescription describes the update tool for LLMs.
-const UpdateToolDescription = `Updates an existing entity in Lunatask.
-
-Required:
-- entity: Type to update (task, note, person)
-- id: Entity UUID or lunatask:// deep link
-
-Entity-specific fields (only provided fields are modified):
-
-**task**:
-- name: New task title
-- area_id: Move to area (UUID, deep link, or config key)
-- goal_id: Move to goal (UUID, 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 or natural language)
-
-**note**:
-- name: New note title
-- notebook_id: Move to notebook (UUID)
-- content: Replace content (Markdown)
-- date: Note date (YYYY-MM-DD or natural language)
-
-**person**:
-- 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)
-
-Returns the updated entity's deep link.`
+const UpdateToolDescription = `Update an existing Lunatask entity.
+Only provided fields are modified—omit fields to leave unchanged.
+Task note/content replaces existing (not appended). Idempotent.`
+
+// UpdateToolAnnotations returns hints about tool behavior.
+func UpdateToolAnnotations() *mcp.ToolAnnotations {
+	return &mcp.ToolAnnotations{
+		IdempotentHint: true,
+	}
+}
+
+// UpdateInputSchema returns a custom schema with enum constraints.
+func UpdateInputSchema() *jsonschema.Schema {
+	schema, _ := jsonschema.For[UpdateInput](nil)
+
+	schema.Properties["entity"].Enum = []any{
+		EntityTask, EntityNote, EntityPerson,
+	}
+	schema.Properties["status"].Enum = toAnyStrings(lunatask.AllTaskStatuses())
+	schema.Properties["priority"].Enum = prioritiesToAny(lunatask.AllPriorities())
+	schema.Properties["motivation"].Enum = toAnyStrings(lunatask.AllMotivations())
+	schema.Properties["relationship"].Enum = toAnyStrings(lunatask.AllRelationshipStrengths())
+
+	return schema
+}
 
 // UpdateInput is the input schema for the consolidated update tool.
 type UpdateInput struct {
-	Entity string `json:"entity" jsonschema:"required"`
-	ID     string `json:"id"     jsonschema:"required"`
+	Entity string `json:"entity" jsonschema:"Entity type to update"`
+	ID     string `json:"id"     jsonschema:"UUID or lunatask:// deep link"`
 
 	// Common fields
-	Name    *string `json:"name,omitempty"`
-	Content *string `json:"content,omitempty"`
+	Name    *string `json:"name,omitempty"    jsonschema:"New title/name"`
+	Content *string `json:"content,omitempty" jsonschema:"New content (Markdown, replaces existing)"`
 
 	// Task-specific fields
-	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"`
+	AreaID      *string `json:"area_id,omitempty"      jsonschema:"Move to area (UUID, deep link, or config key)"`
+	GoalID      *string `json:"goal_id,omitempty"      jsonschema:"Move to goal (requires area_id)"`
+	Status      *string `json:"status,omitempty"       jsonschema:"New task status"`
+	Note        *string `json:"note,omitempty"         jsonschema:"New task note (Markdown, replaces existing)"`
+	Priority    *string `json:"priority,omitempty"     jsonschema:"New priority level"`
+	Estimate    *int    `json:"estimate,omitempty"     jsonschema:"Time estimate in minutes (0-720)"`
+	Motivation  *string `json:"motivation,omitempty"   jsonschema:"Task motivation"`
+	Important   *bool   `json:"important,omitempty"    jsonschema:"Eisenhower matrix: important"`
+	Urgent      *bool   `json:"urgent,omitempty"       jsonschema:"Eisenhower matrix: urgent"`
+	ScheduledOn *string `json:"scheduled_on,omitempty" jsonschema:"Schedule date (strtotime syntax)"`
 
 	// Note-specific fields
-	NotebookID *string `json:"notebook_id,omitempty"`
-	Date       *string `json:"date,omitempty"`
+	NotebookID *string `json:"notebook_id,omitempty" jsonschema:"Move to notebook (UUID)"`
+	Date       *string `json:"date,omitempty"        jsonschema:"Note date (strtotime syntax)"`
 
 	// Person-specific fields
-	FirstName    *string `json:"first_name,omitempty"`
-	LastName     *string `json:"last_name,omitempty"`
-	Relationship *string `json:"relationship,omitempty"`
+	FirstName    *string `json:"first_name,omitempty"   jsonschema:"New first name"`
+	LastName     *string `json:"last_name,omitempty"    jsonschema:"New last name"`
+	Relationship *string `json:"relationship,omitempty" jsonschema:"Relationship strength"`
 }
 
 // UpdateOutput is the output schema for the consolidated update tool.

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

@@ -18,20 +18,22 @@ import (
 const TrackToolName = "track_habit"
 
 // TrackToolDescription describes the track habit tool for LLMs.
-const TrackToolDescription = `Records that a habit was performed on a specific date.
+const TrackToolDescription = `Record that a habit was performed.
 
-Required:
-- habit_id: Habit UUID, deep link, or config key
+Use lunatask://habits resource to discover valid habit IDs and config keys.
+Tracks for today by default. Idempotent—re-tracking the same date has no effect.`
 
-Optional:
-- performed_on: Date performed (YYYY-MM-DD or natural language, default: today)
-
-Use the lunatask://habits resource to discover valid habit IDs.`
+// TrackToolAnnotations returns hints about tool behavior.
+func TrackToolAnnotations() *mcp.ToolAnnotations {
+	return &mcp.ToolAnnotations{
+		IdempotentHint: true,
+	}
+}
 
 // TrackInput is the input schema for tracking a habit.
 type TrackInput struct {
-	HabitID     string  `json:"habit_id"               jsonschema:"required"`
-	PerformedOn *string `json:"performed_on,omitempty"`
+	HabitID     string  `json:"habit_id"               jsonschema:"Habit UUID, lunatask:// deep link, or config key"`
+	PerformedOn *string `json:"performed_on,omitempty" jsonschema:"Date performed (strtotime syntax, default: today)"`
 }
 
 // TrackOutput is the output schema for tracking a habit.

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

@@ -18,23 +18,25 @@ import (
 const ToolName = "add_timeline_note"
 
 // ToolDescription describes the add timeline note tool for LLMs.
-const ToolDescription = `Adds a timeline note to a person's memory timeline in Lunatask.
+const ToolDescription = `Add a note to a person's memory timeline.
 
-Required:
-- person_id: Person UUID or lunatask://person/... deep link
+Append-only—each call creates a new timeline entry. Great for tracking
+interactions, meetings, or memorable moments with someone.`
 
-Optional:
-- content: Markdown content describing the interaction
-- date: Date of interaction (YYYY-MM-DD or natural language, default: today)
+// ToolAnnotations returns hints about tool behavior.
+func ToolAnnotations() *mcp.ToolAnnotations {
+	return &mcp.ToolAnnotations{
+		DestructiveHint: ptr(false),
+	}
+}
 
-This is append-only — adds to the person's memory timeline.
-Great for tracking when you last interacted with someone.`
+func ptr[T any](v T) *T { return &v }
 
 // 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"`
+	PersonID string  `json:"person_id"         jsonschema:"Person UUID or lunatask:// deep link"`
+	Content  *string `json:"content,omitempty" jsonschema:"Markdown content describing the interaction"`
+	Date     *string `json:"date,omitempty"    jsonschema:"Date of interaction (strtotime syntax, default: today)"`
 }
 
 // Output is the output schema for adding a timeline note.

internal/mcp/tools/timestamp/handler.go 🔗

@@ -18,25 +18,22 @@ import (
 const ToolName = "get_timestamp"
 
 // ToolDescription describes the tool for LLMs.
-const ToolDescription = `Parses natural language date/time expressions into RFC3339 timestamps.
-Uses PHP strtotime syntax.
-
-Accepts expressions like:
-- "today", "tomorrow", "yesterday"
-- "next Monday", "last Friday"
-- "next week", "last month", "next year"
-- "+3 days", "-1 week", "3 days" (use +/- prefix, not "ago" or "in")
-- Compound: "next Friday +2 weeks"
-- "March 5", "January 15 2024"
-- "2024-01-15"
-- "" (empty string returns today)
-
-Returns the timestamp in RFC3339 format (e.g., "2024-01-15T00:00:00Z").
-Use this tool to convert human-readable dates before passing them to task/habit tools.`
+const ToolDescription = `Parse natural language dates into RFC3339 timestamps.
+
+Use before passing dates to other tools. Supports PHP strtotime syntax:
+relative (+3 days, next Monday), named (March 5), ISO (2024-01-15).
+Empty input returns today.`
+
+// ToolAnnotations returns hints about tool behavior.
+func ToolAnnotations() *mcp.ToolAnnotations {
+	return &mcp.ToolAnnotations{
+		ReadOnlyHint: true,
+	}
+}
 
 // Input is the input schema for the timestamp tool.
 type Input struct {
-	Date string `json:"date"`
+	Date string `json:"date" jsonschema:"Date/time expression to parse (empty = today)"`
 }
 
 // Output is the output schema for the timestamp tool.