feat(mcp): scaffold consolidated CRUD tools

Amolith created

Add stub handlers for create, update, delete, and query tools that will
replace the 23 per-entity tools with 7 consolidated tools:

- create: task, note, person, journal
- update: task, note, person
- delete: task, note, person
- query: all entities (default-disabled fallback for resources)

Includes Entity* constants, ToolsConfig flags, and wiring in server.go.
All handlers return "not yet implemented" pending migration.

Assisted-by: Claude Sonnet 4 via Crush

Change summary

cmd/mcp/mcp.go                    |   5 
cmd/mcp/server.go                 |  47 ++++++++
internal/config/config.go         |  11 +
internal/mcp/tools/crud/create.go | 193 +++++++++++++++++++++++++++++++++
internal/mcp/tools/crud/delete.go |  82 ++++++++++++++
internal/mcp/tools/crud/query.go  | 181 ++++++++++++++++++++++++++++++
internal/mcp/tools/crud/update.go | 131 ++++++++++++++++++++++
7 files changed, 649 insertions(+), 1 deletion(-)

Detailed changes

cmd/mcp/mcp.go 🔗

@@ -12,6 +12,7 @@ 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"
@@ -183,6 +184,10 @@ var validToolNames = map[string]func(*config.ToolsConfig, bool){
 	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 },
 }
 
 // resolveTools modifies cfg.MCP.Tools based on CLI flags.

cmd/mcp/server.go 🔗

@@ -23,6 +23,7 @@ import (
 	"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"
@@ -367,6 +368,8 @@ func registerTools(
 			Description: journal.CreateToolDescription,
 		}, journalHandler.HandleCreate)
 	}
+
+	registerCRUDTools(mcpServer, cfg, tools, accessToken, areaProviders, habitProviders, notebookProviders)
 }
 
 func registerHabitTools(
@@ -427,6 +430,50 @@ func registerConfigListTools(
 	}
 }
 
