Skip to content

Commit 84ea84e

Browse files
authored
Merge pull request #48 from blaknite/debug-build-with-test-engine
Debug build with test engine
2 parents be00803 + 56906c3 commit 84ea84e

22 files changed

Lines changed: 2660 additions & 195 deletions

Dockerfile.local

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Build stage
2+
FROM public.ecr.aws/docker/library/golang:1.24.2 AS builder
3+
4+
WORKDIR /app
5+
6+
# Copy go.mod and go.sum first to leverage Docker cache
7+
COPY go.mod go.sum ./
8+
RUN go mod download
9+
10+
# Copy the rest of the source code
11+
COPY . .
12+
13+
# Build the binary
14+
RUN CGO_ENABLED=0 go build -o buildkite-mcp-server ./cmd/buildkite-mcp-server/main.go
15+
16+
# Final stage
17+
FROM alpine:3.21
18+
19+
# Install ca-certificates for HTTPS requests
20+
RUN apk --no-cache add ca-certificates
21+
22+
WORKDIR /app
23+
24+
# Copy the binary from the builder stage
25+
COPY --from=builder /app/buildkite-mcp-server /app/buildkite-mcp-server
26+
27+
# Set the entrypoint to run the server in stdio mode
28+
ENTRYPOINT ["/app/buildkite-mcp-server", "stdio"]

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ This is an [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introd
1818
* `get_build` - Get a build in Buildkite
1919
* `current_user` - Get the current user
2020
* `user_token_organization` - Get the organization associated with the user token used for this request
21+
* `get_jobs` - Get a list of jobs for a build
2122
* `get_job_logs` - Get the logs of a job in a Buildkite build
2223
* `access_token` - Get the details for the API access token that was used to authenticate the request
2324
* `list_artifacts` - List the artifacts for a Buildkite build
2425
* `get_artifact` - Get an artifact from a Buildkite build
2526
* `list_annotations` - List the annotations for a Buildkite build
27+
* `list_test_runs` - List all test runs for a test suite in Test Engine
28+
* `get_test_run` - Get a specific test run in Test Engine
29+
* `get_failed_test_executions` - Get a list of the failed test executions for a run in Test Engine
30+
* `get_test` - Get a test in Test Engine
2631

2732
Example of the `get_pipeline` tool in action.
2833

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.24.2
44

55
require (
66
github.com/alecthomas/kong v1.11.0
7-
github.com/buildkite/go-buildkite/v4 v4.1.0
7+
github.com/buildkite/go-buildkite/v4 v4.4.0
88
github.com/buildkite/terminal-to-html/v3 v3.16.8
99
github.com/huantt/plaintext-extractor v1.1.0
1010
github.com/mark3labs/mcp-go v0.31.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ github.com/alecthomas/kong v1.11.0 h1:y++1gI7jf8O7G7l4LZo5ASFhrhJvzc+WgF/arranEm
44
github.com/alecthomas/kong v1.11.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
55
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
66
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
7-
github.com/buildkite/go-buildkite/v4 v4.1.0 h1:n1f3EAe8/64ju9QjSWJYMjD8++mn1pOLK/tZscD5DKo=
8-
github.com/buildkite/go-buildkite/v4 v4.1.0/go.mod h1:xlYVIETMCk46KUkmfRoztoIf888KwdY5uZXNinZ1PX0=
7+
github.com/buildkite/go-buildkite/v4 v4.4.0 h1:RnAhNs+7Xyb+I+kdHiH1uudjjHQZudIGDeYy45WYVCA=
8+
github.com/buildkite/go-buildkite/v4 v4.4.0/go.mod h1:fMPu+/7hXzJ7Gy3HpGuVCLgeHqzDdrgHuwQvt8p370I=
99
github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc=
1010
github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM=
1111
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf h1:yxlp0s+Sge9UsKEK0Bsvjiopb9XRk+vxylmZ9eGBfm8=

internal/buildkite/buildkite.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,80 @@ func withPagination() mcp.ToolOption {
3333
)(tool)
3434
}
3535
}
36+
37+
// ClientSidePaginationParams represents parameters for client-side pagination
38+
type ClientSidePaginationParams struct {
39+
Page int
40+
PerPage int
41+
}
42+
43+
// ClientSidePaginatedResult represents a paginated result for client-side pagination
44+
type ClientSidePaginatedResult[T any] struct {
45+
Items []T `json:"items"`
46+
Page int `json:"page"`
47+
PerPage int `json:"per_page"`
48+
Total int `json:"total"`
49+
TotalPages int `json:"total_pages"`
50+
HasNext bool `json:"has_next"`
51+
HasPrev bool `json:"has_prev"`
52+
}
53+
54+
// withClientSidePagination adds client-side pagination options to a tool
55+
func withClientSidePagination() mcp.ToolOption {
56+
return func(tool *mcp.Tool) {
57+
mcp.WithNumber("page",
58+
mcp.Description("Page number for pagination (min 1)"),
59+
mcp.Min(1),
60+
)(tool)
61+
62+
mcp.WithNumber("perPage",
63+
mcp.Description("Results per page for pagination (min 1, max 100)"),
64+
mcp.Min(1),
65+
mcp.Max(100),
66+
)(tool)
67+
}
68+
}
69+
70+
// getClientSidePaginationParams extracts client-side pagination parameters from request
71+
// Always returns pagination params with sensible defaults
72+
func getClientSidePaginationParams(r mcp.CallToolRequest) ClientSidePaginationParams {
73+
page := r.GetInt("page", 1)
74+
perPage := r.GetInt("perPage", 25) // Default page size for client-side pagination
75+
76+
return ClientSidePaginationParams{
77+
Page: page,
78+
PerPage: perPage,
79+
}
80+
}
81+
82+
// applyClientSidePagination applies client-side pagination to a slice of items
83+
func applyClientSidePagination[T any](items []T, params ClientSidePaginationParams) ClientSidePaginatedResult[T] {
84+
total := len(items)
85+
totalPages := (total + params.PerPage - 1) / params.PerPage
86+
if totalPages == 0 {
87+
totalPages = 1
88+
}
89+
90+
startIndex := (params.Page - 1) * params.PerPage
91+
endIndex := startIndex + params.PerPage
92+
93+
var paginatedItems []T
94+
if startIndex >= total {
95+
paginatedItems = []T{}
96+
} else {
97+
if endIndex > total {
98+
endIndex = total
99+
}
100+
paginatedItems = items[startIndex:endIndex]
101+
}
102+
103+
return ClientSidePaginatedResult[T]{
104+
Items: paginatedItems,
105+
Page: params.Page,
106+
PerPage: params.PerPage,
107+
Total: total,
108+
TotalPages: totalPages,
109+
HasNext: params.Page < totalPages,
110+
HasPrev: params.Page > 1,
111+
}
112+
}

