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
}