Skip to content

Commit eb7f519

Browse files
committed
extend the core toolset for annotations
1 parent e908d39 commit eb7f519

1 file changed

Lines changed: 243 additions & 2 deletions

File tree

pkg/buildkite/annotations.go

Lines changed: 243 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import (
1212
// AnnotationsClient describes the subset of the Buildkite client we need for annotations.
1313
type AnnotationsClient interface {
1414
ListByBuild(ctx context.Context, org, pipelineSlug, buildNumber string, opts *buildkite.AnnotationListOptions) ([]buildkite.Annotation, *buildkite.Response, error)
15+
Create(ctx context.Context, org, pipelineSlug, buildNumber string, ac buildkite.AnnotationCreate) (buildkite.Annotation, *buildkite.Response, error)
16+
Delete(ctx context.Context, org, pipelineSlug, buildNumber, annotationID string) (*buildkite.Response, error)
17+
ListByJob(ctx context.Context, org, pipelineSlug, buildNumber, jobID string, opts *buildkite.AnnotationListOptions) ([]buildkite.Annotation, *buildkite.Response, error)
18+
CreateForJob(ctx context.Context, org, pipelineSlug, buildNumber, jobID string, ac buildkite.AnnotationCreate) (buildkite.Annotation, *buildkite.Response, error)
19+
DeleteForJob(ctx context.Context, org, pipelineSlug, buildNumber, jobID, annotationID string) (*buildkite.Response, error)
1520
}
1621

1722
type ListAnnotationsArgs struct {
@@ -22,13 +27,60 @@ type ListAnnotationsArgs struct {
2227
PerPage int `json:"per_page,omitempty" jsonschema:"Results per page for pagination (min 1\\, max 100)"`
2328
}
2429

30+
type CreateAnnotationArgs struct {
31+
OrgSlug string `json:"org_slug"`
32+
PipelineSlug string `json:"pipeline_slug"`
33+
BuildNumber string `json:"build_number"`
34+
Body string `json:"body" jsonschema:"The annotation body as HTML or Markdown"`
35+
Style string `json:"style,omitempty" jsonschema:"Optional annotation style: success\, info\, warning\, or error"`
36+
Priority int `json:"priority,omitempty" jsonschema:"Optional annotation priority from 1 to 10"`
37+
Context string `json:"context,omitempty" jsonschema:"Optional annotation context used to identify or append to an annotation"`
38+
Append bool `json:"append,omitempty" jsonschema:"Append the body to an existing annotation with the same context"`
39+
}
40+
41+
type DeleteAnnotationArgs struct {
42+
OrgSlug string `json:"org_slug"`
43+
PipelineSlug string `json:"pipeline_slug"`
44+
BuildNumber string `json:"build_number"`
45+
AnnotationID string `json:"annotation_id"`
46+
}
47+
48+
type ListJobAnnotationsArgs struct {
49+
OrgSlug string `json:"org_slug"`
50+
PipelineSlug string `json:"pipeline_slug"`
51+
BuildNumber string `json:"build_number"`
52+
JobID string `json:"job_id"`
53+
Page int `json:"page,omitempty" jsonschema:"Page number for pagination (min 1)"`
54+
PerPage int `json:"per_page,omitempty" jsonschema:"Results per page for pagination (min 1\\, max 100)"`
55+
}
56+
57+
type CreateJobAnnotationArgs struct {
58+
OrgSlug string `json:"org_slug"`
59+
PipelineSlug string `json:"pipeline_slug"`
60+
BuildNumber string `json:"build_number"`
61+
JobID string `json:"job_id"`
62+
Body string `json:"body" jsonschema:"The annotation body as HTML or Markdown"`
63+
Style string `json:"style,omitempty" jsonschema:"Optional annotation style: success\, info\, warning\, or error"`
64+
Priority int `json:"priority,omitempty" jsonschema:"Optional annotation priority from 1 to 10"`
65+
Context string `json:"context,omitempty" jsonschema:"Optional annotation context used to identify or append to an annotation"`
66+
Append bool `json:"append,omitempty" jsonschema:"Append the body to an existing annotation with the same context"`
67+
}
68+
69+
type DeleteJobAnnotationArgs struct {
70+
OrgSlug string `json:"org_slug"`
71+
PipelineSlug string `json:"pipeline_slug"`
72+
BuildNumber string `json:"build_number"`
73+
JobID string `json:"job_id"`
74+
AnnotationID string `json:"annotation_id"`
75+
}
76+
2577
// ListAnnotations returns an MCP tool + handler pair that lists annotations for a build.
2678
func ListAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListAnnotationsArgs, any], []string) {
2779
return mcp.Tool{
2880
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",
81+
Description: "List all annotations for a build, including their context, scope, style, rendered HTML content, and timestamps",
3082
Annotations: &mcp.ToolAnnotations{
31-
Title: "List Annotations",
83+
Title: "List Build Annotations",
3284
ReadOnlyHint: true,
3385
},
3486
}, func(ctx context.Context, request *mcp.CallToolRequest, args ListAnnotationsArgs) (*mcp.CallToolResult, any, error) {
@@ -67,3 +119,192 @@ func ListAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListAnnotationsArgs, any],
67119
return mcpTextResult(span, &result)
68120
}, []string{"read_builds"}
69121
}
122+
123+
func CreateAnnotation() (mcp.Tool, mcp.ToolHandlerFor[CreateAnnotationArgs, any], []string) {
124+
return mcp.Tool{
125+
Name: "create_annotation",
126+
Description: "Create a build-level annotation on a Buildkite build using HTML or Markdown content",
127+
Annotations: &mcp.ToolAnnotations{
128+
Title: "Create Build Annotation",
129+
},
130+
}, func(ctx context.Context, request *mcp.CallToolRequest, args CreateAnnotationArgs) (*mcp.CallToolResult, any, error) {
131+
ctx, span := trace.Start(ctx, "buildkite.CreateAnnotation")
132+
defer span.End()
133+
134+
span.SetAttributes(
135+
attribute.String("org_slug", args.OrgSlug),
136+
attribute.String("pipeline_slug", args.PipelineSlug),
137+
attribute.String("build_number", args.BuildNumber),
138+
attribute.String("context", args.Context),
139+
attribute.String("style", args.Style),
140+
attribute.Int("priority", args.Priority),
141+
attribute.Bool("append", args.Append),
142+
)
143+
144+
deps := DepsFromContext(ctx)
145+
annotation, _, err := deps.AnnotationsClient.Create(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, buildkite.AnnotationCreate{
146+
Body: args.Body,
147+
Context: args.Context,
148+
Style: args.Style,
149+
Priority: args.Priority,
150+
Append: args.Append,
151+
})
152+
if err != nil {
153+
return handleBuildkiteError(err)
154+
}
155+
156+
return mcpTextResult(span, &annotation)
157+
}, []string{"write_builds"}
158+
}
159+
160+
func DeleteAnnotation() (mcp.Tool, mcp.ToolHandlerFor[DeleteAnnotationArgs, any], []string) {
161+
return mcp.Tool{
162+
Name: "delete_annotation",
163+
Description: "Delete a build-level annotation from a Buildkite build",
164+
Annotations: &mcp.ToolAnnotations{
165+
Title: "Delete Build Annotation",
166+
DestructiveHint: boolPtr(true),
167+
},
168+
}, func(ctx context.Context, request *mcp.CallToolRequest, args DeleteAnnotationArgs) (*mcp.CallToolResult, any, error) {
169+
ctx, span := trace.Start(ctx, "buildkite.DeleteAnnotation")
170+
defer span.End()
171+
172+
span.SetAttributes(
173+
attribute.String("org_slug", args.OrgSlug),
174+
attribute.String("pipeline_slug", args.PipelineSlug),
175+
attribute.String("build_number", args.BuildNumber),
176+
attribute.String("annotation_id", args.AnnotationID),
177+
)
178+
179+
deps := DepsFromContext(ctx)
180+
_, err := deps.AnnotationsClient.Delete(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.AnnotationID)
181+
if err != nil {
182+
return handleBuildkiteError(err)
183+
}
184+
185+
return mcpTextResult(span, map[string]any{
186+
"deleted": true,
187+
"scope": "build",
188+
"annotation_id": args.AnnotationID,
189+
})
190+
}, []string{"write_builds"}
191+
}
192+
193+
func ListJobAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListJobAnnotationsArgs, any], []string) {
194+
return mcp.Tool{
195+
Name: "list_job_annotations",
196+
Description: "List all annotations for a specific job, including their context, scope, style, rendered HTML content, and timestamps",
197+
Annotations: &mcp.ToolAnnotations{
198+
Title: "List Job Annotations",
199+
ReadOnlyHint: true,
200+
},
201+
}, func(ctx context.Context, request *mcp.CallToolRequest, args ListJobAnnotationsArgs) (*mcp.CallToolResult, any, error) {
202+
ctx, span := trace.Start(ctx, "buildkite.ListJobAnnotations")
203+
defer span.End()
204+
205+
paginationParams := paginationFromArgs(args.Page, args.PerPage)
206+
207+
span.SetAttributes(
208+
attribute.String("org_slug", args.OrgSlug),
209+
attribute.String("pipeline_slug", args.PipelineSlug),
210+
attribute.String("build_number", args.BuildNumber),
211+
attribute.String("job_id", args.JobID),
212+
attribute.Int("page", paginationParams.Page),
213+
attribute.Int("per_page", paginationParams.PerPage),
214+
)
215+
216+
deps := DepsFromContext(ctx)
217+
annotations, resp, err := deps.AnnotationsClient.ListByJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, &buildkite.AnnotationListOptions{
218+
ListOptions: paginationParams,
219+
})
220+
if err != nil {
221+
return handleBuildkiteError(err)
222+
}
223+
224+
result := PaginatedResult[buildkite.Annotation]{
225+
Items: annotations,
226+
Headers: map[string]string{
227+
"Link": resp.Header.Get("Link"),
228+
},
229+
}
230+
231+
span.SetAttributes(
232+
attribute.Int("item_count", len(annotations)),
233+
)
234+
235+
return mcpTextResult(span, &result)
236+
}, []string{"read_builds"}
237+
}
238+
239+
func CreateJobAnnotation() (mcp.Tool, mcp.ToolHandlerFor[CreateJobAnnotationArgs, any], []string) {
240+
return mcp.Tool{
241+
Name: "create_job_annotation",
242+
Description: "Create a job-level annotation on a specific Buildkite job using HTML or Markdown content",
243+
Annotations: &mcp.ToolAnnotations{
244+
Title: "Create Job Annotation",
245+
},
246+
}, func(ctx context.Context, request *mcp.CallToolRequest, args CreateJobAnnotationArgs) (*mcp.CallToolResult, any, error) {
247+
ctx, span := trace.Start(ctx, "buildkite.CreateJobAnnotation")
248+
defer span.End()
249+
250+
span.SetAttributes(
251+
attribute.String("org_slug", args.OrgSlug),
252+
attribute.String("pipeline_slug", args.PipelineSlug),
253+
attribute.String("build_number", args.BuildNumber),
254+
attribute.String("job_id", args.JobID),
255+
attribute.String("context", args.Context),
256+
attribute.String("style", args.Style),
257+
attribute.Int("priority", args.Priority),
258+
attribute.Bool("append", args.Append),
259+
)
260+
261+
deps := DepsFromContext(ctx)
262+
annotation, _, err := deps.AnnotationsClient.CreateForJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, buildkite.AnnotationCreate{
263+
Body: args.Body,
264+
Context: args.Context,
265+
Style: args.Style,
266+
Priority: args.Priority,
267+
Append: args.Append,
268+
})
269+
if err != nil {
270+
return handleBuildkiteError(err)
271+
}
272+
273+
return mcpTextResult(span, &annotation)
274+
}, []string{"write_builds"}
275+
}
276+
277+
func DeleteJobAnnotation() (mcp.Tool, mcp.ToolHandlerFor[DeleteJobAnnotationArgs, any], []string) {
278+
return mcp.Tool{
279+
Name: "delete_job_annotation",
280+
Description: "Delete a job-level annotation from a specific job in a Buildkite build",
281+
Annotations: &mcp.ToolAnnotations{
282+
Title: "Delete Job Annotation",
283+
DestructiveHint: boolPtr(true),
284+
},
285+
}, func(ctx context.Context, request *mcp.CallToolRequest, args DeleteJobAnnotationArgs) (*mcp.CallToolResult, any, error) {
286+
ctx, span := trace.Start(ctx, "buildkite.DeleteJobAnnotation")
287+
defer span.End()
288+
289+
span.SetAttributes(
290+
attribute.String("org_slug", args.OrgSlug),
291+
attribute.String("pipeline_slug", args.PipelineSlug),
292+
attribute.String("build_number", args.BuildNumber),
293+
attribute.String("job_id", args.JobID),
294+
attribute.String("annotation_id", args.AnnotationID),
295+
)
296+
297+
deps := DepsFromContext(ctx)
298+
_, err := deps.AnnotationsClient.DeleteForJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, args.AnnotationID)
299+
if err != nil {
300+
return handleBuildkiteError(err)
301+
}
302+
303+
return mcpTextResult(span, map[string]any{
304+
"deleted": true,
305+
"scope": "job",
306+
"job_id": args.JobID,
307+
"annotation_id": args.AnnotationID,
308+
})
309+
}, []string{"write_builds"}
310+
}

0 commit comments

Comments
 (0)