Skip to content

feat: CP-1037 New MCP tool to create revision from dependency list #3706

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

Merged
merged 4 commits into from
Aug 15, 2025
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 @@ -37,6 +37,7 @@ func New() *Registry {
r.RegisterTool(ListSourceFilesTool())
r.RegisterTool(DownloadSourceFileTool())
r.RegisterTool(GetIngredientDetailsTool())
r.RegisterTool(CreateIngredientRevisionTool())

r.RegisterPrompt(ProjectPrompt())
r.RegisterPrompt(IngredientPrompt())
Expand Down
54 changes: 54 additions & 0 deletions cmd/state-mcp/internal/registry/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/primer"
"github.com/ActiveState/cli/internal/runners/hello"
"github.com/ActiveState/cli/internal/runners/mcp/createrevision"
"github.com/ActiveState/cli/internal/runners/mcp/downloadlogs"
"github.com/ActiveState/cli/internal/runners/mcp/downloadsource"
"github.com/ActiveState/cli/internal/runners/mcp/ingredientdetails"
Expand Down Expand Up @@ -243,3 +244,56 @@ func GetInstructionsTool() Tool {
},
}
}

func CreateIngredientRevisionTool() Tool {
return Tool{
Category: CategoryDebug,
Tool: mcp.NewTool(
"create_ingredient_revision",
mcp.WithDescription("Creates a new revision for the specified ingredient version"),
mcp.WithString("namespace", mcp.Description("The namespace of the ingredient, e.g. language/python"), mcp.Required()),
mcp.WithString("name", mcp.Description("The name of the ingredient, e.g. numpy"), mcp.Required()),
mcp.WithString("version", mcp.Description("The version of the ingredient, e.g. 0.1.0"), mcp.Required()),
mcp.WithString("dependencies", mcp.Description(`The JSON representation of dependencies, e.g.
[ { "conditions": [ { "feature": "alternative-built-language", "namespace": "language", "requirements": [{"comparator": "eq", "sortable_version": []}] } ], "description": "Camel build dependency", "feature": "camel", "namespace": "builder", "requirements": [{"comparator": "gte", "sortable_version": ["0"], "version": "0"}], "type": "build" }, { "conditions": null, "description": "Extracted from source distribution in PyPI.", "feature": "cython", "namespace": "language/python", "original_requirement": "Cython <3.0,>=0.29.24", "requirements": [ {"comparator": "gte", "sortable_version": ["0","0","29","24"], "version": "0.29.24"}, {"comparator": "lt", "sortable_version": ["0","3"], "version": "3.0"} ], "type": "build" }, { "conditions": null, "description": "Extracted from source distribution in PyPI.", "feature": "setuptools", "namespace": "language/python", "original_requirement": "setuptools ==59.2.0", "requirements": [{"comparator": "eq", "sortable_version": ["0","59","2"], "version": "59.2.0"}], "type": "runtime" } ]
`), mcp.Required()),
mcp.WithString("comment", mcp.Description("A short summary of the changes you made, and why you made them - including the file that declares an added or updated dependency, e.g. updated dependencies and python version, as per pyproject.toml"), mcp.Required()),
),
Handler: func(ctx context.Context, p *primer.Values, mcpRequest mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, err := mcpRequest.RequireString("namespace")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("an ingredient namespace str is required: %s", errs.JoinMessage(err))), nil
}
name, err := mcpRequest.RequireString("name")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("an ingredient name str is required: %s", errs.JoinMessage(err))), nil
}
version, err := mcpRequest.RequireString("version")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("an ingredient version str is required: %s", errs.JoinMessage(err))), nil
}
dependencies, err := mcpRequest.RequireString("dependencies")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("the ingredient dependencies str is required: %s", errs.JoinMessage(err))), nil
}
comment, err := mcpRequest.RequireString("comment")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("a comment str for the ingredient is required: %s", errs.JoinMessage(err))), nil
}

params := createrevision.NewParams(namespace, name, version, dependencies, comment)

runner := createrevision.New(p)

err = runner.Run(params)

if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("error creating new ingredient version revision: %s", errs.JoinMessage(err))), nil
}

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

import (
"encoding/json"

"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/inventory/inventory_models"
"github.com/ActiveState/cli/pkg/platform/authentication"
"github.com/ActiveState/cli/pkg/platform/model"
)

type CreateRevisionRunner struct {
auth *authentication.Auth
output output.Outputer
}

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

type Params struct {
namespace string
name string
version string
dependencies string
comment string
}

func NewParams(namespace string, name string, version string, dependencies string, comment string) *Params {
return &Params{
namespace: namespace,
name: name,
version: version,
dependencies: dependencies,
comment: comment,
}
}

func (runner *CreateRevisionRunner) Run(params *Params) error {
// Unmarshal JSON with the new dependency info
var dependencies []inventory_models.Dependency
err := json.Unmarshal([]byte(params.dependencies), &dependencies)
if err != nil {
return errs.Wrap(err, "error unmarshaling dependencies, dependency JSON is in wrong format")
}

// Retrieve ingredient version to access ingredient and version IDs
ingredient, err := model.GetIngredientByNameAndVersion(params.namespace, params.name, params.version, nil, runner.auth)
if err != nil {
return errs.Wrap(err, "error fetching ingredient")
}

newRevision, err := model.CreateNewIngredientVersionRevision(ingredient, params.comment, dependencies, runner.auth)
if err != nil {
return errs.Wrap(err, "error creating ingredient version revision")
}

marshalledRevision, err := json.Marshal(newRevision)
if err != nil {
return errs.Wrap(err, "error marshalling revision response")
}
runner.output.Print(string(marshalledRevision))

return nil
}
96 changes: 96 additions & 0 deletions pkg/platform/model/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,99 @@ func FilterCurrentPlatform(hostPlatform string, platforms []strfmt.UUID, preferr

return platformIDs[0], nil
}

