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	notetool "git.secluded.site/lune/internal/mcp/tools/note"
 25	persontool "git.secluded.site/lune/internal/mcp/tools/person"
 26	"git.secluded.site/lune/internal/mcp/tools/task"
 27	"git.secluded.site/lune/internal/mcp/tools/timestamp"
 28	"github.com/modelcontextprotocol/go-sdk/mcp"
 29	"github.com/spf13/cobra"
 30)
 31
 32var version = "dev"
 33
 34func newMCPServer(cfg *config.Config, accessToken string) *mcp.Server {
 35	mcpServer := mcp.NewServer(
 36		&mcp.Implementation{
 37			Name:    "lune",
 38			Version: version,
 39		},
 40		nil,
 41	)
 42
 43	areaProviders := toAreaProviders(cfg.Areas)
 44	habitProviders := shared.ToHabitProviders(cfg.Habits)
 45	notebookProviders := shared.ToNotebookProviders(cfg.Notebooks)
 46
 47	registerResources(mcpServer, areaProviders, habitProviders, notebookProviders)
 48	registerResourceTemplates(mcpServer, accessToken)
 49	registerTools(mcpServer, cfg, accessToken, areaProviders, habitProviders, notebookProviders)
 50
 51	return mcpServer
 52}
 53
 54func toAreaProviders(cfgAreas []config.Area) []shared.AreaProvider {
 55	providers := make([]shared.AreaProvider, 0, len(cfgAreas))
 56
 57	for _, area := range cfgAreas {
 58		providers = append(providers, shared.AreaProvider{
 59			ID:       area.ID,
 60			Name:     area.Name,
 61			Key:      area.Key,
 62			Workflow: area.Workflow,
 63			Goals:    shared.ToGoalProviders(area.Goals),
 64		})
 65	}
 66
 67	return providers
 68}
 69
 70func registerResources(
 71	mcpServer *mcp.Server,
 72	areaProviders []shared.AreaProvider,
 73	habitProviders []shared.HabitProvider,
 74	notebookProviders []shared.NotebookProvider,
 75) {
 76	areasHandler := areas.NewHandler(areaProviders)
 77	mcpServer.AddResource(&mcp.Resource{
 78		Name:        "areas",
 79		URI:         areas.ResourceURI,
 80		Description: areas.ResourceDescription,
 81		MIMEType:    "application/json",
 82	}, areasHandler.HandleRead)
 83
 84	habitsHandler := habits.NewHandler(habitProviders)
 85	mcpServer.AddResource(&mcp.Resource{
 86		Name:        "habits",
 87		URI:         habits.ResourceURI,
 88		Description: habits.ResourceDescription,
 89		MIMEType:    "application/json",
 90	}, habitsHandler.HandleRead)
 91
 92	notebooksHandler := notebooks.NewHandler(notebookProviders)
 93	mcpServer.AddResource(&mcp.Resource{
 94		Name:        "notebooks",
 95		URI:         notebooks.ResourceURI,
 96		Description: notebooks.ResourceDescription,
 97		MIMEType:    "application/json",
 98	}, notebooksHandler.HandleRead)
 99}
