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}