From 3959af16ff41a8b1b755cea7f44149e95eca9bca Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 24 Dec 2025 11:00:25 -0700 Subject: [PATCH] feat(mcp): add tool enable/disable CLI flags Mutually exclusive CLI flags for runtime control over MCP tool exposure: - --enabled-tools a,b,c: ignore config, enable only listed tools - --disabled-tools x,y,z: ignore config, enable everything except listed Tool names match the MCP tool names exactly (what the model sees). Validates tool names against known set before starting server. Assisted-by: Claude Sonnet 4 via Crush --- cmd/mcp/mcp.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 7 deletions(-) diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index e62f75f3445e627959106f29b18cef6734b77409..c4b95004a3979a1ae06cacbd2d5d599beab5b063 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -11,6 +11,13 @@ import ( "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/mcp/tools/habit" + "git.secluded.site/lune/internal/mcp/tools/journal" + "git.secluded.site/lune/internal/mcp/tools/note" + "git.secluded.site/lune/internal/mcp/tools/person" + "git.secluded.site/lune/internal/mcp/tools/task" + "git.secluded.site/lune/internal/mcp/tools/timestamp" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -22,14 +29,18 @@ const ( ) var ( - errUnknownTransport = errors.New("unknown transport; use stdio, sse, or http") - errNoToken = errors.New("no access token; run 'lune init' first") + errUnknownTransport = errors.New("unknown transport; use stdio, sse, or http") + errNoToken = errors.New("no access token; run 'lune init' first") + errMutuallyExclusive = errors.New("--enabled-tools and --disabled-tools are mutually exclusive") + errUnknownTool = errors.New("unknown tool name") ) var ( - transport string - host string - port int + transport string + host string + port int + enabledTools []string + disabledTools []string ) // Cmd is the mcp command for starting the MCP server. @@ -58,14 +69,26 @@ func init() { "Transport type: stdio, sse, http (default: stdio or config)") Cmd.Flags().StringVar(&host, "host", "", "Server host (for sse/http)") Cmd.Flags().IntVar(&port, "port", 0, "Server port (for sse/http)") + Cmd.Flags().StringSliceVar(&enabledTools, "enabled-tools", nil, + "Enable only these tools (comma-separated); overrides config") + Cmd.Flags().StringSliceVar(&disabledTools, "disabled-tools", nil, + "Disable these tools (comma-separated); overrides config") } func runMCP(cmd *cobra.Command, _ []string) error { + if len(enabledTools) > 0 && len(disabledTools) > 0 { + return errMutuallyExclusive + } + cfg, err := loadConfig() if err != nil { return err } + if err := resolveTools(cfg); err != nil { + return err + } + token, err := client.GetToken() if err != nil { return fmt.Errorf("getting access token: %w", err) @@ -77,9 +100,11 @@ func runMCP(cmd *cobra.Command, _ []string) error { mcpServer := newMCPServer(cfg, token) - effectiveTransport := resolveTransport(cfg) + return runTransport(cmd, mcpServer, cfg) +} - switch effectiveTransport { +func runTransport(cmd *cobra.Command, mcpServer *mcpsdk.Server, cfg *config.Config) error { + switch resolveTransport(cfg) { case TransportStdio: return runStdio(mcpServer) case TransportSSE: @@ -129,3 +154,78 @@ func resolvePort(cfg *config.Config) int { return cfg.MCP.Port } + +// validToolNames maps MCP tool names to their ToolsConfig field setters. +var validToolNames = map[string]func(*config.ToolsConfig, bool){ + timestamp.ToolName: func(t *config.ToolsConfig, v bool) { t.GetTimestamp = v }, + task.CreateToolName: func(t *config.ToolsConfig, v bool) { t.CreateTask = v }, + task.UpdateToolName: func(t *config.ToolsConfig, v bool) { t.UpdateTask = v }, + task.DeleteToolName: func(t *config.ToolsConfig, v bool) { t.DeleteTask = v }, + task.ListToolName: func(t *config.ToolsConfig, v bool) { t.ListTasks = v }, + task.ShowToolName: func(t *config.ToolsConfig, v bool) { t.ShowTask = v }, + note.CreateToolName: func(t *config.ToolsConfig, v bool) { t.CreateNote = v }, + note.UpdateToolName: func(t *config.ToolsConfig, v bool) { t.UpdateNote = v }, + note.DeleteToolName: func(t *config.ToolsConfig, v bool) { t.DeleteNote = v }, + note.ListToolName: func(t *config.ToolsConfig, v bool) { t.ListNotes = v }, + person.CreateToolName: func(t *config.ToolsConfig, v bool) { t.CreatePerson = v }, + person.UpdateToolName: func(t *config.ToolsConfig, v bool) { t.UpdatePerson = v }, + person.DeleteToolName: func(t *config.ToolsConfig, v bool) { t.DeletePerson = v }, + person.ListToolName: func(t *config.ToolsConfig, v bool) { t.ListPeople = v }, + person.TimelineToolName: func(t *config.ToolsConfig, v bool) { t.PersonTimeline = v }, + habit.TrackToolName: func(t *config.ToolsConfig, v bool) { t.TrackHabit = v }, + journal.CreateToolName: func(t *config.ToolsConfig, v bool) { t.CreateJournal = v }, + // TODO: Add these once implemented: + // - show_note (ShowNote) + // - show_person (ShowPerson) + // - list_habits (ListHabits) + // - list_areas (ListAreas) - needs config field + // - list_goals (ListGoals) - needs config field +} + +// resolveTools modifies cfg.MCP.Tools based on CLI flags. +// If --enabled-tools is set, only those tools are enabled. +// If --disabled-tools is set, all tools except those are enabled. +// If neither is set, config values are used unchanged. +func resolveTools(cfg *config.Config) error { + if len(enabledTools) > 0 { + // Validate all tool names first + for _, name := range enabledTools { + if _, ok := validToolNames[name]; !ok { + return fmt.Errorf("%w: %s", errUnknownTool, name) + } + } + + // Start with everything disabled + cfg.MCP.Tools = config.ToolsConfig{} + + // Enable only specified tools + for _, name := range enabledTools { + validToolNames[name](&cfg.MCP.Tools, true) + } + + return nil + } + + if len(disabledTools) > 0 { + // Validate all tool names first + for _, name := range disabledTools { + if _, ok := validToolNames[name]; !ok { + return fmt.Errorf("%w: %s", errUnknownTool, name) + } + } + + // Start with everything enabled + cfg.MCP.Tools = config.ToolsConfig{} + cfg.MCP.Tools.ApplyDefaults() + + // Disable specified tools + for _, name := range disabledTools { + validToolNames[name](&cfg.MCP.Tools, false) + } + + return nil + } + + // Neither flag set, use config as-is + return nil +}