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
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.14 h1:n2rIJRwwa9cQXFFDVl9QIGif1IeXfoGw8nvYS6WKcPM=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.14/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.15 h1:ZJjirglHljaZqrHu8/3HeNK7RkMBHL0ZJTfPtgYlQlg=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.15/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.16 h1:e87B3FRtJy0//tao5HI5Fit2vN2RlXeLRy67wo8wOmg=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.16/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.17 h1:Afv/OJWzTwPPD+1r3yErDI/mn3BuKePjFFMrGUXC1fk=
github.com/massdriver-cloud/massdriver-sdk-go v0.2.17/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY=
github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74=
Expand Down
4 changes: 2 additions & 2 deletions mcp/tools/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type AddComponentInput struct {
BundleName string `json:"bundle_name" jsonschema:"Name of the bundle to add (e.g., 'aws-aurora-postgres')."`
ID string `json:"id" jsonschema:"Short identifier for this component, max 20 lowercase alphanumeric characters. Immutable after creation."`
Name string `json:"name" jsonschema:"Display name for this component (e.g., 'Billing Database')."`
Description string `json:"description,omitempty" jsonschema:"Optional description of this component's purpose."`
Description string `json:"description,omitempty" jsonschema:"Optional description of this component's purpose. Max 255 characters."`
Attributes map[string]any `json:"attributes,omitempty" jsonschema:"Optional. Custom attribute tags at the component scope. Must conform to the organization's custom-attribute schema; some may be required."`
}

Expand Down Expand Up @@ -124,7 +124,7 @@ var UpdateComponentTool = &mcpsdk.Tool{
type UpdateComponentInput struct {
ID string `json:"id" jsonschema:"The component ID to update."`
Name *string `json:"name,omitempty" jsonschema:"Optional. New display name. Omit to leave unchanged; cannot be set to an empty string."`
Description *string `json:"description,omitempty" jsonschema:"Optional. New description. Omit to leave unchanged; pass an empty string to clear it."`
Description *string `json:"description,omitempty" jsonschema:"Optional. New description, max 255 characters. Omit to leave unchanged; pass an empty string to clear it."`
Attributes map[string]any `json:"attributes,omitempty" jsonschema:"Optional. Replacement custom attribute tags at the component scope. Omit to leave unchanged; when provided, replaces the full attribute set. Must conform to the organization's custom-attribute schema."`
}

Expand Down
77 changes: 60 additions & 17 deletions mcp/tools/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/massdriver-cloud/massdriver-sdk-go/massdriver/platform/deployments"
"github.com/massdriver-cloud/massdriver-sdk-go/massdriver/platform/instances"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
)

