diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index 64bc764482ae078da389338987dfe5728f89570c..1d208cd7456c90098baf42888fc9d5db11458637 100644 --- a/cmd/mcp/mcp.go +++ b/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. diff --git a/cmd/mcp/server.go b/cmd/mcp/server.go index 2f2232ce8a7bde7eb2f0f3241f2d229807d88469..fafcea206c72d7a7cc5c83e1de9193503e5fba4b 100644 --- a/cmd/mcp/server.go +++ b/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, diff --git a/internal/config/config.go b/internal/config/config.go index 049ce150979b26c7a37f47c39d4470254d06cdfb..ad1a0d1449d1f4b78b6b71537ae32e6f5f7faa57 100644 --- a/internal/config/config.go +++ b/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 } } diff --git a/internal/mcp/tools/crud/create.go b/internal/mcp/tools/crud/create.go new file mode 100644 index 0000000000000000000000000000000000000000..97f60991f4fa2caaf7799aa88ec3448ed05696cf --- /dev/null +++ b/internal/mcp/tools/crud/create.go @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/internal/mcp/tools/crud/delete.go b/internal/mcp/tools/crud/delete.go new file mode 100644 index 0000000000000000000000000000000000000000..2837c10240ef3cf91f28273b86d05bd053110bdf --- /dev/null +++ b/internal/mcp/tools/crud/delete.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/internal/mcp/tools/crud/query.go b/internal/mcp/tools/crud/query.go new file mode 100644 index 0000000000000000000000000000000000000000..c8ad2837b69d3509ca4e0d0b9cacbd1bec0bc0c0 --- /dev/null +++ b/internal/mcp/tools/crud/query.go @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +} diff --git a/internal/mcp/tools/crud/update.go b/internal/mcp/tools/crud/update.go new file mode 100644 index 0000000000000000000000000000000000000000..df9f4e57eb2504fd932e72a2d82d52914fe8a7f0 --- /dev/null +++ b/internal/mcp/tools/crud/update.go @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: Amolith +// +// 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 +}