๐Ÿ“ฆ veggiemonk / backlog

๐Ÿ“„ server.go ยท 142 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142// Package mcp creates an MCP server for managing the tasks.
package mcp

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/imjasonh/version"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/veggiemonk/backlog/internal/commit"
	"github.com/veggiemonk/backlog/internal/core"
	"github.com/veggiemonk/backlog/internal/logging"
)

// TaskStore interface matches the one expected by the MCP handlers
type TaskStore interface {
	Get(id string) (core.Task, error)
	Create(params core.CreateTaskParams) (core.Task, error)
	Update(task *core.Task, params core.EditTaskParams) error
	List(params core.ListTasksParams) (core.ListResult, error)
	Path(t core.Task) string
	Archive(id core.TaskID) (string, error)
}

// Server wraps the MCP server with backlog-specific functionality
type Server struct {
	mcpServer *mcp.Server
	handler   *handler
}

// handler contains the MCP tool implementations
type handler struct {
	store      TaskStore
	mu         *sync.Mutex
	autoCommit bool
}

func (h *handler) commit(id, title, path, oldPath, msg string) error {
	if h.autoCommit {
		commitMsg := fmt.Sprintf("feat(task): %s %s - \"%s\"", msg, id, title)
		if err := commit.Add(path, oldPath, commitMsg); err != nil {
			return fmt.Errorf("auto-commit failed: %w", err)
		}
	}
	return nil
}

// NewServer creates a new MCP server configured for backlog
func NewServer(store TaskStore, autoCommit bool) (*Server, error) {
	ver := version.Get()
	mcpServer := mcp.NewServer(
		&mcp.Implementation{
			Name:    "backlog MCP Server",
			Title:   "backlog",
			Version: ver.Version,
		}, &mcp.ServerOptions{
			Instructions: "Use this MCP server to manage your backlog tasks programmatically." + PromptMCPInstructions,
			HasPrompts:   true,
			HasTools:     true,
			HasResources: true,
		},
	)

	h := &handler{
		store:      store,
		mu:         &sync.Mutex{},
		autoCommit: autoCommit,
	}

	server := &Server{
		mcpServer: mcpServer,
		handler:   h,
	}

	// Install all functionality
	if err := server.addTools(); err != nil {
		return nil, err
	}
	server.addResources()
	server.addPrompts()

	return server, nil
}

// RunHTTP starts the server with streamable HTTP transport
func (s *Server) RunHTTP(ctx context.Context, port int) error {
	addr := net.JoinHostPort("localhost", strconv.Itoa(port))
	handler := mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server { return s.mcpServer }, nil)
	logging.Info("MCP server starting", "transport", "http", "address", addr)
	server := &http.Server{
		Addr:              addr,
		Handler:           handler,
		ReadTimeout:       10 * 1e9, // 10 seconds
		WriteTimeout:      10 * 1e9, // 10 seconds
		IdleTimeout:       60 * 1e9, // 60 seconds
		ReadHeaderTimeout: 5 * 1e9,  // 5 seconds
	}
	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = server.Shutdown(shutdownCtx) // best effort shutdown
	}()
	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		return err
	}
	return nil
}

// RunStdio starts the server with stdio transport
func (s *Server) RunStdio(ctx context.Context) error {
	return s.mcpServer.Run(ctx, &mcp.StdioTransport{})
}

// addTools adds all MCP tools to the server
func (s *Server) addTools() error {
	if err := s.registerTaskCreate(); err != nil {
		return err
	}
	if err := s.registerTaskBatchCreate(); err != nil {
		return err
	}
	if err := s.registerTaskList(); err != nil {
		return err
	}
	if err := s.registerTaskView(); err != nil {
		return err
	}
	if err := s.registerTaskEdit(); err != nil {
		return err
	}
	if err := s.registerTaskArchive(); err != nil {
		return err
	}
	return nil
}