Skip to content

Commit 97a879f

Browse files
authored
Merge pull request #268 from buildkite/feat_pipeline_schedules
feat: add tools for managing pipeline schedules
2 parents 1256e17 + 18186e5 commit 97a879f

9 files changed

Lines changed: 526 additions & 43 deletions

File tree

.buildkite/pipeline.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ steps:
4848
- "windows"
4949
artifact_paths:
5050
- dist/**/*
51+
secrets:
52+
- MISE_GITHUB_TOKEN
5153
plugins:
5254
- mise#v1.1.1: ~
5355

internal/commands/http.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,21 @@ func (c *HTTPCmd) Run(ctx context.Context, globals *Globals) error {
2727
}
2828

2929
deps := buildkite.ToolDependencies{
30-
BuildsClient: globals.Client.Builds,
31-
PipelinesClient: globals.Client.Pipelines,
32-
ClustersClient: globals.Client.Clusters,
33-
ClusterQueuesClient: globals.Client.ClusterQueues,
34-
ArtifactsClient: &buildkite.BuildkiteClientAdapter{Client: globals.Client},
35-
AnnotationsClient: globals.Client.Annotations,
36-
OrganizationsClient: globals.Client.Organizations,
37-
UserClient: globals.Client.User,
38-
AccessTokensClient: globals.Client.AccessTokens,
39-
JobsClient: globals.Client.Jobs,
40-
TestRunsClient: globals.Client.TestRuns,
41-
TestExecutionsClient: globals.Client.TestRuns,
42-
TestsClient: globals.Client.Tests,
43-
BuildkiteLogsClient: globals.BuildkiteLogsClient,
30+
BuildsClient: globals.Client.Builds,
31+
PipelinesClient: globals.Client.Pipelines,
32+
PipelineSchedulesClient: globals.Client.PipelineSchedules,
33+
ClustersClient: globals.Client.Clusters,
34+
ClusterQueuesClient: globals.Client.ClusterQueues,
35+
ArtifactsClient: &buildkite.BuildkiteClientAdapter{Client: globals.Client},
36+
AnnotationsClient: globals.Client.Annotations,
37+
OrganizationsClient: globals.Client.Organizations,
38+
UserClient: globals.Client.User,
39+
AccessTokensClient: globals.Client.AccessTokens,
40+
JobsClient: globals.Client.Jobs,
41+
TestRunsClient: globals.Client.TestRuns,
42+
TestExecutionsClient: globals.Client.TestRuns,
43+
TestsClient: globals.Client.Tests,
44+
BuildkiteLogsClient: globals.BuildkiteLogsClient,
4445
}
4546

4647
factory := server.NewPerRequestServerFactory(globals.Version, deps, c.EnabledToolsets, c.ReadOnly)

internal/commands/stdio.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@ func (c *StdioCmd) Run(ctx context.Context, globals *Globals) error {
2121
}
2222

2323
deps := buildkite.ToolDependencies{
24-
BuildsClient: globals.Client.Builds,
25-
PipelinesClient: globals.Client.Pipelines,
26-
ClustersClient: globals.Client.Clusters,
27-
ClusterQueuesClient: globals.Client.ClusterQueues,
28-
ArtifactsClient: &buildkite.BuildkiteClientAdapter{Client: globals.Client},
29-
AnnotationsClient: globals.Client.Annotations,
30-
OrganizationsClient: globals.Client.Organizations,
31-
UserClient: globals.Client.User,
32-
AccessTokensClient: globals.Client.AccessTokens,
33-
JobsClient: globals.Client.Jobs,
34-
TestRunsClient: globals.Client.TestRuns,
35-
TestExecutionsClient: globals.Client.TestRuns,
36-
TestsClient: globals.Client.Tests,
37-
BuildkiteLogsClient: globals.BuildkiteLogsClient,
24+
BuildsClient: globals.Client.Builds,
25+
PipelinesClient: globals.Client.Pipelines,
26+
PipelineSchedulesClient: globals.Client.PipelineSchedules,
27+
ClustersClient: globals.Client.Clusters,
28+
ClusterQueuesClient: globals.Client.ClusterQueues,
29+
ArtifactsClient: &buildkite.BuildkiteClientAdapter{Client: globals.Client},
30+
AnnotationsClient: globals.Client.Annotations,
31+
OrganizationsClient: globals.Client.Organizations,
32+
UserClient: globals.Client.User,
33+
AccessTokensClient: globals.Client.AccessTokens,
34+
JobsClient: globals.Client.Jobs,
35+
TestRunsClient: globals.Client.TestRuns,
36+
TestExecutionsClient: globals.Client.TestRuns,
37+
TestsClient: globals.Client.Tests,
38+
BuildkiteLogsClient: globals.BuildkiteLogsClient,
3839
}
3940

