server.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package mcp
  6
  7import (
  8	"context"
  9	"fmt"
 10	"net"
 11	"net/http"
 12	"strconv"
 13
 14	"git.secluded.site/lune/internal/config"
 15	"git.secluded.site/lune/internal/mcp/resources/areas"
 16	"git.secluded.site/lune/internal/mcp/resources/habits"
 17	"git.secluded.site/lune/internal/mcp/resources/notebooks"
 18	"git.secluded.site/lune/internal/mcp/shared"
 19	"git.secluded.site/lune/internal/mcp/tools/habit"
 20	"git.secluded.site/lune/internal/mcp/tools/task"
 21	"git.secluded.site/lune/internal/mcp/tools/timestamp"
 22	"github.com/modelcontextprotocol/go-sdk/mcp"
 23	"github.com/spf13/cobra"
 24)
 25
 26var version = "dev"
 27
 28func newMCPServer(cfg *config.Config, accessToken string) *mcp.Server {
 29	mcpServer := mcp.NewServer(
 30		&mcp.Implementation{
 31			Name:    "lune",
 32			Version: version,
 33		},
 34		nil,
 35	)
 36
 37	areaProviders := toAreaProviders(cfg.Areas)
 38	habitProviders := shared.ToHabitProviders(cfg.Habits)
 39	notebookProviders := shared.ToNotebookProviders(cfg.Notebooks)
 40
 41	registerResources(mcpServer, areaProviders, habitProviders, notebookProviders)
 42	registerTools(mcpServer, cfg, accessToken, areaProviders, habitProviders)
 43
 44	return mcpServer
 45}
 46
 47func toAreaProviders(cfgAreas []config.Area) []shared.AreaProvider {
 48	providers := make([]shared.AreaProvider, 0, len(cfgAreas))
 49
 50	for _, area := range cfgAreas {
 51		providers = append(providers, shared.AreaProvider{
 52			ID:    area.ID,
 53			Name:  area.Name,
 54			Key:   area.Key,
 55			Goals: shared.ToGoalProviders(area.Goals),
 56		})
 57	}
 58
 59	return providers
 60}
 61
 62func registerResources(
 63	mcpServer *mcp.Server,
 64	areaProviders []shared.AreaProvider,
 65	habitProviders []shared.HabitProvider,
 66	notebookProviders []shared.NotebookProvider,
 67) {
 68	areasHandler := areas.NewHandler(areaProviders)
 69	mcpServer.AddResource(&mcp.Resource{
 70		Name:        "areas",
 71		URI:         areas.ResourceURI,
 72		Description: areas.ResourceDescription,
 73		MIMEType:    "application/json",
 74	}, areasHandler.HandleRead)
 75
 76	habitsHandler := habits.NewHandler(habitProviders)
 77	mcpServer.AddResource(&mcp.Resource{
 78		Name:        "habits",
 79		URI:         habits.ResourceURI,
 80		Description: habits.ResourceDescription,
 81		MIMEType:    "application/json",
 82	}, habitsHandler.HandleRead)
 83
 84	notebooksHandler := notebooks.NewHandler(notebookProviders)
 85	mcpServer.AddResource(&mcp.Resource{
 86		Name:        "notebooks",
 87		URI:         notebooks.ResourceURI,
 88		Description: notebooks.ResourceDescription,
 89		MIMEType:    "application/json",
 90	}, notebooksHandler.HandleRead)
 91}
 92
 93func registerTools(
 94	mcpServer *mcp.Server,
 95	cfg *config.Config,
 96	accessToken string,
 97	areaProviders []shared.AreaProvider,
 98	habitProviders []shared.HabitProvider,
 99) {
100	tools := &cfg.MCP.Tools
101
102	if tools.GetTimestamp {
103		tsHandler := timestamp.NewHandler(cfg.MCP.Timezone)
104		mcp.AddTool(mcpServer, &mcp.Tool{
105			Name:        timestamp.ToolName,
106			Description: timestamp.ToolDescription,
107		}, tsHandler.Handle)
108	}
109
110	taskHandler := task.NewHandler(accessToken, areaProviders)
111
112	if tools.CreateTask {
113		mcp.AddTool(mcpServer, &mcp.Tool{
114			Name:        task.CreateToolName,
115			Description: task.CreateToolDescription,
116		}, taskHandler.HandleCreate)
117	}
118
119	if tools.UpdateTask {
120		mcp.AddTool(mcpServer, &mcp.Tool{
121			Name:        task.UpdateToolName,
122			Description: task.UpdateToolDescription,
123		}, taskHandler.HandleUpdate)
124	}
125
126	if tools.DeleteTask {
127		mcp.AddTool(mcpServer, &mcp.Tool{
128			Name:        task.DeleteToolName,
129			Description: task.DeleteToolDescription,
130		}, taskHandler.HandleDelete)
131	}
132
133	if tools.ListTasks {
134		mcp.AddTool(mcpServer, &mcp.Tool{
135			Name:        task.ListToolName,
136			Description: task.ListToolDescription,
137		}, taskHandler.HandleList)
138	}
139
140	if tools.ShowTask {
141		mcp.AddTool(mcpServer, &mcp.Tool{
142			Name:        task.ShowToolName,
143			Description: task.ShowToolDescription,
144		}, taskHandler.HandleShow)
145	}
146
147	if tools.TrackHabit {
148		habitHandler := habit.NewHandler(accessToken, habitProviders)
149		mcp.AddTool(mcpServer, &mcp.Tool{
150			Name:        habit.TrackToolName,
151			Description: habit.TrackToolDescription,
152		}, habitHandler.HandleTrack)
153	}
154}
155
156func runStdio(mcpServer *mcp.Server) error {
157	if err := mcpServer.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
158		return fmt.Errorf("stdio server error: %w", err)
159	}
160
161	return nil
162}
163
164func runSSE(cmd *cobra.Command, mcpServer *mcp.Server, cfg *config.Config) error {
165	hostPort := net.JoinHostPort(resolveHost(cfg), strconv.Itoa(resolvePort(cfg)))
166	handler := mcp.NewSSEHandler(func(_ *http.Request) *mcp.Server {
167		return mcpServer
168	}, nil)
169
170	fmt.Fprintf(cmd.OutOrStdout(), "SSE server listening on %s\n", hostPort)
171
172	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
173	if err := http.ListenAndServe(hostPort, handler); err != nil {
174		return fmt.Errorf("SSE server error: %w", err)
175	}
176
177	return nil
178}
179
180func runHTTP(cmd *cobra.Command, mcpServer *mcp.Server, cfg *config.Config) error {
181	hostPort := net.JoinHostPort(resolveHost(cfg), strconv.Itoa(resolvePort(cfg)))
182	handler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
183		return mcpServer
184	}, nil)
185
186	fmt.Fprintf(cmd.OutOrStdout(), "HTTP server listening on %s\n", hostPort)
187
188	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
189	if err := http.ListenAndServe(hostPort, handler); err != nil {
190		return fmt.Errorf("HTTP server error: %w", err)
191	}
192
193	return nil
194}