100
101func registerResourceTemplates(mcpServer *mcp.Server, accessToken string) {
102	taskHandler := taskrs.NewHandler(accessToken)
103	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
104		Name:        "task",
105		URITemplate: taskrs.ResourceTemplate,
106		Description: taskrs.ResourceDescription,
107		MIMEType:    "application/json",
108	}, taskHandler.HandleRead)
109
110	noteHandler := noters.NewHandler(accessToken)
111	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
112		Name:        "note",
113		URITemplate: noters.ResourceTemplate,
114		Description: noters.ResourceDescription,
115		MIMEType:    "application/json",
116	}, noteHandler.HandleRead)
117
118	personHandler := personrs.NewHandler(accessToken)
119	mcpServer.AddResourceTemplate(&mcp.ResourceTemplate{
120		Name:        "person",
121		URITemplate: personrs.ResourceTemplate,
122		Description: personrs.ResourceDescription,
123		MIMEType:    "application/json",
124	}, personHandler.HandleRead)
125}
126
127func registerTools(
128	mcpServer *mcp.Server,
129	cfg *config.Config,
130	accessToken string,
131	areaProviders []shared.AreaProvider,
132	habitProviders []shared.HabitProvider,
133	notebookProviders []shared.NotebookProvider,
134) {
135	tools := &cfg.MCP.Tools
136
137	if tools.GetTimestamp {
138		tsHandler := timestamp.NewHandler(cfg.MCP.Timezone)
139		mcp.AddTool(mcpServer, &mcp.Tool{
140			Name:        timestamp.ToolName,
141			Description: timestamp.ToolDescription,
142		}, tsHandler.Handle)
143	}
144
145	registerTaskTools(mcpServer, cfg, tools, accessToken, areaProviders)
146	registerNoteTools(mcpServer, tools, accessToken, notebookProviders)
147	registerPersonTools(mcpServer, tools, accessToken)
148
149	if tools.TrackHabit {
150		habitHandler := habit.NewHandler(accessToken, habitProviders)
151		mcp.AddTool(mcpServer, &mcp.Tool{
152			Name:        habit.TrackToolName,
153			Description: habit.TrackToolDescription,
154		}, habitHandler.HandleTrack)
155	}
156
157	if tools.CreateJournal {
158		journalHandler := journal.NewHandler(accessToken)
159		mcp.AddTool(mcpServer, &mcp.Tool{
160			Name:        journal.CreateToolName,
161			Description: journal.CreateToolDescription,
162		}, journalHandler.HandleCreate)
163	}
164}
165
166func registerTaskTools(
167	mcpServer *mcp.Server,
168	cfg *config.Config,
169	tools *config.ToolsConfig,
170	accessToken string,
171	areaProviders []shared.AreaProvider,
172) {
173	taskHandler := task.NewHandler(accessToken, cfg, areaProviders)
174
175	if tools.CreateTask {
176		mcp.AddTool(mcpServer, &mcp.Tool{
177			Name:        task.CreateToolName,
178			Description: task.CreateToolDescription,
179		}, taskHandler.HandleCreate)
180	}
181
182	if tools.UpdateTask {
183		mcp.AddTool(mcpServer, &mcp.Tool{
184			Name:        task.UpdateToolName,
185			Description: task.UpdateToolDescription,
186		}, taskHandler.HandleUpdate)
187	}
188
189	if tools.DeleteTask {
190		mcp.AddTool(mcpServer, &mcp.Tool{
191			Name:        task.DeleteToolName,
192			Description: task.DeleteToolDescription,
193		}, taskHandler.HandleDelete)
194	}
195
196	if tools.ListTasks {
197		mcp.AddTool(mcpServer, &mcp.Tool{
198			Name:        task.ListToolName,
199			Description: task.ListToolDescription,
200		}, taskHandler.HandleList)
201	}
202
203	if tools.ShowTask {
204		mcp.AddTool(mcpServer, &mcp.Tool{
205			Name:        task.ShowToolName,
206			Description: task.ShowToolDescription,
207		}, taskHandler.HandleShow)
208	}
209}
210
211func registerNoteTools(
212	mcpServer *mcp.Server,
213	tools *config.ToolsConfig,
214	accessToken string,
215	notebookProviders []shared.NotebookProvider,
216) {
217	noteHandler := notetool.NewHandler(accessToken, notebookProviders)
218
219	if tools.CreateNote {
220		mcp.AddTool(mcpServer, &mcp.Tool{
221			Name:        notetool.CreateToolName,
222			Description: notetool.CreateToolDescription,
223		}, noteHandler.HandleCreate)
224	}
225
226	if tools.UpdateNote {
227		mcp.AddTool(mcpServer, &mcp.Tool{
228			Name:        notetool.UpdateToolName,
229			Description: notetool.UpdateToolDescription,
230		}, noteHandler.HandleUpdate)
231	}
232
233	if tools.DeleteNote {
234		mcp.AddTool(mcpServer, &mcp.Tool{
235			Name:        notetool.DeleteToolName,
236			Description: notetool.DeleteToolDescription,
237		}, noteHandler.HandleDelete)
238	}
239
240	if tools.ListNotes {
241		mcp.AddTool(mcpServer, &mcp.Tool{
242			Name:        notetool.ListToolName,
243			Description: notetool.ListToolDescription,
244		}, noteHandler.HandleList)
245	}
246}
247
248func registerPersonTools(
249	mcpServer *mcp.Server,
250	tools *config.ToolsConfig,
251	accessToken string,
252) {
253	personHandler := persontool.NewHandler(accessToken)
254
255	if tools.CreatePerson {
256		mcp.AddTool(mcpServer, &mcp.Tool{
257			Name:        persontool.CreateToolName,
258			Description: persontool.CreateToolDescription,
259		}, personHandler.HandleCreate)
260	}
261
262	if tools.UpdatePerson {
263		mcp.AddTool(mcpServer, &mcp.Tool{
264			Name:        persontool.UpdateToolName,
265			Description: persontool.UpdateToolDescription,
266		}, personHandler.HandleUpdate)
267	}
268
269	if tools.DeletePerson {
270		mcp.AddTool(mcpServer, &mcp.Tool{
271			Name:        persontool.DeleteToolName,
272			Description: persontool.DeleteToolDescription,
273		}, personHandler.HandleDelete)
274	}
275
276	if tools.ListPeople {
277		mcp.AddTool(mcpServer, &mcp.Tool{
278			Name:        persontool.ListToolName,
279			Description: persontool.ListToolDescription,
280		}, personHandler.HandleList)
281	}
282
283	if tools.PersonTimeline {
284		mcp.AddTool(mcpServer, &mcp.Tool{
285			Name:        persontool.TimelineToolName,
286			Description: persontool.TimelineToolDescription,
287		}, personHandler.HandleTimeline)
288	}
289}
290
291func runStdio(mcpServer *mcp.Server) error {
292	if err := mcpServer.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
293		return fmt.Errorf("stdio server error: %w", err)
294	}
295
296	return nil
297}
298
299func runSSE(cmd *cobra.Command, mcpServer *mcp.Server, cfg *config.Config) error {
300	hostPort := net.JoinHostPort(resolveHost(cfg), strconv.Itoa(resolvePort(cfg)))
301	handler := mcp.NewSSEHandler(func(_ *http.Request) *mcp.Server {
302		return mcpServer
303	}, nil)
304
305	fmt.Fprintf(cmd.OutOrStdout(), "SSE server listening on %s\n", hostPort)
306
307	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
308	if err := http.ListenAndServe(hostPort, handler); err != nil {
309		return fmt.Errorf("SSE server error: %w", err)
310	}
311
312	return nil
313}
314
315func runHTTP(cmd *cobra.Command, mcpServer *mcp.Server, cfg *config.Config) error {
316	hostPort := net.JoinHostPort(resolveHost(cfg), strconv.Itoa(resolvePort(cfg)))
317	handler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
318		return mcpServer
319	}, nil)
320
321	fmt.Fprintf(cmd.OutOrStdout(), "HTTP server listening on %s\n", hostPort)
322
323	//nolint:gosec // MCP SDK controls server lifecycle; timeouts not applicable
324	if err := http.ListenAndServe(hostPort, handler); err != nil {
325		return fmt.Errorf("HTTP server error: %w", err)
326	}
327
328	return nil
329}