Skip to content

Commit 2b310d6

Browse files
committed
address comments; remove destructive
1 parent 0a0a4b0 commit 2b310d6

3 files changed

Lines changed: 139 additions & 325 deletions

File tree

pkg/buildkite/annotations.go

Lines changed: 70 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,31 @@ 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)
1521
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)
1722
ListByJob(ctx context.Context, org, pipelineSlug, buildNumber, jobID string, opts *buildkite.AnnotationListOptions) ([]buildkite.Annotation, *buildkite.Response, error)
1823
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)
2024
}
2125

2226
type ListAnnotationsArgs struct {
2327
OrgSlug string `json:"org_slug"`
2428
PipelineSlug string `json:"pipeline_slug"`
2529
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"`
2632
Page int `json:"page,omitempty" jsonschema:"Page number for pagination (min 1)"`
2733
PerPage int `json:"per_page,omitempty" jsonschema:"Results per page for pagination (min 1\\, max 100)"`
2834
}
@@ -31,76 +37,80 @@ type CreateAnnotationArgs struct {
3137
OrgSlug string `json:"org_slug"`
3238
PipelineSlug string `json:"pipeline_slug"`
3339
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"`
3442
Body string `json:"body" jsonschema:"The annotation body as HTML or Markdown"`
3543
Style string `json:"style,omitempty" jsonschema:"Optional annotation style: success, info, warning, or error"`
3644
Priority int `json:"priority,omitempty" jsonschema:"Optional annotation priority from 1 to 10"`
3745
Context string `json:"context,omitempty" jsonschema:"Optional annotation context used to identify or append to an annotation"`
3846
Append bool `json:"append,omitempty" jsonschema:"Append the body to an existing annotation with the same context"`
3947
}
4048

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)"`
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+
}
5565
}
5666

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-
77-
// ListAnnotations returns an MCP tool + handler pair that lists annotations for a build.
67+
// ListAnnotations returns an MCP tool + handler pair that lists annotations for a build or job.
7868
func ListAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListAnnotationsArgs, any], []string) {
7969
return mcp.Tool{
8070
Name: "list_annotations",
81-
Description: "List all annotations for a build, including their context, scope, style, rendered HTML content, and timestamps",
71+
Description: "List annotations for a build or a specific job. Use scope='build' (default) or scope='job' with job_id",
8272
Annotations: &mcp.ToolAnnotations{
83-
Title: "List Build Annotations",
73+
Title: "List Annotations",
8474
ReadOnlyHint: true,
8575
},
8676
}, func(ctx context.Context, request *mcp.CallToolRequest, args ListAnnotationsArgs) (*mcp.CallToolResult, any, error) {
8777
ctx, span := trace.Start(ctx, "buildkite.ListAnnotations")
8878
defer span.End()
8979

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

9287
span.SetAttributes(
9388
attribute.String("org_slug", args.OrgSlug),
9489
attribute.String("pipeline_slug", args.PipelineSlug),
9590
attribute.String("build_number", args.BuildNumber),
91+
attribute.String("scope", scope),
92+
attribute.String("job_id", args.JobID),
9693
attribute.Int("page", paginationParams.Page),
9794
attribute.Int("per_page", paginationParams.PerPage),
9895
)
9996

10097
deps := DepsFromContext(ctx)
101-
annotations, resp, err := deps.AnnotationsClient.ListByBuild(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, &buildkite.AnnotationListOptions{
102-
ListOptions: paginationParams,
103-
})
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+
}
104114
if err != nil {
105115
return handleBuildkiteError(err)
106116
}
@@ -120,191 +130,59 @@ func ListAnnotations() (mcp.Tool, mcp.ToolHandlerFor[ListAnnotationsArgs, any],
120130
}, []string{"read_builds"}
121131
}
122132

133+
// CreateAnnotation returns an MCP tool + handler pair that creates an annotation on a build or job.
123134
func CreateAnnotation() (mcp.Tool, mcp.ToolHandlerFor[CreateAnnotationArgs, any], []string) {
124135
return mcp.Tool{
125136
Name: "create_annotation",
126-
Description: "Create a build-level annotation on a Buildkite build using HTML or Markdown content",
137+
Description: "Create an annotation on a build or specific job. Use scope='build' (default) or scope='job' with job_id",
127138
Annotations: &mcp.ToolAnnotations{
128-
Title: "Create Build Annotation",
139+
Title: "Create Annotation",
129140
},
130141
}, func(ctx context.Context, request *mcp.CallToolRequest, args CreateAnnotationArgs) (*mcp.CallToolResult, any, error) {
131142
ctx, span := trace.Start(ctx, "buildkite.CreateAnnotation")
132143
defer span.End()
133144

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)
145+
scope, validationErr := normalizeAnnotationScope(args.Scope, args.JobID)
146+
if validationErr != "" {
147+
return utils.NewToolResultError(validationErr), nil, nil
154148
}
155149

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-
250150
span.SetAttributes(
251151
attribute.String("org_slug", args.OrgSlug),
252152
attribute.String("pipeline_slug", args.PipelineSlug),
253153
attribute.String("build_number", args.BuildNumber),
154+
attribute.String("scope", scope),
254155
attribute.String("job_id", args.JobID),
255156
attribute.String("context", args.Context),
256157
attribute.String("style", args.Style),
257158
attribute.Int("priority", args.Priority),
258159
attribute.Bool("append", args.Append),
259160
)
260161

261-
deps := DepsFromContext(ctx)
262-
annotation, _, err := deps.AnnotationsClient.CreateForJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, buildkite.AnnotationCreate{
162+
create := buildkite.AnnotationCreate{
263163
Body: args.Body,
264164
Context: args.Context,
265165
Style: args.Style,
266166
Priority: args.Priority,
267167
Append: args.Append,
268-
})
269-
if err != nil {
270-
return handleBuildkiteError(err)
271168
}
272169

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()
170+
deps := DepsFromContext(ctx)
288171

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),
172+
var (
173+
annotation buildkite.Annotation
174+
err error
295175
)
296176

297-
deps := DepsFromContext(ctx)
298-
_, err := deps.AnnotationsClient.DeleteForJob(ctx, args.OrgSlug, args.PipelineSlug, args.BuildNumber, args.JobID, args.AnnotationID)
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+
}
299182
if err != nil {
300183
return handleBuildkiteError(err)
301184
}
302185

303-
return mcpTextResult(span, map[string]any{
304-
"deleted": true,
305-
"scope": "job",
306-
"job_id": args.JobID,
307-
"annotation_id": args.AnnotationID,
308-
})
186+
return mcpTextResult(span, &annotation)
309187
}, []string{"write_builds"}
310188
}

0 commit comments

Comments
 (0)