@@ -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.
1319type 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
1726type 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.
2668func 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