4041
log.Info().Msg("Starting MCP server over stdio")

mise.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[settings]
22
experimental = true
33
lockfile = true
4+
github.github_attestations = false
5+
github.slsa = false
46

57
[tools]
68
go = "1.26.2"

pkg/buildkite/dependencies.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@ import (
88

99
// ToolDependencies holds all client interfaces needed by tool handlers.
1010
type ToolDependencies struct {
11-
BuildsClient BuildsClient
12-
PipelinesClient PipelinesClient
13-
ClustersClient ClustersClient
14-
ClusterQueuesClient ClusterQueuesClient
15-
ArtifactsClient ArtifactsClient
16-
AnnotationsClient AnnotationsClient
17-
OrganizationsClient OrganizationsClient
18-
UserClient UserClient
19-
AccessTokensClient AccessTokenClient
20-
JobsClient JobsClient
21-
TestRunsClient TestRunsClient
22-
TestExecutionsClient TestExecutionsClient
23-
TestsClient TestsClient
24-
BuildkiteLogsClient BuildkiteLogsClient
11+
BuildsClient BuildsClient
12+
PipelinesClient PipelinesClient
13+
PipelineSchedulesClient PipelineSchedulesClient
14+
ClustersClient ClustersClient
15+
ClusterQueuesClient ClusterQueuesClient
16+
ArtifactsClient ArtifactsClient
17+
AnnotationsClient AnnotationsClient
18+
OrganizationsClient OrganizationsClient
19+
UserClient UserClient
20+
AccessTokensClient AccessTokenClient
21+
JobsClient JobsClient
22+
TestRunsClient TestRunsClient
23+
TestExecutionsClient TestExecutionsClient
24+
TestsClient TestsClient
25+
BuildkiteLogsClient BuildkiteLogsClient
2526
}
2627

2728
type contextKey struct{}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package buildkite
2+
3+
import (
4+
"context"
5+
6+
"github.com/buildkite/buildkite-mcp-server/pkg/trace"
7+
"github.com/buildkite/go-buildkite/v5"
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
"go.opentelemetry.io/otel/attribute"
10+
)
11+
12+
type PipelineSchedulesClient interface {
13+
List(ctx context.Context, org, pipelineSlug string, opt *buildkite.PipelineScheduleListOptions) ([]buildkite.PipelineSchedule, *buildkite.Response, error)
14+
Get(ctx context.Context, org, pipelineSlug, id string) (buildkite.PipelineSchedule, *buildkite.Response, error)
15+
Create(ctx context.Context, org, pipelineSlug string, in buildkite.CreatePipelineSchedule) (buildkite.PipelineSchedule, *buildkite.Response, error)
16+
Update(ctx context.Context, org, pipelineSlug, id string, in buildkite.UpdatePipelineSchedule) (buildkite.PipelineSchedule, *buildkite.Response, error)
17+
}
18+
19+
type ListPipelineSchedulesArgs struct {
20+
OrgSlug string `json:"org_slug"`
21+
PipelineSlug string `json:"pipeline_slug"`
22+
Page int `json:"page,omitempty" jsonschema:"Page number for pagination (min 1)"`
23+
PerPage int `json:"per_page,omitempty" jsonschema:"Results per page for pagination (min 1\\, max 100)"`
24+
}
25+
26+
func ListPipelineSchedules() (mcp.Tool, mcp.ToolHandlerFor[ListPipelineSchedulesArgs, any], []string) {
27+
return mcp.Tool{
28+
Name: "list_pipeline_schedules",
29+
Description: "List the pipeline schedules for a pipeline, including cron expression, target branch, environment variables, enabled state, and next scheduled build time",
30+
Annotations: &mcp.ToolAnnotations{
31+
Title: "List Pipeline Schedules",
32+
ReadOnlyHint: true,
33+
},
34+
}, func(ctx context.Context, request *mcp.CallToolRequest, args ListPipelineSchedulesArgs) (*mcp.CallToolResult, any, error) {
35+
ctx, span := trace.Start(ctx, "buildkite.ListPipelineSchedules")
36+
defer span.End()
37+
38+
paginationParams := paginationFromArgs(args.Page, args.PerPage)
39+
40+
span.SetAttributes(
41+
attribute.String("org_slug", args.OrgSlug),
42+
attribute.String("pipeline_slug", args.PipelineSlug),
43+
attribute.Int("page", paginationParams.Page),
44+
attribute.Int("per_page", paginationParams.PerPage),
45+
)
46+
47+
deps := DepsFromContext(ctx)
48+
schedules, resp, err := deps.PipelineSchedulesClient.List(ctx, args.OrgSlug, args.PipelineSlug, &buildkite.PipelineScheduleListOptions{
49+
ListOptions: paginationParams,
50+
})
51+
if err != nil {
52+
return handleBuildkiteError(err)
53+
}
54+
55+
result := PaginatedResult[buildkite.PipelineSchedule]{
56+
Items: schedules,
57+
Headers: map[string]string{
58+
"Link": resp.Header.Get("Link"),
59+
},
60+
}
61+
62+
span.SetAttributes(
63+
attribute.Int("item_count", len(schedules)),
64+
)
65+
66+
return mcpTextResult(span, &result)
67+
}, []string{"read_pipelines"}
68+
}
69+
70+
type GetPipelineScheduleArgs struct {
71+
OrgSlug string `json:"org_slug"`
72+
PipelineSlug string `json:"pipeline_slug"`
73+
ScheduleID string `json:"schedule_id"`
74+
}
75+
76+
func GetPipelineSchedule() (mcp.Tool, mcp.ToolHandlerFor[GetPipelineScheduleArgs, any], []string) {
77+
return mcp.Tool{
78+
Name: "get_pipeline_schedule",
79+
Description: "Get detailed information about a single pipeline schedule including its cron expression, target branch, environment variables, enabled state, last failure, and next build time",
80+
Annotations: &mcp.ToolAnnotations{
81+
Title: "Get Pipeline Schedule",
82+
ReadOnlyHint: true,
83+
},
84+
}, func(ctx context.Context, request *mcp.CallToolRequest, args GetPipelineScheduleArgs) (*mcp.CallToolResult, any, error) {
85+
ctx, span := trace.Start(ctx, "buildkite.GetPipelineSchedule")
86+
defer span.End()
87+
88+
span.SetAttributes(
89+
attribute.String("org_slug", args.OrgSlug),
90+
attribute.String("pipeline_slug", args.PipelineSlug),
91+
attribute.String("schedule_id", args.ScheduleID),
92+
)
93+
94+
deps := DepsFromContext(ctx)
95+
schedule, _, err := deps.PipelineSchedulesClient.Get(ctx, args.OrgSlug, args.PipelineSlug, args.ScheduleID)
96+
if err != nil {
97+
return handleBuildkiteError(err)
98+
}
99+
100+
return mcpTextResult(span, &schedule)
101+
}, []string{"read_pipelines"}
102+
}
103+
104+
type CreatePipelineScheduleArgs struct {
105+
OrgSlug string `json:"org_slug"`
106+
PipelineSlug string `json:"pipeline_slug"`
107+
Cronline string `json:"cronline" jsonschema:"Schedule interval as a crontab expression (e.g. '0 0 * * *') or predefined value (e.g. '@daily'\\, '@hourly'\\, '@weekly'\\, '@monthly'\\, '@yearly')"`
108+
Label string `json:"label,omitempty" jsonschema:"Descriptive label for the schedule"`
109+
Message string `json:"message,omitempty" jsonschema:"Message attached to triggered builds"`
110+
Commit string `json:"commit,omitempty" jsonschema:"Commit reference (defaults to HEAD)"`
111+
Branch string `json:"branch,omitempty" jsonschema:"Target branch (defaults to the pipeline default branch)"`
112+
Env map[string]string `json:"env,omitempty" jsonschema:"Environment variables to set on triggered builds"`
113+
Enabled *bool `json:"enabled,omitempty" jsonschema:"Whether the schedule is active. Defaults to true if unset."`
114+
}
115+
116+
func CreatePipelineSchedule() (mcp.Tool, mcp.ToolHandlerFor[CreatePipelineScheduleArgs, any], []string) {
117+
return mcp.Tool{
118+
Name: "create_pipeline_schedule",
119+
Description: "Create a new pipeline schedule that triggers builds on a cron-driven interval",
120+
Annotations: &mcp.ToolAnnotations{
121+
Title: "Create Pipeline Schedule",
122+
},
123+
}, func(ctx context.Context, request *mcp.CallToolRequest, args CreatePipelineScheduleArgs) (*mcp.CallToolResult, any, error) {
124+
ctx, span := trace.Start(ctx, "buildkite.CreatePipelineSchedule")
125+
defer span.End()
126+
127+
span.SetAttributes(
128+
attribute.String("org_slug", args.OrgSlug),
129+
attribute.String("pipeline_slug", args.PipelineSlug),
130+
attribute.String("cronline", args.Cronline),
131+
)
132+
133+
create := buildkite.CreatePipelineSchedule{
134+
Cronline: args.Cronline,
135+
Label: args.Label,
136+
Message: args.Message,
137+
Commit: args.Commit,
138+
Branch: args.Branch,
139+
Env: args.Env,
140+
Enabled: args.Enabled,
141+
}
142+
143+
deps := DepsFromContext(ctx)
144+
schedule, _, err := deps.PipelineSchedulesClient.Create(ctx, args.OrgSlug, args.PipelineSlug, create)
145+
if err != nil {
146+
return handleBuildkiteError(err)
147+
}
148+
149+
return mcpTextResult(span, &schedule)
150+
}, []string{"write_pipelines"}
151+
}
152+
153+
type UpdatePipelineScheduleArgs struct {
154+
OrgSlug string `json:"org_slug"`
155+
PipelineSlug string `json:"pipeline_slug"`
156+
ScheduleID string `json:"schedule_id"`
157+
Cronline *string `json:"cronline,omitempty" jsonschema:"Schedule interval as a crontab expression or predefined value"`
158+
Label *string `json:"label,omitempty"`
159+
Message *string `json:"message,omitempty"`
160+
Commit *string `json:"commit,omitempty"`
161+
Branch *string `json:"branch,omitempty"`
162+
Env map[string]string `json:"env,omitempty" jsonschema:"Environment variables to set on triggered builds. Providing this field REPLACES the existing env map entirely — include all keys you want to retain."`
163+
Enabled *bool `json:"enabled,omitempty" jsonschema:"Whether the schedule is active. Re-enabling clears previous failure data."`
164+
}
165+
166+
func UpdatePipelineSchedule() (mcp.Tool, mcp.ToolHandlerFor[UpdatePipelineScheduleArgs, any], []string) {
167+
return mcp.Tool{
168+
Name: "update_pipeline_schedule",
169+
Description: "Modify an existing pipeline schedule's cron expression, branch, environment variables, or enabled state",
170+
Annotations: &mcp.ToolAnnotations{
171+
Title: "Update Pipeline Schedule",
172+
},
173+
}, func(ctx context.Context, request *mcp.CallToolRequest, args UpdatePipelineScheduleArgs) (*mcp.CallToolResult, any, error) {
174+
ctx, span := trace.Start(ctx, "buildkite.UpdatePipelineSchedule")
175+
defer span.End()
176+
177+
span.SetAttributes(
178+
attribute.String("org_slug", args.OrgSlug),
179+
attribute.String("pipeline_slug", args.PipelineSlug),
180+
attribute.String("schedule_id", args.ScheduleID),
181+
)
182+
183+
update := buildkite.UpdatePipelineSchedule{}
184+
if args.Cronline != nil {
185+
update.Cronline = buildkite.Some(*args.Cronline)
186+
}
187+
if args.Label != nil {
188+
update.Label = buildkite.Some(*args.Label)
189+
}
190+
if args.Message != nil {
191+
update.Message = buildkite.Some(*args.Message)
192+
}
193+
if args.Commit != nil {
194+
update.Commit = buildkite.Some(*args.Commit)
195+
}
196+
if args.Branch != nil {
197+
update.Branch = buildkite.Some(*args.Branch)
198+
}
199+
if args.Env != nil {
200+
update.Env = buildkite.Some(args.Env)
201+
}
202+
if args.Enabled != nil {
203+
update.Enabled = buildkite.Some(*args.Enabled)
204+
}
205+
206+
deps := DepsFromContext(ctx)
207+
schedule, _, err := deps.PipelineSchedulesClient.Update(ctx, args.OrgSlug, args.PipelineSlug, args.ScheduleID, update)
208+
if err != nil {
209+
return handleBuildkiteError(err)
210+
}
211+
212+
return mcpTextResult(span, &schedule)
213+
}, []string{"write_pipelines"}
214+
}

0 commit comments

Comments
 (0)