-
Notifications
You must be signed in to change notification settings - Fork 2
add new mcp tools for cloud build #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
72e51b0
60edcbe
62dd5de
b49e30e
5808353
09b6e1e
4ed3a6a
2ead99b
e188839
b8d5e42
49e2bf6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,11 +20,13 @@ import ( | |||||||||||||||||||||
| "strings" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2" | ||||||||||||||||||||||
| cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| logging "cloud.google.com/go/logging/apiv2" | ||||||||||||||||||||||
| build "google.golang.org/api/cloudbuild/v1" | ||||||||||||||||||||||
| "google.golang.org/api/iterator" | ||||||||||||||||||||||
| "google.golang.org/protobuf/types/known/timestamppb" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" | ||||||||||||||||||||||
| loggingpb "cloud.google.com/go/logging/apiv2/loggingpb" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // contextKey is a private type to use as a key for context values. | ||||||||||||||||||||||
|
|
@@ -51,8 +53,18 @@ type CloudBuildClient interface { | |||||||||||||||||||||
| GetLatestBuildForTrigger(ctx context.Context, projectID, location, triggerID string) (*cloudbuildpb.Build, error) | ||||||||||||||||||||||
| ListBuildTriggers(ctx context.Context, projectID, location string) ([]*cloudbuildpb.BuildTrigger, error) | ||||||||||||||||||||||
| RunBuildTrigger(ctx context.Context, projectID, location, triggerID, branch, tag, commitSha string) (*cloudbuild.RunBuildTriggerOperation, error) | ||||||||||||||||||||||
| ListBuilds(ctx context.Context, projectID, location string) ([]*cloudbuildpb.Build, error) | ||||||||||||||||||||||
| GetBuildInfo(ctx context.Context, projectID, location, buildID string) (BuildInfo, error) | ||||||||||||||||||||||
| StartBuild(ctx context.Context, projectID string, source *cloudbuildpb.Source) (*cloudbuild.CreateBuildOperation, error) | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| type BuildInfo struct { | ||||||||||||||||||||||
| BuildDetails *cloudbuildpb.Build | ||||||||||||||||||||||
| Logs string | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Comment on lines
+66
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||
| // NewCloudBuildClient creates a new Cloud Build client. | ||||||||||||||||||||||
| func NewCloudBuildClient(ctx context.Context) (CloudBuildClient, error) { | ||||||||||||||||||||||
| c, err := cloudbuild.NewClient(ctx) | ||||||||||||||||||||||
|
|
@@ -65,13 +77,23 @@ func NewCloudBuildClient(ctx context.Context) (CloudBuildClient, error) { | |||||||||||||||||||||
| return nil, fmt.Errorf("failed to create Cloud Build service: %v", err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return &CloudBuildClientImpl{v1client: c, legacyClient: c2}, nil | ||||||||||||||||||||||
| loggingClient, err := logging.NewClient(ctx) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return nil, fmt.Errorf("failed to create Logging client: %v", err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return &CloudBuildClientImpl{ | ||||||||||||||||||||||
| v1client: c, | ||||||||||||||||||||||
| legacyClient: c2, | ||||||||||||||||||||||
| loggingClient: loggingClient, | ||||||||||||||||||||||
| }, nil | ||||||||||||||||||||||
|
Comment on lines
+85
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The formatting of this struct literal is inconsistent. Running
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // CloudBuildClientImpl is an implementation of the CloudBuildClient interface. | ||||||||||||||||||||||
| type CloudBuildClientImpl struct { | ||||||||||||||||||||||
| v1client *cloudbuild.Client | ||||||||||||||||||||||
| legacyClient *build.Service | ||||||||||||||||||||||
| loggingClient *logging.Client | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // CreateCloudBuildTrigger creates a new build trigger. | ||||||||||||||||||||||
|
|
@@ -181,3 +203,75 @@ func (c *CloudBuildClientImpl) RunBuildTrigger(ctx context.Context, projectID, l | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| return op, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func (c *CloudBuildClientImpl) ListBuilds(ctx context.Context, projectID, location string) ([]*cloudbuildpb.Build, error) { | ||||||||||||||||||||||
| req := &cloudbuildpb.ListBuildsRequest{ | ||||||||||||||||||||||
| Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location), | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| it := c.v1client.ListBuilds(ctx, req) | ||||||||||||||||||||||
| var builds []*cloudbuildpb.Build | ||||||||||||||||||||||
| for { | ||||||||||||||||||||||
| build, err := it.Next() | ||||||||||||||||||||||
| if err == iterator.Done { | ||||||||||||||||||||||
| break | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return nil, fmt.Errorf("failed to list builds: %v", err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| builds = append(builds, build) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return builds, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func (c *CloudBuildClientImpl) GetBuildInfo(ctx context.Context, projectID, location, buildID string) (BuildInfo, error) { | ||||||||||||||||||||||
| req := &cloudbuildpb.GetBuildRequest{ | ||||||||||||||||||||||
| Name: fmt.Sprintf("projects/%s/locations/%s/builds/%s", projectID, location, buildID), | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| build, err := c.v1client.GetBuild(ctx, req) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return BuildInfo{}, fmt.Errorf("failed to get build info: %w", err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| info := BuildInfo{BuildDetails: build} | ||||||||||||||||||||||
| logReq := &loggingpb.ListLogEntriesRequest{ | ||||||||||||||||||||||
| ResourceNames: []string{fmt.Sprintf("projects/%s", projectID)}, | ||||||||||||||||||||||
| Filter: fmt.Sprintf(`resource.type="build" AND resource.labels.build_id="%s" AND logName="projects/%s/logs/cloudbuild"`, buildID, projectID), | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| it := c.loggingClient.ListLogEntries(ctx, logReq) | ||||||||||||||||||||||
| var logs []string | ||||||||||||||||||||||
| for { | ||||||||||||||||||||||
| entry, err := it.Next() | ||||||||||||||||||||||
| if err == iterator.Done { | ||||||||||||||||||||||
| break | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return BuildInfo{}, fmt.Errorf("failed to list log entries: %w", err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| var logMessage string | ||||||||||||||||||||||
| switch payload := entry.Payload.(type) { | ||||||||||||||||||||||
| case *loggingpb.LogEntry_TextPayload: | ||||||||||||||||||||||
| logMessage = payload.TextPayload | ||||||||||||||||||||||
| case *loggingpb.LogEntry_JsonPayload: | ||||||||||||||||||||||
| logMessage = fmt.Sprintf("%v", payload.JsonPayload) | ||||||||||||||||||||||
|
Comment on lines
+254
to
+255
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using case *loggingpb.LogEntry_JsonPayload:
jsonBytes, err := protojson.Marshal(payload.JsonPayload)
if err != nil {
logMessage = fmt.Sprintf("failed to marshal json payload to string: %v", err)
} else {
logMessage = string(jsonBytes)
} |
||||||||||||||||||||||
| case *loggingpb.LogEntry_ProtoPayload: | ||||||||||||||||||||||
| logMessage = fmt.Sprintf("%v", payload.ProtoPayload) | ||||||||||||||||||||||
| default: | ||||||||||||||||||||||
| return BuildInfo{}, fmt.Errorf("unknown log entry payload type") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| logs = append(logs, logMessage) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| info.Logs = strings.Join(logs, "\n") | ||||||||||||||||||||||
| return info, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func (c *CloudBuildClientImpl) StartBuild(ctx context.Context, projectID string, source *cloudbuildpb.Source) (*cloudbuild.CreateBuildOperation, error) { | ||||||||||||||||||||||
| req := &cloudbuildpb.CreateBuildRequest{ | ||||||||||||||||||||||
| Parent: fmt.Sprintf("projects/%s/locations/global", projectID), | ||||||||||||||||||||||
| Build: &cloudbuildpb.Build{Source: source}, | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+267
to
+271
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation of func (c *CloudBuildClientImpl) StartBuild(ctx context.Context, projectID, location string, source *cloudbuildpb.Source) (*cloudbuild.CreateBuildOperation, error) {
req := &cloudbuildpb.CreateBuildRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", projectID, location),
Build: &cloudbuildpb.Build{Source: source},
} |
||||||||||||||||||||||
| ops, err := c.v1client.CreateBuild(ctx, req) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| return nil, fmt.Errorf("failed to start build: %v", err) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return ops, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -25,6 +25,8 @@ import ( | |||||
| cloudbuildclient "devops-mcp-server/cloudbuild/client" | ||||||
| iamclient "devops-mcp-server/iam/client" | ||||||
| resourcemanagerclient "devops-mcp-server/resourcemanager/client" | ||||||
|
|
||||||
| cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" | ||||||
| ) | ||||||
|
|
||||||
| // Handler holds the clients for the cloudbuild service. | ||||||
|
|
@@ -39,6 +41,9 @@ func (h *Handler) Register(server *mcp.Server) { | |||||
| addCreateTriggerTool(server, h.CbClient, h.IClient, h.RClient) | ||||||
| addRunTriggerTool(server, h.CbClient) | ||||||
| addListTriggersTool(server, h.CbClient) | ||||||
| addListBuildsTool(server, h.CbClient) | ||||||
| addGetBuildInfoTool(server, h.CbClient) | ||||||
| addStartBuildTool(server, h.CbClient) | ||||||
| } | ||||||
|
|
||||||
| type RunTriggerArgs struct { | ||||||
|
|
@@ -146,3 +151,62 @@ func IsValidServiceAccount(sa string) bool { | |||||
| var saRegex = regexp.MustCompile(`^serviceAccount:[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com$`) | ||||||
| return saRegex.MatchString(sa) | ||||||
| } | ||||||
|
|
||||||
| type ListBuildsArgs struct { | ||||||
| ProjectID string `json:"project_id" jsonschema:"The Google Cloud project ID."` | ||||||
| Location string `json:"location" jsonschema:"The Google Cloud location for the builds."` | ||||||
| } | ||||||
|
|
||||||
| type GetBuildInfoArgs struct { | ||||||
| ProjectID string `json:"project_id" jsonschema:"The Google Cloud project ID."` | ||||||
| Location string `json:"location" jsonschema:"The Google Cloud location for the build."` | ||||||
| BuildID string `json:"build_id" jsonschema:"The ID of the build."` | ||||||
| } | ||||||
|
|
||||||
| type StartBuildArgs struct { | ||||||
| ProjectID string `json:"project_id" jsonschema:"The Google Cloud project ID."` | ||||||
| Location string `json:"location" jsonschema:"The Google Cloud location for the build."` | ||||||
| Bucket string `json:"bucket" jsonschema:"The Cloud Storage bucket where the source is located."` | ||||||
| Object string `json:"object" jsonschema:"The Cloud Storage object (file) where the source is located."` | ||||||
| } | ||||||
|
|
||||||
| func addListBuildsTool(server *mcp.Server, cbClient cloudbuildclient.CloudBuildClient) { | ||||||
| listBuildsToolFunc := func(ctx context.Context, req *mcp.CallToolRequest, args ListBuildsArgs) (*mcp.CallToolResult, any, error) { | ||||||
| res, err := cbClient.ListBuilds(ctx, args.ProjectID, args.Location) | ||||||
| if err != nil { | ||||||
| return &mcp.CallToolResult{}, nil, fmt.Errorf("failed to list builds: %w", err) | ||||||
| } | ||||||
| return &mcp.CallToolResult{}, map[string]any{"builds": res}, nil | ||||||
| } | ||||||
| mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.list_builds", Description: "Lists all Cloud Builds in a given location and project."}, listBuildsToolFunc) | ||||||
| } | ||||||
|
|
||||||
| func addGetBuildInfoTool(server *mcp.Server, cbClient cloudbuildclient.CloudBuildClient) { | ||||||
| getBuildInfoToolFunc := func(ctx context.Context, req *mcp.CallToolRequest, args GetBuildInfoArgs) (*mcp.CallToolResult, any, error) { | ||||||
| res, err := cbClient.GetBuildInfo(ctx, args.ProjectID, args.Location, args.BuildID) | ||||||
| if err != nil { | ||||||
| return &mcp.CallToolResult{}, nil, fmt.Errorf("failed to get build info: %w", err) | ||||||
| } | ||||||
| return &mcp.CallToolResult{}, res, nil | ||||||
| } | ||||||
| mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.get_build_info", Description: "Gets information about a specific Cloud Build."}, getBuildInfoToolFunc) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tool name
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| func addStartBuildTool(server *mcp.Server, cbClient cloudbuildclient.CloudBuildClient) { | ||||||
| startBuildToolFunc := func(ctx context.Context, req *mcp.CallToolRequest, args StartBuildArgs) (*mcp.CallToolResult, any, error) { | ||||||
| source:= &cloudbuildpb.Source{ | ||||||
| Source: &cloudbuildpb.Source_StorageSource{ | ||||||
| StorageSource: &cloudbuildpb.StorageSource{ | ||||||
| Bucket: args.Bucket, | ||||||
| Object: args.Object, | ||||||
| }, | ||||||
| }, | ||||||
| } | ||||||
| res, err := cbClient.StartBuild(ctx, args.ProjectID, source) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| if err != nil { | ||||||
| return &mcp.CallToolResult{}, nil, fmt.Errorf("failed to start build: %w", err) | ||||||
| } | ||||||
| return &mcp.CallToolResult{}, res, nil | ||||||
| } | ||||||
| mcp.AddTool(server, &mcp.Tool{Name: "cloudbuild.start_build", Description: "Starts a new Cloud Build with the given source locally."}, startBuildToolFunc) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tool name
Suggested change
|
||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.