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	noters "git.secluded.site/lune/internal/mcp/resources/note"
 18	"git.secluded.site/lune/internal/mcp/resources/notebooks"
 19	personrs "git.secluded.site/lune/internal/mcp/resources/person"
 20	taskrs "git.secluded.site/lune/internal/mcp/resources/task"
 21	"git.secluded.site/lune/internal/mcp/shared"
 22	"git.secluded.site/lune/internal/mcp/tools/habit"
 23	"git.secluded.site/lune/internal/mcp/tools/journal"
 24	"git.secluded.site/lune/internal/mcp/tools/task"
 25	"git.secluded.site/lune/internal/mcp/tools/timestamp"
 26	"github.com/modelcontextprotocol/go-sdk/mcp"
 27	"github.com/spf13/cobra"
 28)
 29
 30var version = "dev"
 31
 32func newMCPServer(cfg *config.Config, accessToken string) *mcp.Server {
 33	mcpServer := mcp.NewServer(
 34		&mcp.Implementation{
 35			Name:    "lune",
 36			Version: version,
 37		},
 38		nil,
 39	)
 40
 41	areaProviders := toAreaProviders(cfg.Areas)
 42	habitProviders := shared.ToHabitProviders(cfg.Habits)
 43	notebookProviders := shared.ToNotebookProviders(cfg.Notebooks)
 44
 45	registerResources(mcpServer, areaProviders, habitProviders, notebookProviders)
 46	registerResourceTemplates(mcpServer, accessToken)
 47	registerTools(mcpServer, cfg, accessToken, areaProviders, habitProviders)
 48
 49	return mcpServer
 50}
 51
 52func toAreaProviders(cfgAreas []config.Area) []shared.AreaProvider {
 53	providers := make([]shared.AreaProvider, 0, len(cfgAreas))
 54
 55	for _, area := range cfgAreas {
 56		providers = append(providers, shared.AreaProvider{
 57			ID:    area.ID,
 58			Name:  area.Name,
 59			Key:   area.Key,
 60			Goals: shared.ToGoalProviders(area.Goals),
 61		})
 62	}
 63
 64	return providers
 65}
 66
 67func registerResources(
 68	mcpServer *mcp.Server,
 69	areaProviders []shared.AreaProvider,
 70	habitProviders []shared.HabitProvider,
 71	notebookProviders []shared.NotebookProvider,
 72) {
 73	areasHandler := areas.NewHandler(areaProviders)
 74	mcpServer.AddResource(&mcp.Resource{
 75		Name:        "areas",
 76		URI:         areas.ResourceURI,
 77		Description: areas.ResourceDescription,
 78		MIMEType:    "application/json",
 79	}, areasHandler.HandleRead)
 80
 81	habitsHandler := habits.NewHandler(habitProviders)
 82	mcpServer.AddResource(&mcp.Resource{
 83		Name:        "habits",
 84		URI:         habits.ResourceURI,
 85		Description: habits.ResourceDescription,
 86		MIMEType:    "application/json",
 87	}, habitsHandler.HandleRead)
 88
 89	notebooksHandler := notebooks.NewHandler(notebookProviders)
 90	mcpServer.AddResource(&mcp.Resource{
 91		Name:        "notebooks",
 92		URI:         notebooks.ResourceURI,
 93		Description: notebooks.ResourceDescription,
 94		MIMEType:    "application/json",
 95	}, notebooksHandler.HandleRead)
 96}
 97
 98func registerResourceTemplates(mcpServer *mcp.Server, accessToken string) {
 99	taskHandler := taskrs.NewHandler(accessToken)
100	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
101		Name:        "task",
102		URITemplate: taskrs.ResourceTemplate,
103		Description: taskrs.ResourceDescription,
104		MIMEType:    "application/json",
105	}, taskHandler.HandleRead)
106
107	noteHandler := noters.NewHandler(accessToken)
108	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
109		Name:        "note",
110		URITemplate: noters.ResourceTemplate,
111		Description: noters.ResourceDescription,
112		MIMEType:    "application/json",
113	}, noteHandler.HandleRead)
114
115	personHandler := personrs.NewHandler(accessToken)
116	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
117		Name:        "person",
118		URITemplate: personrs.ResourceTemplate,
119		Description: personrs.ResourceDescription,
120		MIMEType:    "application/json",
121	}, personHandler.HandleRead)
122}
123
124func registerTools(
125	mcpServer *mcp.Server,
126	cfg *config.Config,
127	accessToken string,
128	areaProviders []shared.AreaProvider,
129	habitProviders []shared.HabitProvider,
130) {
131	tools := &cfg.MCP.Tools
132
133	if tools.GetTimestamp {
134		tsHandler := timestamp.NewHandler(cfg.MCP.Timezone)
135		mcp.AddTool(mcpServer, &mcp.Tool{
136			Name:        timestamp.ToolName,
137			Description: timestamp.ToolDescription,
138		}, tsHandler.Handle)
139	}
140
141	registerTaskTools(mcpServer, tools, accessToken, areaProviders)
142
143	if tools.TrackHabit {
144		habitHandler := habit.NewHandler(accessToken, habitProviders)
145		mcp.AddTool(mcpServer, &mcp.Tool{
146			Name:        habit.TrackToolName,
147			Description: habit.TrackToolDescription,
148		}, habitHandler.HandleTrack)
149	}
150
151	if tools.CreateJournal {
152		journalHandler := journal.NewHandler(accessToken)
153		mcp.AddTool(mcpServer, &mcp.Tool{
154			Name:        journal.CreateToolName,
155			Description: journal.CreateToolDescription,
156		}, journalHandler.HandleCreate)
157	}
158}
159
160func registerTaskTools(
161	mcpServer *mcp.Server,
162	tools *config.ToolsConfig,
163	accessToken string,
164	areaProviders []shared.AreaProvider,
165) {
166	taskHandler := task.NewHandler(accessToken, areaProviders)
167
168	if tools.CreateTask {
169		mcp.AddTool(mcpServer, &mcp.Tool{
170			Name:        task.CreateToolName,
171			Description: task.CreateToolDescription,
172		}, taskHandler.HandleCreate)
173	}
174
175	if tools.UpdateTask {
176		mcp.AddTool(mcpServer, &mcp.Tool{
177			Name:        task.UpdateToolName,
178			Description: task.UpdateToolDescription,
179		}, taskHandler.HandleUpdate)
180	}
181
182	if tools.DeleteTask {
183		mcp.AddTool(mcpServer, &mcp.Tool{
184			Name:        task.DeleteToolName,
185			Description: task.DeleteToolDescription,
186		}, taskHandler.HandleDelete)
187	}
188
189	if tools.ListTasks {
190		mcp.AddTool(mcpServer, &mcp.Tool{
191			Name:        task.ListToolName,
192			Description: task.ListToolDescription,
193		}, taskHandler.HandleList)
194	}
195
196	if tools.ShowTask {
197		mcp.AddTool(mcpServer, &mcp.Tool{
198			Name:        task.ShowToolName,
199			Description: task.ShowToolDescription,
200		}, taskHandler.HandleShow)
201	}
202}
203
204func runStdio(mcpServer *mcp.Server) error {
205	if err := mcpServer.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
206		return fmt.Errorf("stdio server error: %w", err)
207	}
208
209	return nil
210}
211
212func runSSE(cmd *cobra.Command, mcpServer *mcp.Server, cfg *config.Config) error {
213	hostPort := net.JoinHostPort(resolveHost(cfg), strconv.Itoa(resolvePort(cfg)))
214	handler := mcp.NewSSEHandler(func(_ *http.Request) *mcp.Server {
215		return mcpServer
216	}, nil)
217
218	fmt.Fprintf(cmd.OutOrStdout(), "SSE server listening on %s\n", hostPort)
219
220	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
221	if err := http.ListenAndServe(hostPort, handler); err != nil {
222		return fmt.Errorf("SSE server error: %w", err)
223	}
224
225	return nil
226}
227
228func runHTTP(cmd *cobra.Command, mcpServer *mcp.Server, cfg *config.Config) error {
229	hostPort := net.JoinHostPort(resolveHost(cfg), strconv.Itoa(resolvePort(cfg)))
230	handler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
231		return mcpServer
232	}, nil)
233
234	fmt.Fprintf(cmd.OutOrStdout(), "HTTP server listening on %s\n", hostPort)
235
236	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
237	if err := http.ListenAndServe(hostPort, handler); err != nil {
238		return fmt.Errorf("HTTP server error: %w", err)
239	}
240
241	return nil
242}