1package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "log"
10 "net/http"
11 "os"
12
13 "github.com/BurntSushi/toml"
14 "github.com/mark3labs/mcp-go/mcp"
15 "github.com/mark3labs/mcp-go/server"
16)
17
18// Area represents a Lunatask area with its name and ID
19type Area struct {
20 Name string `toml:"name"`
21 ID string `toml:"id"`
22}
23
24// Config holds the application's configuration loaded from TOML
25type Config struct {
26 AccessToken string `toml:"access_token"`
27 Areas []Area `toml:"areas"`
28}
29
30
31// LunataskCreateTaskRequest represents the request payload for creating a task in Lunatask
32type LunataskCreateTaskRequest struct {
33 Name string `json:"name"`
34 Source string `json:"source"`
35 AreaID string `json:"area_id"`
36}
37
38// LunataskCreateTaskResponse represents the response from Lunatask API when creating a task
39type LunataskCreateTaskResponse struct {
40 Task struct {
41 ID string `json:"id"`
42 } `json:"task"`
43}
44
45func main() {
46 // Determine config path from command-line arguments
47 configPath := "./config.toml"
48 for i, arg := range os.Args {
49 if arg == "-c" || arg == "--config" {
50 if i+1 < len(os.Args) {
51 configPath = os.Args[i+1]
52 }
53 }
54 }
55
56 // Load and decode TOML config
57 var config Config
58 if _, err := toml.DecodeFile(configPath, &config); err != nil {
59 log.Fatalf("Failed to load config file %s: %v", configPath, err)
60 }
61
62 if config.AccessToken == "" || config.AreaID == "" {
63 log.Fatalf("Config file must provide access_token and area_id")
64 }
65
66 mcpServer := NewMCPServer(&config)
67
68 sseServer := server.NewSSEServer(mcpServer, server.WithBaseURL("http://localhost:8080"))
69 log.Printf("SSE server listening on :8080")
70 if err := sseServer.Start(":8080"); err != nil {
71 log.Fatalf("Server error: %v", err)
72 }
73}
74
75func NewMCPServer(config *Config) *server.MCPServer {
76 hooks := &server.Hooks{}
77
78 hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
79 fmt.Printf("beforeAny: %s, %v, %v\n", method, id, message)
80 })
81 hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
82 fmt.Printf("onSuccess: %s, %v, %v, %v\n", method, id, message, result)
83 })
84 hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
85 fmt.Printf("onError: %s, %v, %v, %v\n", method, id, message, err)
86 })
87 hooks.AddBeforeInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest) {
88 fmt.Printf("beforeInitialize: %v, %v\n", id, message)
89 })
90 hooks.AddAfterInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
91 fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
92 })
93 hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
94 fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
95 })
96 hooks.AddBeforeCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest) {
97 fmt.Printf("beforeCallTool: %v, %v\n", id, message)
98 })
99
100 mcpServer := server.NewMCPServer(
101 "Lunatask MCP Server",
102 "1.0.0",
103 server.WithHooks(hooks),
104 )
105
106 // Pass config to the handler through closure
107 mcpServer.AddTool(mcp.NewTool("create_task",
108 mcp.WithDescription("Creates a new task"),
109 mcp.WithString("name",
110 mcp.Description("Name of the task"),
111 mcp.Required(),
112 ),
113 ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
114 return handleCreateTask(ctx, request, config)
115 })
116
117 return mcpServer
118}
119
120func handleCreateTask(
121 ctx context.Context,
122 request mcp.CallToolRequest,
123 config *Config,
124) (*mcp.CallToolResult, error) {
125 // Extract the name parameter from the request
126 arguments := request.Params.Arguments
127 name, ok := arguments["name"].(string)
128 if !ok {
129 return nil, fmt.Errorf("invalid value for argument 'name'")
130 }
131
132 // Create the payload for the Lunatask API using our struct
133 payload := LunataskCreateTaskRequest{
134 Name: name,
135 Source: "lmcps",
136 AreaID: config.AreaID,
137 }
138
139 // Convert the payload to JSON
140 payloadBytes, err := json.Marshal(payload)
141 if err != nil {
142 return nil, fmt.Errorf("failed to marshal payload: %w", err)
143 }
144
145 // Create the HTTP request
146 req, err := http.NewRequestWithContext(
147 ctx,
148 "POST",
149 "https://api.lunatask.app/v1/tasks",
150 bytes.NewBuffer(payloadBytes),
151 )
152 if err != nil {
153 return nil, fmt.Errorf("failed to create HTTP request: %w", err)
154 }
155
156 // Set the required headers
157 req.Header.Set("Content-Type", "application/json")
158 req.Header.Set("Authorization", "bearer " + config.AccessToken)
159
160 // Send the request
161 client := &http.Client{}
162 resp, err := client.Do(req)
163 if err != nil {
164 return nil, fmt.Errorf("failed to send HTTP request: %w", err)
165 }
166 defer resp.Body.Close()
167
168 // Handle duplicate task (204 No Content)
169 if resp.StatusCode == http.StatusNoContent {
170 return &mcp.CallToolResult{
171 Content: []mcp.Content{
172 mcp.TextContent{
173 Type: "text",
174 Text: "Duplicate task found, no new task created.",
175 },
176 },
177 }, nil
178 }
179
180 // Check for error responses
181 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
182 respBody, _ := io.ReadAll(resp.Body)
183 return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody))
184 }
185
186 // Parse the response
187 var response LunataskCreateTaskResponse
188
189 respBody, err := io.ReadAll(resp.Body)
190 if err != nil {
191 return nil, fmt.Errorf("failed to read response body: %w", err)
192 }
193
194 err = json.Unmarshal(respBody, &response)
195 if err != nil {
196 return nil, fmt.Errorf("failed to parse response: %w", err)
197 }
198
199 // Return success result
200 return &mcp.CallToolResult{
201 Content: []mcp.Content{
202 mcp.TextContent{
203 Type: "text",
204 Text: fmt.Sprintf("Task created successfully! Task ID: %s", response.Task.ID),
205 },
206 },
207 }, nil
208}