main.go

  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}