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}