diff --git a/cmd/mcp/server.go b/cmd/mcp/server.go index e2e7383ed01943cffc2c8d2b7c093afbb42e290d..b94c9136ac7fac0219fdb2eaad06d4635135278e 100644 --- a/cmd/mcp/server.go +++ b/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) } } diff --git a/go.mod b/go.mod index 668b2cf1f0f3cf2bb75f8fb04e13ea2578b1b3c2..e25a4ef078c0cb86b1dba90756358de382546321 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index aea474a4808dce67314d9464c00dca8e338830bd..d67c0e69bd3003307ed4caef0f40ee4f6672e12b 100644 --- a/go.sum +++ b/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= diff --git a/internal/mcp/tools/crud/create.go b/internal/mcp/tools/crud/create.go index 88116b7f899e4eaee24c2a547716d836a13d4c82..b3540e723b64ec2d21333bb40fa00133b9fc9368 100644 --- a/internal/mcp/tools/crud/create.go +++ b/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. diff --git a/internal/mcp/tools/crud/delete.go b/internal/mcp/tools/crud/delete.go index a75610d3a52d215395cddc5b78df9dff4c79d4eb..fa474875c69a27f64de3d04dabdc6a3fa4c8ed77 100644 --- a/internal/mcp/tools/crud/delete.go +++ b/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. diff --git a/internal/mcp/tools/crud/query.go b/internal/mcp/tools/crud/query.go index ab6597e85219ff992940dea14cb7e3a81e696264..e2c511b08e760f2934e2c596b3e1a86260370aa6 100644 --- a/internal/mcp/tools/crud/query.go +++ b/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. diff --git a/internal/mcp/tools/crud/update.go b/internal/mcp/tools/crud/update.go index 32efc1d6949604eb07cba53f1b15c392495e33e4..c54f439c85cb22018915a17d4dd0a6e1a996ec10 100644 --- a/internal/mcp/tools/crud/update.go +++ b/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. diff --git a/internal/mcp/tools/habit/track.go b/internal/mcp/tools/habit/track.go index d1d64632970ba171879de0d618107e7e7a0ea493..2a11c2a9d04a140ecd02b5fce5dcf254d105f64f 100644 --- a/internal/mcp/tools/habit/track.go +++ b/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. diff --git a/internal/mcp/tools/timeline/handler.go b/internal/mcp/tools/timeline/handler.go index 79d25e7b69c21fde58e61d5894684bfa8c4c6a4e..6e93ae6c8c88bb60aeb1979299a4b870a2780569 100644 --- a/internal/mcp/tools/timeline/handler.go +++ b/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. diff --git a/internal/mcp/tools/timestamp/handler.go b/internal/mcp/tools/timestamp/handler.go index 600787ce5facc37719071b9690074075750e2994..43c83fa95e652f200d696084768cc9edd6a38c51 100644 --- a/internal/mcp/tools/timestamp/handler.go +++ b/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.