+func registerCRUDTools(
+	mcpServer *mcp.Server,
+	cfg *config.Config,
+	tools *config.ToolsConfig,
+	accessToken string,
+	areaProviders []shared.AreaProvider,
+	habitProviders []shared.HabitProvider,
+	notebookProviders []shared.NotebookProvider,
+) {
+	if !tools.Create && !tools.Update && !tools.Delete && !tools.Query {
+		return
+	}
+
+	crudHandler := crud.NewHandler(accessToken, cfg, areaProviders, habitProviders, notebookProviders)
+
+	if tools.Create {
+		mcp.AddTool(mcpServer, &mcp.Tool{
+			Name:        crud.CreateToolName,
+			Description: crud.CreateToolDescription,
+		}, crudHandler.HandleCreate)
+	}
+
+	if tools.Update {
+		mcp.AddTool(mcpServer, &mcp.Tool{
+			Name:        crud.UpdateToolName,
+			Description: crud.UpdateToolDescription,
+		}, crudHandler.HandleUpdate)
+	}
+
+	if tools.Delete {
+		mcp.AddTool(mcpServer, &mcp.Tool{
+			Name:        crud.DeleteToolName,
+			Description: crud.DeleteToolDescription,
+		}, crudHandler.HandleDelete)
+	}
+
+	if tools.Query {
+		mcp.AddTool(mcpServer, &mcp.Tool{
+			Name:        crud.QueryToolName,
+			Description: crud.QueryToolDescription,
+		}, crudHandler.HandleQuery)
+	}
+}
+
 func registerTaskTools(
 	mcpServer *mcp.Server,
 	cfg *config.Config,

internal/config/config.go 🔗

@@ -67,6 +67,11 @@ type ToolsConfig struct {
 	ListGoals     bool `toml:"list_goals"`
 
 	CreateJournal bool `toml:"create_journal"`
+
+	Create bool `toml:"create"` // task, note, person, journal
+	Update bool `toml:"update"` // task, note, person
+	Delete bool `toml:"delete"` // task, note, person
+	Query  bool `toml:"query"`  // all entities; default-disabled fallback for resources
 }
 
 // MCPDefaults applies default values to MCP config.
@@ -98,7 +103,8 @@ func (t *ToolsConfig) ApplyDefaults() {
 		!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 {
+		!t.ListAreas && !t.ListGoals && !t.CreateJournal &&
+		!t.Create && !t.Update && !t.Delete && !t.Query {
 		t.GetTimestamp = true
 		t.CreateTask = true
 		t.UpdateTask = true
@@ -119,6 +125,9 @@ func (t *ToolsConfig) ApplyDefaults() {
 		t.TrackHabit = true
 		// ListHabits: default-disabled (fallback for lunatask://habits resource)
 		t.CreateJournal = true
+		t.Create = true
+		t.Update = true
+		t.Delete = true
 	}
 }
 

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

@@ -0,0 +1,193 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package crud provides consolidated MCP tools for Lunatask CRUD operations.
+package crud
+
+import (
+	"context"
+
+	"git.secluded.site/go-lunatask"
+	"git.secluded.site/lune/internal/config"
+	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// Entity type constants.
+const (
+	EntityTask     = "task"
+	EntityNote     = "note"
+	EntityPerson   = "person"
+	EntityJournal  = "journal"
+	EntityArea     = "area"
+	EntityGoal     = "goal"
+	EntityNotebook = "notebook"
+	EntityHabit    = "habit"
+)
+
+// CreateToolName is the name of the consolidated create tool.
+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.`
+
+// 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"`
+
+	// Common fields
+	Name     *string `json:"name,omitempty"`
+	Content  *string `json:"content,omitempty"`
+	Source   *string `json:"source,omitempty"`
+	SourceID *string `json:"source_id,omitempty"`
+
+	// 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"`
+
+	// Note-specific fields
+	NotebookID *string `json:"notebook_id,omitempty"`
+
+	// Person-specific fields
+	FirstName    *string `json:"first_name,omitempty"`
+	LastName     *string `json:"last_name,omitempty"`
+	Relationship *string `json:"relationship,omitempty"`
+
+	// Journal-specific fields
+	Date *string `json:"date,omitempty"`
+}
+
+// CreateOutput is the output schema for the consolidated create tool.
+type CreateOutput struct {
+	Entity   string `json:"entity"`
+	DeepLink string `json:"deep_link,omitempty"`
+	ID       string `json:"id,omitempty"`
+}
+
+// Handler handles consolidated CRUD tool requests.
+type Handler struct {
+	client    *lunatask.Client
+	cfg       *config.Config
+	areas     []shared.AreaProvider
+	habits    []shared.HabitProvider
+	notebooks []shared.NotebookProvider
+}
+
+// NewHandler creates a new consolidated CRUD handler.
+func NewHandler(
+	accessToken string,
+	cfg *config.Config,
+	areas []shared.AreaProvider,
+	habits []shared.HabitProvider,
+	notebooks []shared.NotebookProvider,
+) *Handler {
+	return &Handler{
+		client:    lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
+		cfg:       cfg,
+		areas:     areas,
+		habits:    habits,
+		notebooks: notebooks,
+	}
+}
+
+// HandleCreate creates a new entity based on the entity type.
+func (h *Handler) HandleCreate(
+	ctx context.Context,
+	_ *mcp.CallToolRequest,
+	input CreateInput,
+) (*mcp.CallToolResult, CreateOutput, error) {
+	switch input.Entity {
+	case EntityTask:
+		return h.createTask(ctx, input)
+	case EntityNote:
+		return h.createNote(ctx, input)
+	case EntityPerson:
+		return h.createPerson(ctx, input)
+	case EntityJournal:
+		return h.createJournal(ctx, input)
+	default:
+		return shared.ErrorResult("invalid entity: must be task, note, person, or journal"),
+			CreateOutput{Entity: input.Entity}, nil
+	}
+}
+
+func (h *Handler) createTask(
+	_ context.Context,
+	input CreateInput,
+) (*mcp.CallToolResult, CreateOutput, error) {
+	return shared.ErrorResult("task creation not yet implemented"),
+		CreateOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) createNote(
+	_ context.Context,
+	input CreateInput,
+) (*mcp.CallToolResult, CreateOutput, error) {
+	return shared.ErrorResult("note creation not yet implemented"),
+		CreateOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) createPerson(
+	_ context.Context,
+	input CreateInput,
+) (*mcp.CallToolResult, CreateOutput, error) {
+	return shared.ErrorResult("person creation not yet implemented"),
+		CreateOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) createJournal(
+	_ context.Context,
+	input CreateInput,
+) (*mcp.CallToolResult, CreateOutput, error) {
+	return shared.ErrorResult("journal creation not yet implemented"),
+		CreateOutput{Entity: input.Entity}, nil
+}

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

@@ -0,0 +1,82 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package crud
+
+import (
+	"context"
+
+	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// DeleteToolName is the name of the consolidated delete tool.
+const DeleteToolName = "delete"
+
+// DeleteToolDescription describes the delete tool for LLMs.
+const DeleteToolDescription = `Deletes an entity from Lunatask.
+
+Required:
+- entity: Type to delete (task, note, person)
+- id: Entity UUID or lunatask:// deep link
+
+This action is permanent and cannot be undone.
+
+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"`
+	ID     string `json:"id"     jsonschema:"required"`
+}
+
+// DeleteOutput is the output schema for the consolidated delete tool.
+type DeleteOutput struct {
+	Entity   string `json:"entity"`
+	DeepLink string `json:"deep_link"`
+	Success  bool   `json:"success"`
+}
+
+// HandleDelete deletes an entity based on the entity type.
+func (h *Handler) HandleDelete(
+	ctx context.Context,
+	_ *mcp.CallToolRequest,
+	input DeleteInput,
+) (*mcp.CallToolResult, DeleteOutput, error) {
+	switch input.Entity {
+	case EntityTask:
+		return h.deleteTask(ctx, input)
+	case EntityNote:
+		return h.deleteNote(ctx, input)
+	case EntityPerson:
+		return h.deletePerson(ctx, input)
+	default:
+		return shared.ErrorResult("invalid entity: must be task, note, or person"),
+			DeleteOutput{Entity: input.Entity}, nil
+	}
+}
+
+func (h *Handler) deleteTask(
+	_ context.Context,
+	input DeleteInput,
+) (*mcp.CallToolResult, DeleteOutput, error) {
+	return shared.ErrorResult("task deletion not yet implemented"),
+		DeleteOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) deleteNote(
+	_ context.Context,
+	input DeleteInput,
+) (*mcp.CallToolResult, DeleteOutput, error) {
+	return shared.ErrorResult("note deletion not yet implemented"),
+		DeleteOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) deletePerson(
+	_ context.Context,
+	input DeleteInput,
+) (*mcp.CallToolResult, DeleteOutput, error) {
+	return shared.ErrorResult("person deletion not yet implemented"),
+		DeleteOutput{Entity: input.Entity}, nil
+}

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

@@ -0,0 +1,181 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package crud
+
+import (
+	"context"
+
+	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// QueryToolName is the name of the consolidated query tool.
+const QueryToolName = "query"
+
+// QueryToolDescription describes the query tool for LLMs.
+const QueryToolDescription = `Queries entities from Lunatask. Fallback for agents without MCP resource support.
+
+Required:
+- entity: Type to query (task, note, person, area, goal, notebook, habit)
+
+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
+
+**goal** filters:
+- area_id: Required - area UUID, deep link, or config key
+
+**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.`
+
+// 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
+	ID     *string `json:"id,omitempty"`
+
+	// Task/Goal filters
+	AreaID *string `json:"area_id,omitempty"`
+
+	// Task filters
+	Status           *string `json:"status,omitempty"`
+	IncludeCompleted *bool   `json:"include_completed,omitempty"`
+
+	// Note filters
+	NotebookID *string `json:"notebook_id,omitempty"`
+
+	// Note/Person filters
+	Source   *string `json:"source,omitempty"`
+	SourceID *string `json:"source_id,omitempty"`
+}
+
+// QueryOutput is the output schema for the consolidated query tool.
+type QueryOutput struct {
+	Entity string `json:"entity"`
+	Count  int    `json:"count,omitempty"`
+	// Results will be entity-specific; kept as any for flexibility
+	Items any `json:"items,omitempty"`
+	// Single item fields (when ID is provided)
+	DeepLink string `json:"deep_link,omitempty"`
+}
+
+// HandleQuery queries entities based on the entity type.
+func (h *Handler) HandleQuery(
+	ctx context.Context,
+	_ *mcp.CallToolRequest,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	switch input.Entity {
+	case EntityTask:
+		return h.queryTask(ctx, input)
+	case EntityNote:
+		return h.queryNote(ctx, input)
+	case EntityPerson:
+		return h.queryPerson(ctx, input)
+	case EntityArea:
+		return h.queryArea(ctx, input)
+	case EntityGoal:
+		return h.queryGoal(ctx, input)
+	case EntityNotebook:
+		return h.queryNotebook(ctx, input)
+	case EntityHabit:
+		return h.queryHabit(ctx, input)
+	default:
+		return shared.ErrorResult("invalid entity: must be task, note, person, area, goal, notebook, or habit"),
+			QueryOutput{Entity: input.Entity}, nil
+	}
+}
+
+func (h *Handler) queryTask(
+	_ context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	return shared.ErrorResult("task query not yet implemented"),
+		QueryOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) queryNote(
+	_ context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	return shared.ErrorResult("note query not yet implemented"),
+		QueryOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) queryPerson(
+	_ context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	return shared.ErrorResult("person query not yet implemented"),
+		QueryOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) queryArea(
+	_ context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	if input.ID != nil {
+		return shared.ErrorResult("area entities are config-based and do not support ID lookup"),
+			QueryOutput{Entity: input.Entity}, nil
+	}
+
+	return shared.ErrorResult("area query not yet implemented"),
+		QueryOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) queryGoal(
+	_ context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	if input.ID != nil {
+		return shared.ErrorResult("goal entities are config-based and do not support ID lookup"),
+			QueryOutput{Entity: input.Entity}, nil
+	}
+
+	return shared.ErrorResult("goal query not yet implemented"),
+		QueryOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) queryNotebook(
+	_ context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	if input.ID != nil {
+		return shared.ErrorResult("notebook entities are config-based and do not support ID lookup"),
+			QueryOutput{Entity: input.Entity}, nil
+	}
+
+	return shared.ErrorResult("notebook query not yet implemented"),
+		QueryOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) queryHabit(
+	_ context.Context,
+	input QueryInput,
+) (*mcp.CallToolResult, QueryOutput, error) {
+	if input.ID != nil {
+		return shared.ErrorResult("habit entities are config-based and do not support ID lookup"),
+			QueryOutput{Entity: input.Entity}, nil
+	}
+
+	return shared.ErrorResult("habit query not yet implemented"),
+		QueryOutput{Entity: input.Entity}, nil
+}

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

@@ -0,0 +1,131 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package crud
+
+import (
+	"context"
+
+	"git.secluded.site/lune/internal/mcp/shared"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// UpdateToolName is the name of the consolidated update tool.
+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.`
+
+// 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"`
+	ID     string `json:"id"     jsonschema:"required"`
+
+	// Common fields
+	Name    *string `json:"name,omitempty"`
+	Content *string `json:"content,omitempty"`
+
+	// 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"`
+
+	// Note-specific fields
+	NotebookID *string `json:"notebook_id,omitempty"`
+	Date       *string `json:"date,omitempty"`
+
+	// Person-specific fields
+	FirstName    *string `json:"first_name,omitempty"`
+	LastName     *string `json:"last_name,omitempty"`
+	Relationship *string `json:"relationship,omitempty"`
+}
+
+// UpdateOutput is the output schema for the consolidated update tool.
+type UpdateOutput struct {
+	Entity   string `json:"entity"`
+	DeepLink string `json:"deep_link"`
+}
+
+// HandleUpdate updates an existing entity based on the entity type.
+func (h *Handler) HandleUpdate(
+	ctx context.Context,
+	_ *mcp.CallToolRequest,
+	input UpdateInput,
+) (*mcp.CallToolResult, UpdateOutput, error) {
+	switch input.Entity {
+	case EntityTask:
+		return h.updateTask(ctx, input)
+	case EntityNote:
+		return h.updateNote(ctx, input)
+	case EntityPerson:
+		return h.updatePerson(ctx, input)
+	default:
+		return shared.ErrorResult("invalid entity: must be task, note, or person"),
+			UpdateOutput{Entity: input.Entity}, nil
+	}
+}
+
+func (h *Handler) updateTask(
+	_ context.Context,
+	input UpdateInput,
+) (*mcp.CallToolResult, UpdateOutput, error) {
+	return shared.ErrorResult("task update not yet implemented"),
+		UpdateOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) updateNote(
+	_ context.Context,
+	input UpdateInput,
+) (*mcp.CallToolResult, UpdateOutput, error) {
+	return shared.ErrorResult("note update not yet implemented"),
+		UpdateOutput{Entity: input.Entity}, nil
+}
+
+func (h *Handler) updatePerson(
+	_ context.Context,
+	input UpdateInput,
+) (*mcp.CallToolResult, UpdateOutput, error) {
+	return shared.ErrorResult("person update not yet implemented"),
+		UpdateOutput{Entity: input.Entity}, nil
+}