@@ -0,0 +1,202 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/BurntSushi/toml"
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+)
+
+// Config holds the application's configuration loaded from TOML
+type Config struct {
+ AccessToken string `toml:"access_token"`
+ AreaID string `toml:"area_id"`
+}
+
+
+// LunataskCreateTaskRequest represents the request payload for creating a task in Lunatask
+type LunataskCreateTaskRequest struct {
+ Name string `json:"name"`
+ Source string `json:"source"`
+ AreaID string `json:"area_id"`
+}
+
+// LunataskCreateTaskResponse represents the response from Lunatask API when creating a task
+type LunataskCreateTaskResponse struct {
+ Task struct {
+ ID string `json:"id"`
+ } `json:"task"`
+}
+
+func main() {
+ // Determine config path from command-line arguments
+ configPath := "./config.toml"
+ for i, arg := range os.Args {
+ if arg == "-c" || arg == "--config" {
+ if i+1 < len(os.Args) {
+ configPath = os.Args[i+1]
+ }
+ }
+ }
+
+ // Load and decode TOML config
+ var config Config
+ if _, err := toml.DecodeFile(configPath, &config); err != nil {
+ log.Fatalf("Failed to load config file %s: %v", configPath, err)
+ }
+
+ if config.AccessToken == "" || config.AreaID == "" {
+ log.Fatalf("Config file must provide access_token and area_id")
+ }
+
+ mcpServer := NewMCPServer(&config)
+
+ sseServer := server.NewSSEServer(mcpServer, server.WithBaseURL("http://localhost:8080"))
+ log.Printf("SSE server listening on :8080")
+ if err := sseServer.Start(":8080"); err != nil {
+ log.Fatalf("Server error: %v", err)
+ }
+}
+
+func NewMCPServer(config *Config) *server.MCPServer {
+ hooks := &server.Hooks{}
+
+ hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
+ fmt.Printf("beforeAny: %s, %v, %v\n", method, id, message)
+ })
+ hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) {
+ fmt.Printf("onSuccess: %s, %v, %v, %v\n", method, id, message, result)
+ })
+ hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) {
+ fmt.Printf("onError: %s, %v, %v, %v\n", method, id, message, err)
+ })
+ hooks.AddBeforeInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest) {
+ fmt.Printf("beforeInitialize: %v, %v\n", id, message)
+ })
+ hooks.AddAfterInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
+ fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
+ })
+ hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
+ fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
+ })
+ hooks.AddBeforeCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest) {
+ fmt.Printf("beforeCallTool: %v, %v\n", id, message)
+ })
+
+ mcpServer := server.NewMCPServer(
+ "Lunatask MCP Server",
+ "1.0.0",
+ server.WithHooks(hooks),
+ )
+
+ // Pass config to the handler through closure
+ mcpServer.AddTool(mcp.NewTool("create_task",
+ mcp.WithDescription("Creates a new task"),
+ mcp.WithString("name",
+ mcp.Description("Name of the task"),
+ mcp.Required(),
+ ),
+ ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ return handleCreateTask(ctx, request, config)
+ })
+
+ return mcpServer
+}
+
+func handleCreateTask(
+ ctx context.Context,
+ request mcp.CallToolRequest,
+ config *Config,
+) (*mcp.CallToolResult, error) {
+ // Extract the name parameter from the request
+ arguments := request.Params.Arguments
+ name, ok := arguments["name"].(string)
+ if !ok {
+ return nil, fmt.Errorf("invalid value for argument 'name'")
+ }
+
+ // Create the payload for the Lunatask API using our struct
+ payload := LunataskCreateTaskRequest{
+ Name: name,
+ Source: "lmcps",
+ AreaID: config.AreaID,
+ }
+
+ // Convert the payload to JSON
+ payloadBytes, err := json.Marshal(payload)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal payload: %w", err)
+ }
+
+ // Create the HTTP request
+ req, err := http.NewRequestWithContext(
+ ctx,
+ "POST",
+ "https://api.lunatask.app/v1/tasks",
+ bytes.NewBuffer(payloadBytes),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create HTTP request: %w", err)
+ }
+
+ // Set the required headers
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", "bearer " + config.AccessToken)
+
+ // Send the request
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to send HTTP request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Handle duplicate task (204 No Content)
+ if resp.StatusCode == http.StatusNoContent {
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: "Duplicate task found, no new task created.",
+ },
+ },
+ }, nil
+ }
+
+ // Check for error responses
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ respBody, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody))
+ }
+
+ // Parse the response
+ var response LunataskCreateTaskResponse
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+
+ err = json.Unmarshal(respBody, &response)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ // Return success result
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("Task created successfully! Task ID: %s", response.Task.ID),
+ },
+ },
+ }, nil
+}