Skip to content

Commit b113bc6

Browse files
authored
Merge pull request #270 from buildkite/feat/extend_annotations
extend annotation options
2 parents 7c6873c + 2b310d6 commit b113bc6

3 files changed

Lines changed: 318 additions & 24 deletions

File tree

pkg/buildkite/annotations.go

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,71 @@ import (
44
"context"
55

66
"github.com/buildkite/buildkite-mcp-server/pkg/trace"
7+
"github.com/buildkite/buildkite-mcp-server/pkg/utils"
78
"github.com/buildkite/go-buildkite/v4"
89
"github.com/modelcontextprotocol/go-sdk/mcp"
910
"go.opentelemetry.io/otel/attribute"
1011
)
1112

13+
const (
14+
annotationScopeBuild = "build"
15+
annotationScopeJob = "job"
16+
)
17+
1218
// AnnotationsClient describes the subset of the Buildkite client we need for annotations.
1319
type AnnotationsClient interface {
1420
ListByBuild(ctx context.Context, org, pipelineSlug, buildNumber string, opts *buildkite.AnnotationListOptions) ([]buildkite.Annotation, *buildkite.Response, error)
21+
Create(ctx context.Context, org, pipelineSlug, buildNumber string, ac buildkite.AnnotationCreate) (buildkite.Annotation, *buildkite.Response, error)
22+
ListByJob(ctx context.Context, org, pipelineSlug, buildNumber, jobID string, opts *buildkite.AnnotationListOptions) ([]buildkite.Annotation, *buildkite.Response, error)
23+
CreateForJob(ctx context.Context, org, pipelineSlug, buildNumber, jobID string, ac buildkite.AnnotationCreate) (buildkite.Annotation, *buildkite.Response, error)
1524
}
1625

1726
type ListAnnotationsArgs struct {
1827
OrgSlug string `json:"org_slug"`
1928
PipelineSlug string `json:"pipeline_slug"`
2029
BuildNumber string `json:"build_number"`
30+
Scope string `json:"scope,omitempty" jsonschema:"Annotation scope: build or job (defaults to build)"`
31+
JobID string `json:"job_id,omitempty" jsonschema:"Job ID required when scope is job"`
2132
Page int `json:"page,omitempty" jsonschema:"Page number for pagination (min 1)"`
2233
PerPage int `json:"per_page,omitempty" jsonschema:"Results per page for pagination (min 1\\, max 100)"`
2334
}
2435

