Skip to content
2 changes: 1 addition & 1 deletion pkgs/defang/cli.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildGoModule {
pname = "defang-cli";
version = "git";
src = ../../src;
vendorHash = "sha256-0M0jtBaPE1jSx4nrOR4XMw1Im1tMYTKYCkcKiZ1bj8M="; # TODO: use fetchFromGitHub
vendorHash = "sha256-8caEfevVUKXsYA5YyVDuPZTqMnYXIZnC4kTTGfPmuqA="; # TODO: use fetchFromGitHub

subPackages = [ "cmd/cli" ];

Expand Down
11 changes: 9 additions & 2 deletions src/cmd/cli/command/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/login"
"github.com/DefangLabs/defang/src/pkg/mcp"
"github.com/DefangLabs/defang/src/pkg/mcp/prompts"
"github.com/DefangLabs/defang/src/pkg/mcp/resources"
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
"github.com/DefangLabs/defang/src/pkg/term"
Expand Down Expand Up @@ -70,21 +71,27 @@ set_config - This tool sets or updates configuration variables for a deployed ap
`),
)

cluster := getCluster()

// Setup resources
term.Debug("Setting up resources")
resources.SetupResources(s)

//setup prompts
term.Debug("Setting up prompts")
prompts.SetupPrompts(s, cluster, &providerID)

// Setup tools
term.Debug("Setting up tools")
tools.SetupTools(s, getCluster(), authPort, providerID)
tools.SetupTools(s, cluster, authPort, &providerID)

// Start auth server for docker login flow
if authPort != 0 {
term.Debug("Starting Auth Server for MCP-in-Docker login flow")
term.Debug("Function invoked: cli.InteractiveLoginInsideDocker")

go func() {
if err := login.InteractiveLoginInsideDocker(cmd.Context(), getCluster(), authPort); err != nil {
if err := login.InteractiveLoginInsideDocker(cmd.Context(), cluster, authPort); err != nil {
term.Error("Failed to start auth server", "error", err)
}
}()
Expand Down
7 changes: 6 additions & 1 deletion src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/hexops/gotextdiff v1.0.3
github.com/joho/godotenv v1.5.1
github.com/mark3labs/mcp-go v0.29.0
github.com/mark3labs/mcp-go v0.38.0
github.com/miekg/dns v1.1.59
github.com/moby/patternmatcher v0.6.0
github.com/muesli/termenv v0.15.2
Expand Down Expand Up @@ -77,6 +77,8 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
Expand All @@ -93,7 +95,9 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
Expand All @@ -105,6 +109,7 @@ require (
github.com/spf13/cast v1.7.1 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/zeebo/errs v1.4.0 // indirect
Expand Down
15 changes: 13 additions & 2 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,12 @@ github.com/awslabs/goformation/v7 v7.13.1 h1:QlPn8qwNCqYhrb4GW8kLjT4j1J49n5Qh/an
github.com/awslabs/goformation/v7 v7.13.1/go.mod h1:FTCFMNesubEX0LAd6kIR+YkDD1U+5UaMbXtgPUgsck0=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down Expand Up @@ -213,10 +217,13 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
Expand All @@ -232,8 +239,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mark3labs/mcp-go v0.29.0 h1:sH1NBcumKskhxqYzhXfGc201D7P76TVXiT0fGVhabeI=
github.com/mark3labs/mcp-go v0.29.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.38.0 h1:E5tmJiIXkhwlV0pLAwAT0O5ZjUZSISE/2Jxg+6vpq4I=
github.com/mark3labs/mcp-go v0.38.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
Expand Down Expand Up @@ -304,6 +313,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
Expand Down
129 changes: 129 additions & 0 deletions src/pkg/mcp/prompts/awsBYOC.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package prompts

import (
"context"
"errors"
"os"

"github.com/DefangLabs/defang/src/pkg/cli"
"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func setupAWSBYOPrompt(s *server.MCPServer, cluster string, providerId *client.ProviderID) {
awsBYOCPrompt := mcp.NewPrompt("AWS Setup",
mcp.WithPromptDescription("Setup for AWS"),

mcp.WithArgument("AWS_Credential",
mcp.ArgumentDescription("Your AWS Access Key ID or AWS Profile Name"),
mcp.RequiredArgument(),
),

mcp.WithArgument("AWS_SECRET_ACCESS_KEY",
mcp.ArgumentDescription("Your AWS Secret Access Key"),
),

mcp.WithArgument("AWS_REGION",
mcp.ArgumentDescription("Your AWS Region"),
),
)

s.AddPrompt(awsBYOCPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
// Can never be nil or empty due to RequiredArgument
awsID := req.Params.Arguments["AWS_Credential"]
if isValidAWSKey(awsID) {
err := os.Setenv("AWS_ACCESS_KEY_ID", awsID)
if err != nil {
return nil, err
}

awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "")
region := getStringArg(req.Params.Arguments, "AWS_REGION", "")

if awsSecret == "" {
return nil, errors.New("AWS_SECRET_ACCESS_KEY is required")
}

err = os.Setenv("AWS_SECRET_ACCESS_KEY", awsSecret)
if err != nil {
return nil, err
}

if region == "" {
return nil, errors.New("AWS_REGION is required")
}

err = os.Setenv("AWS_REGION", region)
if err != nil {
return nil, err
}
} else {
err := os.Setenv("AWS_PROFILE", awsID)
if err != nil {
return nil, err
}
}

fabric, err := cli.Connect(ctx, cluster)
if err != nil {
return nil, err
}

_, err = tools.CheckProviderConfigured(ctx, fabric, client.ProviderAWS, "", 0)
if err != nil {
return nil, err
}

*providerId = client.ProviderAWS

//FIXME: Should not be setting both the global and env var
err = os.Setenv("DEFANG_PROVIDER", "aws")
if err != nil {
return nil, err
}

return &mcp.GetPromptResult{
Description: "AWS BYOC Setup Complete",
Messages: []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.NewTextContent(postPrompt),
},
},
}, nil
})
}

// Check if the provided AWS access key ID is valid
// https://medium.com/@TalBeerySec/a-short-note-on-aws-key-id-f88cc4317489
func isValidAWSKey(key string) bool {
// Define accepted AWS access key prefixes
acceptedPrefixes := map[string]bool{
"ABIA": true,
"ACCA": true,
"AGPA": true,
"AIDA": true,
"AKPA": true,
"AKIA": true,
"ANPA": true,
"ANVA": true,
"APKA": true,
"AROA": true,
"ASCA": true,
"ASIA": true,
}

if len(key) < 16 {
return false
}

prefix := key[:4]
_, ok := acceptedPrefixes[prefix]
if !ok {
return false
}

return true
}
10 changes: 10 additions & 0 deletions src/pkg/mcp/prompts/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package prompts

const postPrompt = "Can you deploy my application now."

func getStringArg(args map[string]string, key, defaultValue string) string {
if val, exists := args[key]; exists {
return val
}
return defaultValue
}
61 changes: 61 additions & 0 deletions src/pkg/mcp/prompts/gcpBYOC.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package prompts

import (
"context"
"os"

"github.com/DefangLabs/defang/src/pkg/cli"
"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func setupGCPBYOPrompt(s *server.MCPServer, cluster string, providerId *client.ProviderID) {
gcpBYOPrompt := mcp.NewPrompt("GCP Setup",
mcp.WithPromptDescription("Setup for GCP"),

mcp.WithArgument("GCP_PROJECT_ID",
mcp.ArgumentDescription("Your GCP Project ID"),
mcp.RequiredArgument(),
),
)

s.AddPrompt(gcpBYOPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
// Can never be nil or empty due to RequiredArgument
projectID := req.Params.Arguments["GCP_PROJECT_ID"]

err := os.Setenv("GCP_PROJECT_ID", projectID)
if err != nil {
return nil, err
}

fabric, err := cli.Connect(ctx, cluster)
if err != nil {
return nil, err
}

_, err = tools.CheckProviderConfigured(ctx, fabric, client.ProviderGCP, "", 0)
if err != nil {
return nil, err
}

*providerId = client.ProviderGCP

//FIXME: Should not be setting both the global var and env var
err = os.Setenv("DEFANG_PROVIDER", "gcp")
if err != nil {
return nil, err
}

return &mcp.GetPromptResult{
Description: "GCP BYOC Setup Complete",
Messages: []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.NewTextContent(postPrompt),
},
},
}, nil
})
}
36 changes: 36 additions & 0 deletions src/pkg/mcp/prompts/playgroundSetup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package prompts

import (
"context"
"os"

"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func setupPlaygroundPrompt(s *server.MCPServer, providerId *client.ProviderID) {
playgroundPrompt := mcp.NewPrompt("Playground Setup",
mcp.WithPromptDescription("Setup for Playground"),
)

s.AddPrompt(playgroundPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
*providerId = client.ProviderDefang

//FIXME: Should not be setting both the global and env var
err := os.Setenv("DEFANG_PROVIDER", "defang")
if err != nil {
return nil, err
}

return &mcp.GetPromptResult{
Description: "Defang playground Setup Complete",
Messages: []mcp.PromptMessage{
{
Role: mcp.RoleUser,
Content: mcp.NewTextContent(postPrompt),
},
},
}, nil
})
}
18 changes: 18 additions & 0 deletions src/pkg/mcp/prompts/prompts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package prompts

import (
"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/mark3labs/mcp-go/server"
)

// SetupPrompts configures and adds all prompts to the MCP server
func SetupPrompts(s *server.MCPServer, cluster string, providerId *client.ProviderID) {
//AWS BYOC
setupAWSBYOPrompt(s, cluster, providerId)

//GCP BYOC
setupGCPBYOPrompt(s, cluster, providerId)

//Playground
setupPlaygroundPrompt(s, providerId)
}
Loading
Loading