-
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
Changes from all commits
72e51b0
60edcbe
62dd5de
b49e30e
5808353
09b6e1e
4ed3a6a
2ead99b
e188839
b8d5e42
49e2bf6
76dbe7b
ba4497a
71e2c9b
ffc1bbc
d512f2c
e1745aa
b4877c9
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,14 @@ 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/encoding/protojson" | ||||||||||||||
| "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,6 +54,14 @@ 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, location string, source *cloudbuildpb.Source) (*cloudbuild.CreateBuildOperation, error) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| type BuildInfo struct { | ||||||||||||||
| BuildDetails *cloudbuildpb.Build | ||||||||||||||
| Logs string | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // NewCloudBuildClient creates a new Cloud Build client. | ||||||||||||||
|
|
@@ -65,13 +76,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 | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // 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 +202,80 @@ 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: %w", 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: | ||||||||||||||
| 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") | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+262
to
+264
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. Returning an error here for an unknown log entry payload type will cause the entire log retrieval to fail. It would be more resilient to append a warning message to the logs and continue processing other log entries. This prevents a single malformed or unexpected log entry from breaking the entire functionality.
Suggested change
|
||||||||||||||
| logs = append(logs, logMessage) | ||||||||||||||
| } | ||||||||||||||
| info.Logs = strings.Join(logs, "\n") | ||||||||||||||
| return info, nil | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| 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: %w", 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, args.Location, source) | ||||||
| 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 from a source in Google Cloud Storage."}, startBuildToolFunc) | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.