25-
// ListAnnotations returns an MCP tool + handler pair that lists annotations for a build.
36+
type CreateAnnotationArgs struct {
37+
OrgSlug string `json:"org_slug"`
38+
PipelineSlug string `json:"pipeline_slug"`
39+
BuildNumber string `json:"build_number"`
40+
Scope string `json:"scope,omitempty" jsonschema:"Annotation scope: build or job (defaults to build)"`
41+
JobID string `json:"job_id,omitempty" jsonschema:"Job ID required when scope is job"`
42+
Body string `json:"body" jsonschema:"The annotation body as HTML or Markdown"`
43+
Style string `json:"style,omitempty" jsonschema:"Optional annotation style: success, info, warning, or error"`
44+
Priority int `json:"priority,omitempty" jsonschema:"Optional annotation priority from 1 to 10"`
45+
Context string `json:"context,omitempty" jsonschema:"Optional annotation context used to identify or append to an annotation"`
46+
Append bool `json:"append,omitempty" jsonschema:"Append the body to an existing annotation with the same context"`
47+
}
48+
49+
func normalizeAnnotationScope(scope, jobID string) (string, string) {
50+
if scope == "" {
51+
scope = annotationScopeBuild
52+
}
53+
54+
switch scope {
55+
case annotationScopeBuild:
56+
return scope, ""
57+
case annotationScopeJob:
58+
if jobID == "" {
59+
return "", "job_id is required when scope is 'job'"
60+
}
61+
return scope, ""
62+
default:
63+
return "", "scope must be 'build' or 'job'"
64+
}
65+
}
66+
67+
// ListAnnotations returns an MCP tool + handler pair that lists annotations for a build or job.
2668
func ListAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListAnnotationsArgs, any], []string) {
2769
return mcp.Tool{
2870
Name: "list_annotations",
29-
Description: "List all annotations for a build, including their context, style (success/info/warning/error), rendered HTML content, and creation timestamps",
71+
Description: "List annotations for a build or a specific job. Use scope='build' (default) or scope='job' with job_id",
3072
Annotations: &mcp.ToolAnnotations{
3173
Title: "List Annotations",
3274
ReadOnlyHint: true,
@@ -35,20 +77,40 @@ func ListAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListAnnotationsArgs, any],
3577
ctx, span := trace.Start(ctx, "buildkite.ListAnnotations")
3678
defer span.End()
3779

80+
scope, validationErr := normalizeAnnotationScope(args.Scope, args.JobID)
81+
if validationErr != "" {
82+
return utils.NewToolResultError(validationErr), nil, nil
83+
}
84+
3885
paginationParams := paginationFromArgs(args.Page, args.PerPage)
3986

4087
span.SetAttributes(
4188
attribute.String("org_slug", args.OrgSlug),
4289
attribute.String("pipeline_slug", args.PipelineSlug),
4390
attribute.String("build_number", args.BuildNumber),
91+
attribute.String("scope", scope),
92+
attribute.String("job_id", args.JobID),
4493
attribute.Int("page", paginationParams.Page),
4594
attribute.Int("per_page", paginationParams.PerPage),
4695
)
4796

4897
deps := DepsFromContext(ctx)
49-
annotations, resp, err := deps.AnnotationsClient.ListByBuild(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, &buildkite.AnnotationListOptions{
50-
ListOptions: paginationParams,
51-
})
98+
99+
var (
100+
annotations []buildkite.Annotation
101+
resp *buildkite.Response
102+
err error
103+
)
104+
105+
if scope == annotationScopeJob {
106+
annotations, resp, err = deps.AnnotationsClient.ListByJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, &buildkite.AnnotationListOptions{
107+
ListOptions: paginationParams,
108+
})
109+
} else {
110+
annotations, resp, err = deps.AnnotationsClient.ListByBuild(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, &buildkite.AnnotationListOptions{
111+
ListOptions: paginationParams,
112+
})
113+
}
52114
if err != nil {
53115
return handleBuildkiteError(err)
54116
}
@@ -67,3 +129,60 @@ func ListAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListAnnotationsArgs, any],
67129
return mcpTextResult(span, &result)
68130
}, []string{"read_builds"}
69131
}
132+
133+
// CreateAnnotation returns an MCP tool + handler pair that creates an annotation on a build or job.
134+
func CreateAnnotation() (mcp.Tool, mcp.ToolHandlerFor[CreateAnnotationArgs, any], []string) {
135+
return mcp.Tool{
136+
Name: "create_annotation",
137+
Description: "Create an annotation on a build or specific job. Use scope='build' (default) or scope='job' with job_id",
138+
Annotations: &mcp.ToolAnnotations{
139+
Title: "Create Annotation",
140+
},
141+
}, func(ctx context.Context, request *mcp.CallToolRequest, args CreateAnnotationArgs) (*mcp.CallToolResult, any, error) {
142+
ctx, span := trace.Start(ctx, "buildkite.CreateAnnotation")
143+
defer span.End()
144+
145+
scope, validationErr := normalizeAnnotationScope(args.Scope, args.JobID)
146+
if validationErr != "" {
147+
return utils.NewToolResultError(validationErr), nil, nil
148+
}
149+
150+
span.SetAttributes(
151+
attribute.String("org_slug", args.OrgSlug),
152+
attribute.String("pipeline_slug", args.PipelineSlug),
153+
attribute.String("build_number", args.BuildNumber),
154+
attribute.String("scope", scope),
155+
attribute.String("job_id", args.JobID),
156+
attribute.String("context", args.Context),
157+
attribute.String("style", args.Style),
158+
attribute.Int("priority", args.Priority),
159+
attribute.Bool("append", args.Append),
160+
)
161+
162+
create := buildkite.AnnotationCreate{
163+
Body: args.Body,
164+
Context: args.Context,
165+
Style: args.Style,
166+
Priority: args.Priority,
167+
Append: args.Append,
168+
}
169+
170+
deps := DepsFromContext(ctx)
171+
172+
var (
173+
annotation buildkite.Annotation
174+
err error
175+
)
176+
177+
if scope == annotationScopeJob {
178+
annotation, _, err = deps.AnnotationsClient.CreateForJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, create)
179+
} else {
180+
annotation, _, err = deps.AnnotationsClient.Create(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, create)
181+
}
182+
if err != nil {
183+
return handleBuildkiteError(err)
184+
}
185+
186+
return mcpTextResult(span, &annotation)
187+
}, []string{"write_builds"}
188+
}

0 commit comments

Comments
 (0)