Detailed changes
@@ -63,7 +63,6 @@ linters:
- interfacebloat
- intrange
- iotamixing
- - ireturn
- lll
- loggercheck
- maintidx
@@ -119,6 +118,7 @@ linters:
disable:
- depguard
+ - ireturn
settings:
exhaustruct:
@@ -134,11 +134,11 @@ linters:
- git.secluded.site/go-lunatask.CreateTaskRequest
- git.secluded.site/go-lunatask.UpdateTaskRequest
# Internal output types where some fields are optional
- - git.sr.ht/~amolith/lunatask-mcp-server/tools/tasks.CreateOutput
- - git.sr.ht/~amolith/lunatask-mcp-server/tools/tasks.UpdateOutput
- - git.sr.ht/~amolith/lunatask-mcp-server/tools/tasks.DeleteOutput
- - git.sr.ht/~amolith/lunatask-mcp-server/tools/habits.TrackOutput
- - git.sr.ht/~amolith/lunatask-mcp-server/tools/timestamp.Output
+ - git.secluded.site/lunatask-mcp-server/tools/tasks.CreateOutput
+ - git.secluded.site/lunatask-mcp-server/tools/tasks.UpdateOutput
+ - git.secluded.site/lunatask-mcp-server/tools/tasks.DeleteOutput
+ - git.secluded.site/lunatask-mcp-server/tools/habits.TrackOutput
+ - git.secluded.site/lunatask-mcp-server/tools/timestamp.Output
tagliatelle:
case:
rules:
@@ -118,6 +118,7 @@ func runFreshSetup(cmd *cobra.Command, cfg *config.Config) error {
steps := []wizardStep{
func() wizardNav { return runServerStep(cfg) },
+ func() wizardNav { return runToolsStep(cfg) },
func() wizardNav { return runAreasStep(cfg) },
func() wizardNav { return runHabitsStep(cfg) },
func() wizardNav { return runAccessTokenStep(cmd) },
@@ -171,6 +172,7 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error {
"areas": func() error { return manageAreas(cfg) },
"habits": func() error { return manageHabits(cfg) },
"server": func() error { return configureServer(cfg) },
+ "tools": func() error { return configureTools(cfg) },
"token": func() error { return configureAccessToken(cmd) },
"reset": func() error { return resetConfig(cmd, cfg) },
}
@@ -184,6 +186,7 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error {
huh.NewOption("Manage areas & goals", "areas"),
huh.NewOption("Manage habits", "habits"),
huh.NewOption("Server settings", "server"),
+ huh.NewOption("Enabled tools", "tools"),
huh.NewOption("Access token", "token"),
huh.NewOption("Reset all configuration", "reset"),
huh.NewOption("Done", choiceDone),
@@ -220,11 +223,39 @@ func printConfigSummary(out io.Writer, cfg *config.Config) {
_, _ = fmt.Fprintln(out, ui.Bold.Render("Configuration summary:"))
_, _ = fmt.Fprintf(out, " Areas: %d (%d goals)\n", len(cfg.Areas), goalCount)
_, _ = fmt.Fprintf(out, " Habits: %d\n", len(cfg.Habits))
+ _, _ = fmt.Fprintf(out, " Tools: %s\n", formatToolsSummary(&cfg.Tools))
_, _ = fmt.Fprintf(out, " Server: %s:%d (%s)\n", cfg.Server.Host, cfg.Server.Port, cfg.Server.Transport)
_, _ = fmt.Fprintf(out, " Timezone: %s\n", cfg.Timezone)
_, _ = fmt.Fprintln(out)
}
+func formatToolsSummary(tools *config.ToolsConfig) string {
+ enabled := 0
+ total := 5
+
+ if tools.GetTimestamp {
+ enabled++
+ }
+
+ if tools.CreateTask {
+ enabled++
+ }
+
+ if tools.UpdateTask {
+ enabled++
+ }
+
+ if tools.DeleteTask {
+ enabled++
+ }
+
+ if tools.TrackHabitActivity {
+ enabled++
+ }
+
+ return fmt.Sprintf("%d/%d enabled", enabled, total)
+}
+
func saveWithSummary(cmd *cobra.Command, cfg *config.Config) error {
path, err := config.Path()
if err != nil {
@@ -279,6 +310,14 @@ transport = "stdio"
# Timezone for date parsing (IANA format)
timezone = "UTC"
+# Enable or disable individual MCP tools (all enabled by default)
+[tools]
+get_timestamp = true
+create_task = true
+update_task = true
+delete_task = true
+track_habit_activity = true
+
# Areas of life from Lunatask
# Find IDs in Lunatask: Open area settings → "Copy Area ID" (bottom left)
#
@@ -0,0 +1,109 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package config
+
+import (
+ "errors"
+
+ "github.com/charmbracelet/huh"
+
+ "git.secluded.site/lunatask-mcp-server/internal/config"
+)
+
+// Tool identifiers for the multi-select.
+const (
+ toolGetTimestamp = "get_timestamp"
+ toolCreateTask = "create_task"
+ toolUpdateTask = "update_task"
+ toolDeleteTask = "delete_task"
+ toolTrackHabitActivity = "track_habit_activity"
+)
+
+// runToolsStep runs the tools configuration step.
+func runToolsStep(cfg *config.Config) wizardNav {
+ cfg.Tools.ApplyDefaults()
+
+ selected := toolsToSlice(&cfg.Tools)
+
+ err := huh.NewMultiSelect[string]().
+ Title("Enable MCP tools").
+ Description("Space to toggle, Enter to confirm.").
+ Options(
+ huh.NewOption("get_timestamp - Parse natural language dates", toolGetTimestamp).
+ Selected(cfg.Tools.GetTimestamp),
+ huh.NewOption("create_task - Create new tasks", toolCreateTask).
+ Selected(cfg.Tools.CreateTask),
+ huh.NewOption("update_task - Modify existing tasks", toolUpdateTask).
+ Selected(cfg.Tools.UpdateTask),
+ huh.NewOption("delete_task - Permanently delete tasks", toolDeleteTask).
+ Selected(cfg.Tools.DeleteTask),
+ huh.NewOption("track_habit_activity - Record habit completions", toolTrackHabitActivity).
+ Selected(cfg.Tools.TrackHabitActivity),
+ ).
+ Value(&selected).
+ Run()
+ if err != nil {
+ if errors.Is(err, huh.ErrUserAborted) {
+ return navQuit
+ }
+
+ return navQuit
+ }
+
+ sliceToTools(selected, &cfg.Tools)
+
+ return navNext
+}
+
+// configureTools runs the tools configuration from the main menu.
+func configureTools(cfg *config.Config) error {
+ nav := runToolsStep(cfg)
+ if nav == navQuit {
+ return errQuit
+ }
+
+ return nil
+}
+
+// toolsToSlice converts ToolsConfig bools to a slice of enabled tool names.
+func toolsToSlice(tools *config.ToolsConfig) []string {
+ var enabled []string
+
+ if tools.GetTimestamp {
+ enabled = append(enabled, toolGetTimestamp)
+ }
+
+ if tools.CreateTask {
+ enabled = append(enabled, toolCreateTask)
+ }
+
+ if tools.UpdateTask {
+ enabled = append(enabled, toolUpdateTask)
+ }
+
+ if tools.DeleteTask {
+ enabled = append(enabled, toolDeleteTask)
+ }
+
+ if tools.TrackHabitActivity {
+ enabled = append(enabled, toolTrackHabitActivity)
+ }
+
+ return enabled
+}
+
+// sliceToTools converts a slice of tool names back to ToolsConfig bools.
+func sliceToTools(selected []string, tools *config.ToolsConfig) {
+ enabledSet := make(map[string]bool)
+ for _, s := range selected {
+ enabledSet[s] = true
+ }
+
+ tools.GetTimestamp = enabledSet[toolGetTimestamp]
+ tools.CreateTask = enabledSet[toolCreateTask]
+ tools.UpdateTask = enabledSet[toolUpdateTask]
+ tools.DeleteTask = enabledSet[toolDeleteTask]
+ tools.TrackHabitActivity = enabledSet[toolTrackHabitActivity]
+}
@@ -174,9 +174,9 @@ func newMCPServer(cfg *config.Config, accessToken string) *mcp.Server {
habitProviders := toHabitProviders(cfg.Habits)
registerResources(mcpServer, areaProviders, habitProviders)
- registerTimestampTool(mcpServer, cfg.Timezone)
- registerTaskTools(mcpServer, accessToken, cfg.Timezone, areaProviders)
- registerHabitTool(mcpServer, accessToken, habitProviders)
+ registerTimestampTool(mcpServer, cfg)
+ registerTaskTools(mcpServer, cfg, accessToken, areaProviders)
+ registerHabitTool(mcpServer, cfg, accessToken, habitProviders)
return mcpServer
}
@@ -309,8 +309,12 @@ func registerResources(
}, habitsResourceHandler.HandleRead)
}
-func registerTimestampTool(mcpServer *mcp.Server, timezone string) {
- handler := timestamp.NewHandler(timezone)
+func registerTimestampTool(mcpServer *mcp.Server, cfg *config.Config) {
+ if !cfg.Tools.GetTimestamp {
+ return
+ }
+
+ handler := timestamp.NewHandler(cfg.Timezone)
mcp.AddTool(mcpServer, &mcp.Tool{
Name: "get_timestamp",
@@ -320,33 +324,48 @@ func registerTimestampTool(mcpServer *mcp.Server, timezone string) {
func registerTaskTools(
mcpServer *mcp.Server,
+ cfg *config.Config,
accessToken string,
- timezone string,
areaProviders []shared.AreaProvider,
) {
- handler := tasks.NewHandler(accessToken, timezone, areaProviders)
+ if !cfg.Tools.CreateTask && !cfg.Tools.UpdateTask && !cfg.Tools.DeleteTask {
+ return
+ }
- mcp.AddTool(mcpServer, &mcp.Tool{
- Name: "create_task",
- Description: tasks.CreateToolDescription,
- }, handler.HandleCreate)
+ handler := tasks.NewHandler(accessToken, cfg.Timezone, areaProviders)
- mcp.AddTool(mcpServer, &mcp.Tool{
- Name: "update_task",
- Description: tasks.UpdateToolDescription,
- }, handler.HandleUpdate)
+ if cfg.Tools.CreateTask {
+ mcp.AddTool(mcpServer, &mcp.Tool{
+ Name: "create_task",
+ Description: tasks.CreateToolDescription,
+ }, handler.HandleCreate)
+ }
- mcp.AddTool(mcpServer, &mcp.Tool{
- Name: "delete_task",
- Description: tasks.DeleteToolDescription,
- }, handler.HandleDelete)
+ if cfg.Tools.UpdateTask {
+ mcp.AddTool(mcpServer, &mcp.Tool{
+ Name: "update_task",
+ Description: tasks.UpdateToolDescription,
+ }, handler.HandleUpdate)
+ }
+
+ if cfg.Tools.DeleteTask {
+ mcp.AddTool(mcpServer, &mcp.Tool{
+ Name: "delete_task",
+ Description: tasks.DeleteToolDescription,
+ }, handler.HandleDelete)
+ }
}
func registerHabitTool(
mcpServer *mcp.Server,
+ cfg *config.Config,
accessToken string,
habitProviders []shared.HabitProvider,
) {
+ if !cfg.Tools.TrackHabitActivity {
+ return
+ }
+
handler := habits.NewHandler(accessToken, habitProviders)
mcp.AddTool(mcpServer, &mcp.Tool{
@@ -20,6 +20,7 @@ var ErrNotFound = errors.New("config file not found")
// Config represents the lunatask-mcp-server configuration file structure.
type Config struct {
Server ServerConfig `toml:"server"`
+ Tools ToolsConfig `toml:"tools"`
Timezone string `toml:"timezone"`
Areas []Area `toml:"areas"`
Habits []Habit `toml:"habits"`
@@ -32,6 +33,15 @@ type ServerConfig struct {
Transport string `toml:"transport"`
}
+// ToolsConfig controls which MCP tools are enabled.
+type ToolsConfig struct {
+ GetTimestamp bool `toml:"get_timestamp"`
+ CreateTask bool `toml:"create_task"`
+ UpdateTask bool `toml:"update_task"`
+ DeleteTask bool `toml:"delete_task"`
+ TrackHabitActivity bool `toml:"track_habit_activity"`
+}
+
// Area represents a Lunatask area of life with its goals.
type Area struct {
ID string `json:"id" toml:"id"`
@@ -257,4 +267,19 @@ func (c *Config) applyDefaults() {
if c.Timezone == "" {
c.Timezone = "UTC"
}
+
+ c.Tools.ApplyDefaults()
+}
+
+// ApplyDefaults enables all tools by default.
+func (t *ToolsConfig) ApplyDefaults() {
+ // Use a marker to detect if tools section was present in config.
+ // If all are false, apply defaults (all true).
+ if !t.GetTimestamp && !t.CreateTask && !t.UpdateTask && !t.DeleteTask && !t.TrackHabitActivity {
+ t.GetTimestamp = true
+ t.CreateTask = true
+ t.UpdateTask = true
+ t.DeleteTask = true
+ t.TrackHabitActivity = true
+ }
}
@@ -59,6 +59,7 @@ func (h *Handler) HandleCreate(
// Resolve goal key to ID if provided
var goalID string
+
if input.GoalID != nil && *input.GoalID != "" {
goal := shared.GetGoalInArea(area, *input.GoalID)
if goal == nil {
@@ -68,6 +69,7 @@ func (h *Handler) HandleCreate(
area.GetName(),
)
}
+
goalID = goal.GetID()
}
@@ -224,8 +226,10 @@ func (h *Handler) applyCreateOptions(builder *lunatask.TaskBuilder, input Create
//
//nolint:funlen,gocognit // each field handling is straightforward
func (h *Handler) applyUpdateOptions(builder *lunatask.TaskUpdateBuilder, input UpdateInput) error {
- var resolvedAreaID string
- var resolvedGoalID string
+ var (
+ resolvedAreaID string
+ resolvedGoalID string
+ )
if input.AreaID != nil && *input.AreaID != "" {
area := shared.FindArea(h.areas, *input.AreaID)
@@ -242,6 +246,7 @@ func (h *Handler) applyUpdateOptions(builder *lunatask.TaskUpdateBuilder, input
if goal == nil {
return fmt.Errorf("goal %s not found in area %s", *input.GoalID, area.GetName())
}
+
resolvedGoalID = goal.GetID()
}
}
@@ -255,6 +260,7 @@ func (h *Handler) applyUpdateOptions(builder *lunatask.TaskUpdateBuilder, input
for _, area := range h.areas {
if goal := shared.GetGoalInArea(area, *input.GoalID); goal != nil {
builder.InGoal(goal.GetID())
+
break
}
}