@@ -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.
1319type 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
2226type 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.
7868func 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.
123134func 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