// CreateNewIngredientVersionRevision creates a new revision for the provided ingredient.
// The new revision only updates the comment and dependencies of the ingredient version, all further fields are kept as-is.
func CreateNewIngredientVersionRevision(ingredient *inventory_models.FullIngredientVersion, comment string, dependencies []inventory_models.Dependency, auth *authentication.Auth) (*inventory_operations.AddIngredientVersionRevisionOK, error) {
// Retrieve its latest revision to copy all data - but comment and dependencies - from
getParams := inventory_operations.NewGetIngredientVersionRevisionsParams()
getParams.SetIngredientID(*ingredient.IngredientID)
getParams.SetIngredientVersionID(*ingredient.IngredientVersionID)

client := inventory.Get(auth)
revisions, err := client.GetIngredientVersionRevisions(getParams, auth.ClientAuth())
if err != nil {
return nil, errs.Wrap(err, "error getting version revisions")
}
revision := revisions.Payload.IngredientVersionRevisions[len(revisions.Payload.IngredientVersionRevisions)-1]

// Prepare new ingredient version revision params to create a new revision
// This leaves all the attributes untouched, but dependencies and comments
newParams := inventory_operations.NewAddIngredientVersionRevisionParams()
newParams.SetIngredientID(*ingredient.IngredientID)
newParams.SetIngredientVersionID(*ingredient.IngredientVersionID)

// Extract build script IDs
var buildScriptIDs []strfmt.UUID
for _, script := range revision.BuildScripts {
buildScriptIDs = append(buildScriptIDs, *script.BuildScriptID)
}

// Replicate patches
var patches []*inventory_models.IngredientVersionRevisionCreatePatch
for _, patch := range revision.Patches {
patches = append(patches, &inventory_models.IngredientVersionRevisionCreatePatch{
PatchID: patch.PatchID,
SequenceNumber: patch.SequenceNumber,
})
}

// Retrieve and prepare default and override option sets
optsetParams := inventory_operations.NewGetIngredientVersionIngredientOptionSetsParams()
optsetParams.SetIngredientID(*ingredient.IngredientID)
optsetParams.SetIngredientVersionID(*ingredient.IngredientVersionID)

response, err := client.GetIngredientVersionIngredientOptionSets(optsetParams, auth.ClientAuth())
if err != nil {
return nil, errs.Wrap(err, "error getting optsets")
}

var default_optsets []strfmt.UUID
var override_optsets []strfmt.UUID
for _, optset := range response.Payload.IngredientOptionSetsWithUsageType {
switch *optset.UsageType {
case "default":
default_optsets = append(default_optsets, *optset.IngredientOptionSetID)
case "override":
override_optsets = append(override_optsets, *optset.IngredientOptionSetID)
Copy link
Member

Choose a reason for hiding this comment

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

Suggest adding a default handler that errors out (eg. unrecognized type). Even if there are only 2, that should future proof us somewhat in the sense that if this change in the future it'll be immediately apparent that our code needs updating.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

default:
return nil, errs.New("unrecognized option set type: " + *optset.UsageType)
}
}

// Create and set the new revision object from model, setting the reason as manual change
manual_change := inventory_models.IngredientVersionRevisionCoreReasonManualChange
new_revision := inventory_models.IngredientVersionRevisionCreate{
IngredientVersionRevisionCore: inventory_models.IngredientVersionRevisionCore{
Comment: &comment,
ProvidedFeatures: revision.ProvidedFeatures,
Reason: &manual_change,
ActivestateLicenseExpression: revision.ActivestateLicenseExpression,
AuthorPlatformUserID: revision.AuthorPlatformUserID,
CamelExtras: revision.CamelExtras,
Dependencies: dependencies,
IsIndemnified: revision.IsIndemnified,
IsStableRelease: revision.IsStableRelease,
IsStableRevision: revision.IsStableRevision,
LicenseManifestURI: revision.LicenseManifestURI,
PlatformSourceURI: revision.PlatformSourceURI,
ScannerLicenseExpression: revision.ScannerLicenseExpression,
SourceChecksum: revision.SourceChecksum,
Status: revision.Status,
},
IngredientVersionRevisionCreateAllOf0: inventory_models.IngredientVersionRevisionCreateAllOf0{
BuildScripts: buildScriptIDs,
DefaultIngredientOptionSets: default_optsets,
IngredientOptionSetOverrides: override_optsets,
Patches: patches,
},
}
newParams.SetIngredientVersionRevision(&new_revision)

// Create the new revision and output its marshalled string
newRevision, err := client.AddIngredientVersionRevision(newParams, auth.ClientAuth())
if err != nil {
return nil, errs.Wrap(err, "error creating revision")
}
return newRevision, nil
}
Loading