Expand Down Expand Up @@ -81,14 +82,18 @@ func HandleGetDeployment(c *Client) func(context.Context, *mcpsdk.CallToolReques
var GetDeploymentLogsTool = &mcpsdk.Tool{
Name: "get_deployment_logs",
Description: "Gets the logs for a specific deployment. By default returns a snapshot of the logs so far. " +
"Set follow=true to block until the deployment reaches a terminal status (COMPLETED, FAILED, ABORTED, or REJECTED), then return the final status plus the complete logs — " +
"use this after create_deployment or approve_deployment to deploy and see the result in a single call.",
"Output is capped to the most recent 40KB — a leading note reports when older content was elided; use `tail_lines` to adjust (-1 for everything). " +
"Set follow=true to block until the deployment reaches a terminal status (COMPLETED, FAILED, ABORTED, or REJECTED), then return the final status plus the logs — " +
"use this after create_deployment or approve_deployment to deploy and see the result in a single call. " +
"If follow times out before the deployment finishes, the response says so; call again with follow=true to keep waiting — " +
"combine with a small tail_lines on those repeat calls to avoid re-reading output you already have.",
}

type GetDeploymentLogsInput struct {
ID string `json:"id" jsonschema:"The deployment ID to fetch logs for."`
Follow bool `json:"follow,omitempty" jsonschema:"Optional. If true, wait until the deployment finishes and return the final status plus complete logs. Default false (snapshot of logs so far)."`
Follow bool `json:"follow,omitempty" jsonschema:"Optional. If true, wait until the deployment finishes and return the final status plus logs. Default false (snapshot of logs so far)."`
TimeoutSeconds int `json:"timeout_seconds,omitempty" jsonschema:"Optional. When follow is true, maximum seconds to wait (default 300, max 600)."`
TailLines int `json:"tail_lines,omitempty" jsonschema:"Optional. Return only the last N log lines. Omit for the default cap (last 40KB); pass -1 for the complete log."`
}

func HandleGetDeploymentLogs(c *Client) func(context.Context, *mcpsdk.CallToolRequest, GetDeploymentLogsInput) (*mcpsdk.CallToolResult, any, error) {
Expand All @@ -106,7 +111,7 @@ func HandleGetDeploymentLogs(c *Client) func(context.Context, *mcpsdk.CallToolRe
if logs == "" {
return textResult("no logs available"), nil, nil
}
return textResult(logs), nil, nil
return textResult(tailLogs(logs, args.TailLines)), nil, nil
}

// Follow mode: tail until the deployment terminates (or we time out),
Expand Down Expand Up @@ -151,9 +156,10 @@ func HandleGetDeploymentLogs(c *Client) func(context.Context, *mcpsdk.CallToolRe
if isTerminalDeploymentStatus(status) {
header = fmt.Sprintf("deployment %s finished with status: %s\n\n", args.ID, status)
} else {
header = fmt.Sprintf("deployment %s did not finish within %ds (current status: %s)\n\n", args.ID, timeout, status)
header = fmt.Sprintf("deployment %s did not finish within %ds (current status: %s). "+
"Call get_deployment_logs again with follow=true to continue waiting.\n\n", args.ID, timeout, status)
}
logs := buf.String()
logs := tailLogs(buf.String(), args.TailLines)
if logs == "" {
logs = "(no logs)"
}
Expand All @@ -164,16 +170,18 @@ func HandleGetDeploymentLogs(c *Client) func(context.Context, *mcpsdk.CallToolRe
var CreateDeploymentTool = &mcpsdk.Tool{
Name: "create_deployment",
Description: "Creates and starts a deployment for an instance. Use action PROVISION to deploy, DECOMMISSION to tear down, or PLAN to preview changes. " +
"`params` is REQUIRED for every action (including DECOMMISSION) and is the full parameter set, validated against the instance's params schema (see get_instance.paramsSchema) — it is not a partial override, and there is no 'use saved params' mode. " +
"To redeploy an instance's current configuration, read get_instance.params and pass it back here. To tear down a whole environment without assembling params, use decommission_environment instead. " +
"Every action (including DECOMMISSION) needs the full parameter set, validated against the instance's params schema (see get_instance.paramsSchema) — `params` is not a partial override. " +
"Either pass `params` explicitly, or set `use_latest_params` to true to reuse the instance's current saved configuration (the common case for DECOMMISSION, PLAN of the current config, or redeploy-as-is). " +
"To tear down a whole environment, use decommission_environment instead. To PLAN an existing deployment's params, plan_deployment is more direct. " +
"Use get_deployment_logs with follow=true to block until it finishes and see the result.",
}

type CreateDeploymentInput struct {
InstanceID string `json:"instance_id" jsonschema:"The instance ID to deploy."`
Action string `json:"action" jsonschema:"Deployment action: PROVISION, DECOMMISSION, or PLAN."`
Params map[string]any `json:"params,omitempty" jsonschema:"The full bundle parameter set for this deployment, validated against the instance params schema (get_instance.paramsSchema). Required for every action; omitting it sends an empty map and fails schema validation. Read get_instance.params to reuse current config."`
Message string `json:"message,omitempty" jsonschema:"Optional. Deployment message or reason."`
InstanceID string `json:"instance_id" jsonschema:"The instance ID to deploy."`
Action string `json:"action" jsonschema:"Deployment action: PROVISION, DECOMMISSION, or PLAN."`
Params map[string]any `json:"params,omitempty" jsonschema:"The full bundle parameter set for this deployment, validated against the instance params schema (get_instance.paramsSchema). Required unless use_latest_params is true; omitting both sends an empty map and fails schema validation."`
UseLatestParams bool `json:"use_latest_params,omitempty" jsonschema:"Optional. When true, reuse the instance's saved params — the values from its most recent deployment, even if that deployment failed — instead of passing params. Mutually exclusive with params. Fails if the instance has never been deployed."`
Message string `json:"message,omitempty" jsonschema:"Optional. Deployment message or reason."`
}

func HandleCreateDeployment(c *Client) func(context.Context, *mcpsdk.CallToolRequest, CreateDeploymentInput) (*mcpsdk.CallToolResult, any, error) {
Expand All @@ -189,6 +197,12 @@ func HandleCreateDeployment(c *Client) func(context.Context, *mcpsdk.CallToolReq
// empty map so callers get a clear "required property" validation error
// rather than a cryptic GraphQL "Expected type Map!, found null".
params := args.Params
if args.UseLatestParams {
var err error
if params, err = latestInstanceParams(ctx, c, "create_deployment", args.InstanceID, args.Params); err != nil {
return nil, nil, err
}
}
if params == nil {
params = map[string]any{}
}
Expand Down Expand Up @@ -247,14 +261,16 @@ func HandleAbortDeployment(c *Client) func(context.Context, *mcpsdk.CallToolRequ
var ProposeDeploymentTool = &mcpsdk.Tool{
Name: "propose_deployment",
Description: "Proposes a deployment for approval. Only supports PROVISION and DECOMMISSION actions. The deployment enters PROPOSED status and must be approved or rejected. " +
"`params` is REQUIRED for both actions and is the full parameter set, validated against the instance's params schema (see get_instance.paramsSchema) — not a partial override. To reuse current configuration, read get_instance.params and pass it back.",
"Both actions need the full parameter set, validated against the instance's params schema (see get_instance.paramsSchema) — `params` is not a partial override. " +
"Either pass `params` explicitly, or set `use_latest_params` to true to reuse the instance's current saved configuration.",
}

type ProposeDeploymentInput struct {
InstanceID string `json:"instance_id" jsonschema:"The instance ID to deploy."`
Action string `json:"action" jsonschema:"Deployment action: PROVISION or DECOMMISSION."`
Params map[string]any `json:"params,omitempty" jsonschema:"The full bundle parameter set for this deployment, validated against the instance params schema (get_instance.paramsSchema). Required for every action; omitting it sends an empty map and fails schema validation. Read get_instance.params to reuse current config."`
Message string `json:"message,omitempty" jsonschema:"Optional. Deployment message or reason."`
InstanceID string `json:"instance_id" jsonschema:"The instance ID to deploy."`
Action string `json:"action" jsonschema:"Deployment action: PROVISION or DECOMMISSION."`
Params map[string]any `json:"params,omitempty" jsonschema:"The full bundle parameter set for this deployment, validated against the instance params schema (get_instance.paramsSchema). Required unless use_latest_params is true; omitting both sends an empty map and fails schema validation."`
UseLatestParams bool `json:"use_latest_params,omitempty" jsonschema:"Optional. When true, reuse the instance's saved params — the values from its most recent deployment, even if that deployment failed — instead of passing params. Mutually exclusive with params. Fails if the instance has never been deployed."`
Message string `json:"message,omitempty" jsonschema:"Optional. Deployment message or reason."`
}

func HandleProposeDeployment(c *Client) func(context.Context, *mcpsdk.CallToolRequest, ProposeDeploymentInput) (*mcpsdk.CallToolResult, any, error) {
Expand All @@ -268,6 +284,12 @@ func HandleProposeDeployment(c *Client) func(context.Context, *mcpsdk.CallToolRe

// See HandleCreateDeployment: the API requires a non-null params map.
params := args.Params
if args.UseLatestParams {
var err error
if params, err = latestInstanceParams(ctx, c, "propose_deployment", args.InstanceID, args.Params); err != nil {
return nil, nil, err
}
}
if params == nil {
params = map[string]any{}
}
Expand Down Expand Up @@ -461,6 +483,27 @@ func HandleCompareDeployments(c *Client) func(context.Context, *mcpsdk.CallToolR
}
}

// latestInstanceParams resolves use_latest_params for create_deployment and
// propose_deployment: it fetches the instance and returns its saved params
// (the values from its most recent deployment). Reuse must be explicit and
// unambiguous, so passing params alongside the flag is an error, as is an
// instance that has never been deployed. Never-deployed is judged by the
// instance's lifecycle status, not by whether Params is empty — an empty map
// is the legitimate saved configuration of a bundle with no params.
func latestInstanceParams(ctx context.Context, c *Client, toolName, instanceID string, explicit map[string]any) (map[string]any, error) {
if explicit != nil {
return nil, fmt.Errorf("%s: params and use_latest_params are mutually exclusive — pass one or the other", toolName)
}
instance, err := c.Instances.Get(ctx, instanceID)
if err != nil {
return nil, fmt.Errorf("%s: fetching instance for use_latest_params: %w", toolName, err)
}
if instances.Status(instance.Status) == instances.StatusInitialized {
return nil, fmt.Errorf("%s: use_latest_params requires an instance that has been deployed before, but instance %s has never been deployed — pass params explicitly", toolName, instanceID)
}
return instance.Params, nil
}

// isTerminalDeploymentStatus reports whether a deployment is done and its
// status will not change further.
func isTerminalDeploymentStatus(s string) bool {
Expand Down
Loading
Loading