internal/buildkite/buildkite_test.go

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@ func Test_optionalPaginationParams(t *testing.T) {
1919
name: "valid pagination parameters",
2020
args: map[string]any{
2121
"page": float64(1),
22-
"perPage": float64(31),
22+
"perPage": float64(25),
2323
},
2424
expected: buildkite.ListOptions{
2525
Page: 1,
26-
PerPage: 31,
26+
PerPage: 25,
2727
},
2828
expectErr: false,
2929
},
3030
{
3131
name: "missing pagination parameters should use new defaults (1 per page)",
32-
args: map[string]any{},
32+
args: map[string]any{
33+
"name": "test-name",
34+
},
3335
expected: buildkite.ListOptions{
3436
Page: 1,
3537
PerPage: 1,
@@ -54,6 +56,186 @@ func Test_optionalPaginationParams(t *testing.T) {
5456
}
5557
}
5658

59+
func Test_getClientSidePaginationParams(t *testing.T) {
60+
tests := []struct {
61+
name string
62+
args map[string]any
63+
expectedParams ClientSidePaginationParams
64+
}{
65+
{
66+
name: "valid pagination parameters",
67+
args: map[string]any{
68+
"page": float64(2),
69+
"perPage": float64(10),
70+
},
71+
expectedParams: ClientSidePaginationParams{
72+
Page: 2,
73+
PerPage: 10,
74+
},
75+
},
76+
{
77+
name: "only page parameter",
78+
args: map[string]any{
79+
"page": float64(3),
80+
},
81+
expectedParams: ClientSidePaginationParams{
82+
Page: 3,
83+
PerPage: 25, // default
84+
},
85+
},
86+
{
87+
name: "only perPage parameter",
88+
args: map[string]any{
89+
"perPage": float64(50),
90+
},
91+
expectedParams: ClientSidePaginationParams{
92+
Page: 1, // default
93+
PerPage: 50,
94+
},
95+
},
96+
{
97+
name: "no pagination parameters",
98+
args: map[string]any{
99+
"name": "test-name",
100+
},
101+
expectedParams: ClientSidePaginationParams{
102+
Page: 1, // default
103+
PerPage: 25, // default
104+
},
105+
},
106+
}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
assert := require.New(t)
111+
req := createMCPRequest(t, tt.args)
112+
113+
params := getClientSidePaginationParams(req)
114+
assert.Equal(tt.expectedParams, params)
115+
})
116+
}
117+
}
118+
119+
func Test_applyClientSidePagination(t *testing.T) {
120+
tests := []struct {
121+
name string
122+
items []string
123+
params ClientSidePaginationParams
124+
expectedResult ClientSidePaginatedResult[string]
125+
}{
126+
{
127+
name: "first page with items",
128+
items: []string{"item1", "item2", "item3", "item4", "item5"},
129+
params: ClientSidePaginationParams{
130+
Page: 1,
131+
PerPage: 2,
132+
},
133+
expectedResult: ClientSidePaginatedResult[string]{
134+
Items: []string{"item1", "item2"},
135+
Page: 1,
136+
PerPage: 2,
137+
Total: 5,
138+
TotalPages: 3,
139+
HasNext: true,
140+
HasPrev: false,
141+
},
142+
},
143+
{
144+
name: "middle page",
145+
items: []string{"item1", "item2", "item3", "item4", "item5"},
146+
params: ClientSidePaginationParams{
147+
Page: 2,
148+
PerPage: 2,
149+
},
150+
expectedResult: ClientSidePaginatedResult[string]{
151+
Items: []string{"item3", "item4"},
152+
Page: 2,
153+
PerPage: 2,
154+
Total: 5,
155+
TotalPages: 3,
156+
HasNext: true,
157+
HasPrev: true,
158+
},
159+
},
160+
{
161+
name: "last page",
162+
items: []string{"item1", "item2", "item3", "item4", "item5"},
163+
params: ClientSidePaginationParams{
164+
Page: 3,
165+
PerPage: 2,
166+
},
167+
expectedResult: ClientSidePaginatedResult[string]{
168+
Items: []string{"item5"},
169+
Page: 3,
170+
PerPage: 2,
171+
Total: 5,
172+
TotalPages: 3,
173+
HasNext: false,
174+
HasPrev: true,
175+
},
176+
},
177+
{
178+
name: "page beyond available data",
179+
items: []string{"item1", "item2"},
180+
params: ClientSidePaginationParams{
181+
Page: 5,
182+
PerPage: 2,
183+
},
184+
expectedResult: ClientSidePaginatedResult[string]{
185+
Items: []string{},
186+
Page: 5,
187+
PerPage: 2,
188+
Total: 2,
189+
TotalPages: 1,
190+
HasNext: false,
191+
HasPrev: true,
192+
},
193+
},
194+
{
195+
name: "empty items",
196+
items: []string{},
197+
params: ClientSidePaginationParams{
198+
Page: 1,
199+
PerPage: 10,
200+
},
201+
expectedResult: ClientSidePaginatedResult[string]{
202+
Items: []string{},
203+
Page: 1,
204+
PerPage: 10,
205+
Total: 0,
206+
TotalPages: 1,
207+
HasNext: false,
208+
HasPrev: false,
209+
},
210+
},
211+
{
212+
name: "page size larger than total items",
213+
items: []string{"item1", "item2"},
214+
params: ClientSidePaginationParams{
215+
Page: 1,
216+
PerPage: 10,
217+
},
218+
expectedResult: ClientSidePaginatedResult[string]{
219+
Items: []string{"item1", "item2"},
220+
Page: 1,
221+
PerPage: 10,
222+
Total: 2,
223+
TotalPages: 1,
224+
HasNext: false,
225+
HasPrev: false,
226+
},
227+
},
228+
}
229+
230+
for _, tt := range tests {
231+
t.Run(tt.name, func(t *testing.T) {
232+
assert := require.New(t)
233+
result := applyClientSidePagination(tt.items, tt.params)
234+
assert.Equal(tt.expectedResult, result)
235+
})
236+
}
237+
}
238+
57239
func createMCPRequest(t *testing.T, args map[string]any) mcp.CallToolRequest {
58240
t.Helper()
59241
return mcp.CallToolRequest{

0 commit comments

Comments
 (0)