Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/state-mcp/internal/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func New() *Registry {
r.RegisterTool(DownloadSourceFileTool())
r.RegisterTool(GetIngredientDetailsTool())
r.RegisterTool(CreateIngredientRevisionTool())
r.RegisterTool(RebuildProjectTool())

r.RegisterPrompt(ProjectPrompt())
r.RegisterPrompt(IngredientPrompt())
Expand Down
36 changes: 36 additions & 0 deletions cmd/state-mcp/internal/registry/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/ActiveState/cli/internal/runners/mcp/downloadsource"
"github.com/ActiveState/cli/internal/runners/mcp/ingredientdetails"
"github.com/ActiveState/cli/internal/runners/mcp/projecterrors"
"github.com/ActiveState/cli/internal/runners/mcp/rebuildproject"
"github.com/ActiveState/cli/pkg/project"
"github.com/mark3labs/mcp-go/mcp"
)
Expand Down Expand Up @@ -297,3 +298,38 @@ func CreateIngredientRevisionTool() Tool {
},
}
}

func RebuildProjectTool() Tool {
return Tool{
Category: CategoryDebug,
Tool: mcp.NewTool(
"rebuild_project",
mcp.WithDescription("Triggers a project rebuild after all errors have been addressed"),
mcp.WithString("project", mcp.Description("Project namespace in format 'owner/project'"), mcp.Required()),
),
Handler: func(ctx context.Context, p *primer.Values, mcpRequest mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, err := mcpRequest.RequireString("project")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("a project in the format 'owner/project' is required: %s", errs.JoinMessage(err))), nil
}
ns, err := project.ParseNamespace(namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("error parsing project namespace: %s", errs.JoinMessage(err))), nil
}

params := rebuildproject.NewParams()
params.Namespace = ns

runner := rebuildproject.New(p)
err = runner.Run(params)

if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("error rebuilding project: %s", errs.JoinMessage(err))), nil
}

return mcp.NewToolResultText(
strings.Join(p.Output().History().Print, "\n"),
), nil
},
}
}
90 changes: 90 additions & 0 deletions internal/runners/mcp/rebuildproject/rebuildproject.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package rebuildproject

import (
"fmt"

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/internal/primer"
"github.com/ActiveState/cli/pkg/platform/api/buildplanner/types"
"github.com/ActiveState/cli/pkg/platform/authentication"
"github.com/ActiveState/cli/pkg/platform/model"
"github.com/ActiveState/cli/pkg/platform/model/buildplanner"
bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner"
"github.com/ActiveState/cli/pkg/project"
)

type RebuildProjectRunner struct {
auth *authentication.Auth
output output.Outputer
svcModel *model.SvcModel
}

func New(p *primer.Values) *RebuildProjectRunner {
return &RebuildProjectRunner{
auth: p.Auth(),
output: p.Output(),
svcModel: p.SvcModel(),
}
}

type Params struct {
Namespace *project.Namespaced
}

func NewParams() *Params {
return &Params{}
}

func (runner *RebuildProjectRunner) Run(params *Params) error {
branch, err := model.DefaultBranchForProjectName(params.Namespace.Owner, params.Namespace.Project)
if err != nil {
return fmt.Errorf("error fetching default branch: %w", err)
}

// Collect "before" buildscript
bpm := bpModel.NewBuildPlannerModel(runner.auth, runner.svcModel)
localCommit, err := bpm.FetchCommitNoPoll(*branch.CommitID, params.Namespace.Owner, params.Namespace.Project, nil)
if err != nil {
return errs.Wrap(err, "Failed to fetch build result")
}

// Collect "after" buildscript
bumpedBS, err := localCommit.BuildScript().Clone()
if err != nil {
return errs.Wrap(err, "Failed to clone build script")
}

latest, err := model.FetchLatestRevisionTimeStamp(runner.auth)
if err != nil {
return errs.Wrap(err, "Failed to fetch latest timestamp")
}
bumpedBS.SetAtTime(latest, true)

// Since our platform is commit based we need to create a commit for the "after" buildscript
bumpedCommit, err := bpm.StageCommitAndPoll(bpModel.StageCommitParams{
Owner: params.Namespace.Owner,
Project: params.Namespace.Project,
ParentCommit: branch.CommitID.String(),
Script: bumpedBS,
})
if err != nil {
return errs.Wrap(err, "Failed to stage bumped commit")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it does not solve, it errors here.

e.g.:
`Failed to stage bumped commit:
failed to poll build plan:
Request failed: Could not plan build. Platform responded with:
These are the last lines of the error message:
….2.36) requires Ingredient|language/python|fast-agent-mcp (0.2.36) which depends on Feature|language/python|opentelemetry-instrumentation-google-genai (>=0.2b0), Feature|language/python|fast-agent-mcp (0.2.36) requires Feature|language/python|opentelemetry-instrumentation-google-genai (>=0.2b0).

    So, because no versions of Feature|language/python|opentelemetry-instrumentation-google-genai match >=0.2b0
     and root depends on Feature|language/python|fast-agent-mcp (0.2.36), version solving failed.`

}

// Now, merge the new commit using the branch name to fast-forward
_, err = bpm.MergeCommit(&buildplanner.MergeCommitParams{
Owner: params.Namespace.Owner,
Project: params.Namespace.Project,
TargetRef: branch.Label,
OtherRef: bumpedCommit.CommitID.String(),
Strategy: types.MergeCommitStrategyFastForward,
})
if err != nil {
return fmt.Errorf("error merging commit: %w", err)
}

runner.output.Print("Project is now rebuilding with commit ID " + bumpedCommit.CommitID.String())

return nil
}
Loading