From 027ee787782024e6465cb03b4de61826a179e93b Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Thu, 21 Aug 2025 17:04:44 -0700 Subject: [PATCH 01/13] spike on byoc mcp --- src/go.mod | 7 ++- src/go.sum | 13 ++++++ src/pkg/mcp/resources/resources.go | 70 ++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/go.mod b/src/go.mod index 8c6254ffb..af15d61fa 100644 --- a/src/go.mod +++ b/src/go.mod @@ -41,7 +41,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hexops/gotextdiff v1.0.3 - 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 @@ -76,6 +76,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 @@ -92,7 +94,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 @@ -104,6 +108,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 diff --git a/src/go.sum b/src/go.sum index 2fd69d59f..27ee4c26d 100644 --- a/src/go.sum +++ b/src/go.sum @@ -106,8 +106,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= @@ -209,10 +213,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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -226,8 +233,12 @@ 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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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/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= @@ -298,6 +309,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= diff --git a/src/pkg/mcp/resources/resources.go b/src/pkg/mcp/resources/resources.go index 348acac77..88406a765 100644 --- a/src/pkg/mcp/resources/resources.go +++ b/src/pkg/mcp/resources/resources.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/term" @@ -86,6 +87,73 @@ func setupSamplesResource(s *server.MCPServer) { }) } +var BYOCPrompt = mcp.NewPrompt("BYOC Setup", + mcp.WithPromptDescription("Bring Your Own Cloud setup for AWS, GCP, or DO"), + mcp.WithArgument("cloud", + mcp.ArgumentDescription("Supported Cloud providers: AWS, GCP, or DO"), + mcp.RequiredArgument(), + ), + mcp.WithArgument("AWS_ACCESS_KEY_ID", + mcp.ArgumentDescription("Your AWS Access Key ID"), + mcp.RequiredArgument(), + ), + + mcp.WithArgument("AWS_SECRET_ACCESS_KEY", + mcp.ArgumentDescription("Your AWS Secret Access Key"), + mcp.RequiredArgument(), + ), +) + +func handleBYOCPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + cloud := getStringArg(req.Params.Arguments, "cloud", "aws") + awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") + awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") + var prompt strings.Builder + + // Adjust complexity based on user level + switch cloud { + case "aws", "AWS": + prompt.WriteString(fmt.Sprintf("Please explain %s in simple terms suitable for someone new to the topic. ", awsID)) + prompt.WriteString("Use clear language and avoid jargon. ") + case "gcp", "GCP": + prompt.WriteString(fmt.Sprintf("Please provide a detailed explanation of %s. ", awsID)) + prompt.WriteString("Include technical details but ensure clarity. ") + case "do", "DO": + prompt.WriteString(fmt.Sprintf("Please provide an in-depth analysis of %s. ", awsID)) + prompt.WriteString("Include advanced concepts, edge cases, and technical nuances. ") + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("awsid: %s, awsSecret: %s, cloud: %s", awsID, awsSecret, cloud), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(prompt.String()), + }, + }, + }, nil +} + +func getStringArg(args map[string]string, key, defaultValue string) string { + if val, exists := args[key]; exists { + return val + } + return defaultValue +} + +// func getBoolArg(args map[string]string, key string, defaultValue bool) bool { +// if val, exists := args[key]; exists { +// // Accept "true"/"false" strings +// if val == "true" { +// return true +// } +// if val == "false" { +// return false +// } +// } +// return defaultValue +// } + // setupSamplePrompt configures and adds the sample prompt to the MCP server func setupSamplePrompt(s *server.MCPServer) { samplePrompt := mcp.NewPrompt("Make Dockerfile and compose file", @@ -96,6 +164,8 @@ func setupSamplePrompt(s *server.MCPServer) { ), ) + s.AddPrompt(BYOCPrompt, handleBYOCPrompt) + s.AddPrompt(samplePrompt, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { projectPath, ok := request.Params.Arguments["project_path"] if !ok || projectPath == "" { From 8a65c04f7565d6e94f8faf7117988d675ca31859 Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Tue, 26 Aug 2025 09:30:22 -0700 Subject: [PATCH 02/13] set defang env --- src/pkg/mcp/tools/common.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pkg/mcp/tools/common.go b/src/pkg/mcp/tools/common.go index 30a8392e2..415e8fad8 100644 --- a/src/pkg/mcp/tools/common.go +++ b/src/pkg/mcp/tools/common.go @@ -3,6 +3,7 @@ package tools import ( "crypto/rand" "encoding/base64" + "os" "regexp" "strings" @@ -70,3 +71,10 @@ func CreateRandomConfigValue() string { str = re.ReplaceAllString(str, "") return str } + +func Getenv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} From 97f2804faae6fbe95f41c441fed447794bb514d0 Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Tue, 2 Sep 2025 18:16:10 -0700 Subject: [PATCH 03/13] Able to set provider --- src/cmd/cli/command/mcp.go | 4 +- src/pkg/mcp/resources/resources.go | 199 ++++++++++++++++++----------- src/pkg/mcp/tools/deploy.go | 7 +- src/pkg/mcp/tools/destroy.go | 4 +- src/pkg/mcp/tools/estimate.go | 6 +- src/pkg/mcp/tools/listConfig.go | 4 +- src/pkg/mcp/tools/removeConfig.go | 4 +- src/pkg/mcp/tools/services.go | 4 +- src/pkg/mcp/tools/setConfig.go | 4 +- src/pkg/mcp/tools/tools.go | 6 +- 10 files changed, 147 insertions(+), 95 deletions(-) diff --git a/src/cmd/cli/command/mcp.go b/src/cmd/cli/command/mcp.go index f63cf8305..f7cb54b20 100644 --- a/src/cmd/cli/command/mcp.go +++ b/src/cmd/cli/command/mcp.go @@ -72,11 +72,11 @@ set_config - This tool sets or updates configuration variables for a deployed ap // Setup resources term.Debug("Setting up resources") - resources.SetupResources(s) + resources.SetupResources(s, &providerID) // Setup tools term.Debug("Setting up tools") - tools.SetupTools(s, getCluster(), authPort, providerID) + tools.SetupTools(s, getCluster(), authPort, &providerID) // Start auth server for docker login flow if authPort != 0 { diff --git a/src/pkg/mcp/resources/resources.go b/src/pkg/mcp/resources/resources.go index 88406a765..b8ba39e77 100644 --- a/src/pkg/mcp/resources/resources.go +++ b/src/pkg/mcp/resources/resources.go @@ -14,15 +14,18 @@ import ( ) // SetupResources configures and adds all resources to the MCP server -func SetupResources(s *server.MCPServer) { +func SetupResources(s *server.MCPServer, providerId *client.ProviderID) { // Create and add documentation resource setupDocumentationResource(s) // Create and add samples examples resource setupSamplesResource(s) - // Create and add sample prompt - setupSamplePrompt(s) + // // Create and add sample prompt + // setupSamplePrompt(s) + + //AWS BYOC + setupAWSBYOPrompt(s, providerId) } var knowledgeBasePath = filepath.Join(client.StateDir, "knowledge_base.json") @@ -87,52 +90,41 @@ func setupSamplesResource(s *server.MCPServer) { }) } -var BYOCPrompt = mcp.NewPrompt("BYOC Setup", - mcp.WithPromptDescription("Bring Your Own Cloud setup for AWS, GCP, or DO"), - mcp.WithArgument("cloud", - mcp.ArgumentDescription("Supported Cloud providers: AWS, GCP, or DO"), - mcp.RequiredArgument(), - ), - mcp.WithArgument("AWS_ACCESS_KEY_ID", - mcp.ArgumentDescription("Your AWS Access Key ID"), - mcp.RequiredArgument(), - ), - - mcp.WithArgument("AWS_SECRET_ACCESS_KEY", - mcp.ArgumentDescription("Your AWS Secret Access Key"), - mcp.RequiredArgument(), - ), -) +// func handleAwsBYOCPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { +// awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") +// awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") -func handleBYOCPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - cloud := getStringArg(req.Params.Arguments, "cloud", "aws") - awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") - awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") - var prompt strings.Builder - - // Adjust complexity based on user level - switch cloud { - case "aws", "AWS": - prompt.WriteString(fmt.Sprintf("Please explain %s in simple terms suitable for someone new to the topic. ", awsID)) - prompt.WriteString("Use clear language and avoid jargon. ") - case "gcp", "GCP": - prompt.WriteString(fmt.Sprintf("Please provide a detailed explanation of %s. ", awsID)) - prompt.WriteString("Include technical details but ensure clarity. ") - case "do", "DO": - prompt.WriteString(fmt.Sprintf("Please provide an in-depth analysis of %s. ", awsID)) - prompt.WriteString("Include advanced concepts, edge cases, and technical nuances. ") - } +// err := os.Setenv("DEFANG_PROVIDER", "aws") +// if err != nil { +// return nil, err +// } - return &mcp.GetPromptResult{ - Description: fmt.Sprintf("awsid: %s, awsSecret: %s, cloud: %s", awsID, awsSecret, cloud), - Messages: []mcp.PromptMessage{ - { - Role: "user", - Content: mcp.NewTextContent(prompt.String()), - }, - }, - }, nil -} +// // providerId.Set(cliClient.ProviderAWS.String()) + +// // Set environment variables +// if awsID != "" { +// err := os.Setenv("AWS_ACCESS_KEY_ID", awsID) +// if err != nil { +// return nil, err +// } +// } +// if awsSecret != "" { +// err := os.Setenv("AWS_SECRET_ACCESS_KEY", awsSecret) +// if err != nil { +// return nil, err +// } +// } + +// return &mcp.GetPromptResult{ +// Description: fmt.Sprintf("AWS credentials set - Access Key ID: %s, Secret Access Key: %s", awsID, maskSecret(awsSecret)), +// Messages: []mcp.PromptMessage{ +// { +// Role: "user", +// Content: mcp.NewTextContent("I have my aws credentials set for BYOC Defang. Now use the defang MCP tool \"deploy\" to deploy my application to set my provider to aws."), +// }, +// }, +// }, nil +// } func getStringArg(args map[string]string, key, defaultValue string) string { if val, exists := args[key]; exists { @@ -141,6 +133,14 @@ func getStringArg(args map[string]string, key, defaultValue string) string { return defaultValue } +// maskSecret masks all but the last 4 characters of a secret for safe logging +func maskSecret(secret string) string { + if len(secret) <= 4 { + return strings.Repeat("*", len(secret)) + } + return strings.Repeat("*", len(secret)-4) + secret[len(secret)-4:] +} + // func getBoolArg(args map[string]string, key string, defaultValue bool) bool { // if val, exists := args[key]; exists { // // Accept "true"/"false" strings @@ -154,40 +154,91 @@ func getStringArg(args map[string]string, key, defaultValue string) string { // return defaultValue // } -// setupSamplePrompt configures and adds the sample prompt to the MCP server -func setupSamplePrompt(s *server.MCPServer) { - samplePrompt := mcp.NewPrompt("Make Dockerfile and compose file", - mcp.WithPromptDescription("The user should give you a path to a project directory, and you should create a Dockerfile and compose file for that project. If there is an app folder, make the Dockerfile for that folder. Then make a compose file for original project directory or root of that project directory."), - mcp.WithArgument("project_path", - mcp.ArgumentDescription("Path to the project directory"), +// // setupSamplePrompt configures and adds the sample prompt to the MCP server +// func setupSamplePrompt(s *server.MCPServer) { +// samplePrompt := mcp.NewPrompt("Make Dockerfile and compose file", +// mcp.WithPromptDescription("The user should give you a path to a project directory, and you should create a Dockerfile and compose file for that project. If there is an app folder, make the Dockerfile for that folder. Then make a compose file for original project directory or root of that project directory."), +// mcp.WithArgument("project_path", +// mcp.ArgumentDescription("Path to the project directory"), +// mcp.RequiredArgument(), +// ), +// ) + +// s.AddPrompt(samplePrompt, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { +// projectPath, ok := request.Params.Arguments["project_path"] +// if !ok || projectPath == "" { +// projectPath = "." +// term.Warn("Project path not provided, using current directory", "dir", projectPath) +// } + +// return mcp.NewGetPromptResult( +// "Code assistance to make Dockerfile and compose file", +// []mcp.PromptMessage{ +// mcp.NewPromptMessage( +// mcp.RoleUser, +// mcp.NewTextContent(fmt.Sprintf("You are a helpful code writer. I will give you a path which is %s to a project directory, and you should create a Dockerfile and compose file for that project. If there is an app folder, make the Dockerfile for that folder. Then make a compose file for original project directory or root of that project directory. When creating these files, make sure to use the samples and examples resource for reference of defang. If you need more information, please use the defang documentation resource. When you are creating these files please make sure to scan carefully to expose any ports, start commands, and any other information needed for the project.", projectPath)), +// ), +// mcp.NewPromptMessage( +// mcp.RoleAssistant, +// mcp.NewEmbeddedResource(mcp.TextResourceContents{ +// MIMEType: "application/json", +// URI: "doc:///knowledge_base/knowledge_base.json", +// }), +// ), +// }, +// ), nil +// }) +// } + +// setupAWSBYOPrompt configures and adds the AWS BYOCPrompt to the MCP server +func setupAWSBYOPrompt(s *server.MCPServer, providerId *client.ProviderID) { + awsBYOCPrompt := mcp.NewPrompt("AWS BYOC Setup", + mcp.WithPromptDescription("Bring Your Own Cloud setup for AWS"), + + mcp.WithArgument("AWS_ACCESS_KEY_ID", + mcp.ArgumentDescription("Your AWS Access Key ID"), + mcp.RequiredArgument(), + ), + + mcp.WithArgument("AWS_SECRET_ACCESS_KEY", + mcp.ArgumentDescription("Your AWS Secret Access Key"), mcp.RequiredArgument(), ), ) - s.AddPrompt(BYOCPrompt, handleBYOCPrompt) + s.AddPrompt(awsBYOCPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") + awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") - s.AddPrompt(samplePrompt, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - projectPath, ok := request.Params.Arguments["project_path"] - if !ok || projectPath == "" { - projectPath = "." - term.Warn("Project path not provided, using current directory", "dir", projectPath) + err := os.Setenv("DEFANG_PROVIDER", "aws") + if err != nil { + return nil, err } - return mcp.NewGetPromptResult( - "Code assistance to make Dockerfile and compose file", - []mcp.PromptMessage{ - mcp.NewPromptMessage( - mcp.RoleUser, - mcp.NewTextContent(fmt.Sprintf("You are a helpful code writer. I will give you a path which is %s to a project directory, and you should create a Dockerfile and compose file for that project. If there is an app folder, make the Dockerfile for that folder. Then make a compose file for original project directory or root of that project directory. When creating these files, make sure to use the samples and examples resource for reference of defang. If you need more information, please use the defang documentation resource. When you are creating these files please make sure to scan carefully to expose any ports, start commands, and any other information needed for the project.", projectPath)), - ), - mcp.NewPromptMessage( - mcp.RoleAssistant, - mcp.NewEmbeddedResource(mcp.TextResourceContents{ - MIMEType: "application/json", - URI: "doc:///knowledge_base/knowledge_base.json", - }), - ), + providerId.Set(client.ProviderAWS.String()) + + // Set environment variables + if awsID != "" { + err := os.Setenv("AWS_ACCESS_KEY_ID", awsID) + if err != nil { + return nil, err + } + } + if awsSecret != "" { + err := os.Setenv("AWS_SECRET_ACCESS_KEY", awsSecret) + if err != nil { + return nil, err + } + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("AWS credentials set - Access Key ID: %s, Secret Access Key: %s", awsID, maskSecret(awsSecret)), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent("I have my aws credentials set for BYOC Defang. Now use the defang MCP tool \"deploy\" to deploy my application to set my provider to aws."), + }, }, - ), nil + }, nil }) } diff --git a/src/pkg/mcp/tools/deploy.go b/src/pkg/mcp/tools/deploy.go index 67f9b8e2a..0a7acdfd2 100644 --- a/src/pkg/mcp/tools/deploy.go +++ b/src/pkg/mcp/tools/deploy.go @@ -19,7 +19,7 @@ import ( ) // setupDeployTool configures and adds the deployment tool to the MCP server -func setupDeployTool(s *server.MCPServer, cluster string, providerId cliClient.ProviderID) { +func setupDeployTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { term.Debug("Creating deployment tool") composeUpTool := mcp.NewTool("deploy", mcp.WithDescription("Deploy services using defang"), @@ -69,7 +69,8 @@ func setupDeployTool(s *server.MCPServer, cluster string, providerId cliClient.P client.Track("MCP Deploy Tool") term.Debug("Function invoked: cli.NewProvider") - provider, err := cli.NewProvider(ctx, providerId, client) + + provider, err := cli.NewProvider(ctx, *providerId, client) if err != nil { term.Error("Failed to get new provider", "error", err) return mcp.NewToolResultErrorFromErr("Failed to get new provider", err), err @@ -116,7 +117,7 @@ func setupDeployTool(s *server.MCPServer, cluster string, providerId cliClient.P term.Debugf("Deployment ID: %s", deployResp.Etag) var portal string - if providerId == cliClient.ProviderDefang { + if *providerId == cliClient.ProviderDefang { // Get the portal URL for browser preview portalURL := "https://portal.defang.io/" diff --git a/src/pkg/mcp/tools/destroy.go b/src/pkg/mcp/tools/destroy.go index 4413ff823..a64c89b14 100644 --- a/src/pkg/mcp/tools/destroy.go +++ b/src/pkg/mcp/tools/destroy.go @@ -17,7 +17,7 @@ import ( ) // setupDestroyTool configures and adds the destroy tool to the MCP server -func setupDestroyTool(s *server.MCPServer, cluster string, providerId cliClient.ProviderID) { +func setupDestroyTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { term.Debug("Creating destroy tool") composeDownTool := mcp.NewTool("destroy", mcp.WithDescription("Remove services using defang."), @@ -43,7 +43,7 @@ func setupDestroyTool(s *server.MCPServer, cluster string, providerId cliClient. client.Track("MCP Destroy Tool") term.Debug("Function invoked: cli.NewProvider") - provider, err := cli.NewProvider(ctx, providerId, client) + provider, err := cli.NewProvider(ctx, *providerId, client) if err != nil { term.Error("Failed to get new provider", "error", err) return mcp.NewToolResultErrorFromErr("Failed to get new provider", err), err diff --git a/src/pkg/mcp/tools/estimate.go b/src/pkg/mcp/tools/estimate.go index 7a887faec..f6e93ecc2 100644 --- a/src/pkg/mcp/tools/estimate.go +++ b/src/pkg/mcp/tools/estimate.go @@ -18,9 +18,9 @@ import ( ) // setupEstimateTool configures and adds the estimate tool to the MCP server -func setupEstimateTool(s *server.MCPServer, cluster string, providerId cliClient.ProviderID) { - if providerId == cliClient.ProviderDefang { - providerId = cliClient.ProviderAWS // Default to AWS +func setupEstimateTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { + if *providerId == cliClient.ProviderDefang { + *providerId = cliClient.ProviderAWS // Default to AWS } term.Debug("Creating estimate tool") diff --git a/src/pkg/mcp/tools/listConfig.go b/src/pkg/mcp/tools/listConfig.go index 04ae673f2..0e9fa388b 100644 --- a/src/pkg/mcp/tools/listConfig.go +++ b/src/pkg/mcp/tools/listConfig.go @@ -17,7 +17,7 @@ import ( ) // setupSetConfigTool configures and adds the estimate tool to the MCP server -func setupListConfigTool(s *server.MCPServer, cluster string, providerId cliClient.ProviderID) { +func setupListConfigTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { term.Debug("Creating list config tool") listConfigTool := mcp.NewTool("list_configs", mcp.WithDescription("List all config variables for the defang project"), @@ -53,7 +53,7 @@ func setupListConfigTool(s *server.MCPServer, cluster string, providerId cliClie } term.Debug("Function invoked: cli.NewProvider") - provider, err := cli.NewProvider(ctx, providerId, client) + provider, err := cli.NewProvider(ctx, *providerId, client) if err != nil { term.Error("Failed to get new provider", "error", err) diff --git a/src/pkg/mcp/tools/removeConfig.go b/src/pkg/mcp/tools/removeConfig.go index b55e106b2..48540e4e4 100644 --- a/src/pkg/mcp/tools/removeConfig.go +++ b/src/pkg/mcp/tools/removeConfig.go @@ -16,7 +16,7 @@ import ( ) // setupRemoveConfigTool configures and adds the estimate tool to the MCP server -func setupRemoveConfigTool(s *server.MCPServer, cluster string, providerId cliClient.ProviderID) { +func setupRemoveConfigTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { term.Debug("Creating remove config tool") removeConfigTool := mcp.NewTool("remove_config", mcp.WithDescription("Remove a config variable for the defang project"), @@ -62,7 +62,7 @@ func setupRemoveConfigTool(s *server.MCPServer, cluster string, providerId cliCl } term.Debug("Function invoked: cli.NewProvider") - provider, err := cli.NewProvider(ctx, providerId, client) + provider, err := cli.NewProvider(ctx, *providerId, client) if err != nil { term.Error("Failed to get new provider", "error", err) diff --git a/src/pkg/mcp/tools/services.go b/src/pkg/mcp/tools/services.go index b476e9537..7b89bf454 100644 --- a/src/pkg/mcp/tools/services.go +++ b/src/pkg/mcp/tools/services.go @@ -19,7 +19,7 @@ import ( ) // setupServicesTool configures and adds the services tool to the MCP server -func setupServicesTool(s *server.MCPServer, cluster string, providerId cliClient.ProviderID) { +func setupServicesTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { term.Debug("Creating services tool") servicesTool := mcp.NewTool("services", mcp.WithDescription("List information about services in Defang Playground"), @@ -57,7 +57,7 @@ func setupServicesTool(s *server.MCPServer, cluster string, providerId cliClient // Create a Defang client term.Debug("Function invoked: cli.NewProvider") - provider, err := cli.NewProvider(ctx, providerId, client) + provider, err := cli.NewProvider(ctx, *providerId, client) if err != nil { term.Error("Failed to create provider", "error", err) return mcp.NewToolResultErrorFromErr("Failed to create provider", err), err diff --git a/src/pkg/mcp/tools/setConfig.go b/src/pkg/mcp/tools/setConfig.go index d58319bc5..7375a11fd 100644 --- a/src/pkg/mcp/tools/setConfig.go +++ b/src/pkg/mcp/tools/setConfig.go @@ -16,7 +16,7 @@ import ( ) // setupSetConfigTool configures and adds the estimate tool to the MCP server -func setupSetConfigTool(s *server.MCPServer, cluster string, providerId cliClient.ProviderID) { +func setupSetConfigTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { term.Debug("Creating set config tool") setConfigTool := mcp.NewTool("set_config", mcp.WithDescription("Set a config variable for the defang project"), @@ -73,7 +73,7 @@ func setupSetConfigTool(s *server.MCPServer, cluster string, providerId cliClien } term.Debug("Function invoked: cli.NewProvider") - provider, err := cli.NewProvider(ctx, providerId, client) + provider, err := cli.NewProvider(ctx, *providerId, client) if err != nil { term.Error("Failed to get new provider", "error", err) diff --git a/src/pkg/mcp/tools/tools.go b/src/pkg/mcp/tools/tools.go index 04a27fb63..929ee3a55 100644 --- a/src/pkg/mcp/tools/tools.go +++ b/src/pkg/mcp/tools/tools.go @@ -7,9 +7,9 @@ import ( ) // SetupTools configures and adds all the MCP tools to the server -func SetupTools(s *server.MCPServer, cluster string, authPort int, providerId client.ProviderID) { - if providerId == "" || providerId == client.ProviderAuto { - providerId = client.ProviderDefang // Default to Defang Playground if not specified +func SetupTools(s *server.MCPServer, cluster string, authPort int, providerId *client.ProviderID) { + if providerId == nil || *providerId == client.ProviderAuto { + *providerId = client.ProviderDefang // Default to Defang Playground if not specified } // Create a tool for logging in and getting a new token From 52f4a164956858d2e41599e351871820f9966f2e Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Thu, 4 Sep 2025 11:16:13 -0700 Subject: [PATCH 04/13] Refactor prompt --- src/cmd/cli/command/mcp.go | 5 + src/pkg/mcp/prompts/awsBYOC.go | 67 ++++++++++++ src/pkg/mcp/prompts/common.go | 8 ++ src/pkg/mcp/prompts/prompts.go | 12 +++ src/pkg/mcp/resources/resources.go | 160 ----------------------------- 5 files changed, 92 insertions(+), 160 deletions(-) create mode 100644 src/pkg/mcp/prompts/awsBYOC.go create mode 100644 src/pkg/mcp/prompts/common.go create mode 100644 src/pkg/mcp/prompts/prompts.go diff --git a/src/cmd/cli/command/mcp.go b/src/cmd/cli/command/mcp.go index f7cb54b20..2ebdec4aa 100644 --- a/src/cmd/cli/command/mcp.go +++ b/src/cmd/cli/command/mcp.go @@ -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" @@ -74,6 +75,10 @@ set_config - This tool sets or updates configuration variables for a deployed ap term.Debug("Setting up resources") resources.SetupResources(s, &providerID) + //setup prompts + term.Debug("Setting up prompts") + prompts.SetupPrompts(s, &providerID) + // Setup tools term.Debug("Setting up tools") tools.SetupTools(s, getCluster(), authPort, &providerID) diff --git a/src/pkg/mcp/prompts/awsBYOC.go b/src/pkg/mcp/prompts/awsBYOC.go new file mode 100644 index 000000000..01c3c09d3 --- /dev/null +++ b/src/pkg/mcp/prompts/awsBYOC.go @@ -0,0 +1,67 @@ +package prompts + +import ( + "context" + "errors" + "os" + + "github.com/DefangLabs/defang/src/pkg/cli/client" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func setupAWSBYOPrompt(s *server.MCPServer, providerId *client.ProviderID) { + awsBYOCPrompt := mcp.NewPrompt("AWS BYOC Setup", + mcp.WithPromptDescription("Bring Your Own Cloud setup for AWS"), + + mcp.WithArgument("AWS_ACCESS_KEY_ID", + mcp.ArgumentDescription("Your AWS Access Key ID"), + mcp.RequiredArgument(), + ), + + mcp.WithArgument("AWS_SECRET_ACCESS_KEY", + mcp.ArgumentDescription("Your AWS Secret Access Key"), + mcp.RequiredArgument(), + ), + ) + + s.AddPrompt(awsBYOCPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") + awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") + + providerId.Set(client.ProviderAWS.String()) + + err := os.Setenv("DEFANG_PROVIDER", "aws") + if err != nil { + return nil, err + } + + if awsID == "" { + return nil, errors.New("AWS_ACCESS_KEY_ID is required") + } + + err = os.Setenv("AWS_ACCESS_KEY_ID", awsID) + if err != nil { + return nil, err + } + + 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 + } + + return &mcp.GetPromptResult{ + Description: "AWS BYOC Setup Complete", + Messages: []mcp.PromptMessage{ + { + Role: mcp.RoleUser, + Content: mcp.NewTextContent("Deploy this application to Defang."), + }, + }, + }, nil + }) +} diff --git a/src/pkg/mcp/prompts/common.go b/src/pkg/mcp/prompts/common.go new file mode 100644 index 000000000..e8ebedb86 --- /dev/null +++ b/src/pkg/mcp/prompts/common.go @@ -0,0 +1,8 @@ +package prompts + +func getStringArg(args map[string]string, key, defaultValue string) string { + if val, exists := args[key]; exists { + return val + } + return defaultValue +} diff --git a/src/pkg/mcp/prompts/prompts.go b/src/pkg/mcp/prompts/prompts.go new file mode 100644 index 000000000..3612acb68 --- /dev/null +++ b/src/pkg/mcp/prompts/prompts.go @@ -0,0 +1,12 @@ +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, providerId *client.ProviderID) { + //AWS BYOC + setupAWSBYOPrompt(s, providerId) +} diff --git a/src/pkg/mcp/resources/resources.go b/src/pkg/mcp/resources/resources.go index b8ba39e77..f56bc500f 100644 --- a/src/pkg/mcp/resources/resources.go +++ b/src/pkg/mcp/resources/resources.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/term" @@ -20,12 +19,6 @@ func SetupResources(s *server.MCPServer, providerId *client.ProviderID) { // Create and add samples examples resource setupSamplesResource(s) - - // // Create and add sample prompt - // setupSamplePrompt(s) - - //AWS BYOC - setupAWSBYOPrompt(s, providerId) } var knowledgeBasePath = filepath.Join(client.StateDir, "knowledge_base.json") @@ -89,156 +82,3 @@ func setupSamplesResource(s *server.MCPServer) { }, nil }) } - -// func handleAwsBYOCPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { -// awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") -// awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") - -// err := os.Setenv("DEFANG_PROVIDER", "aws") -// if err != nil { -// return nil, err -// } - -// // providerId.Set(cliClient.ProviderAWS.String()) - -// // Set environment variables -// if awsID != "" { -// err := os.Setenv("AWS_ACCESS_KEY_ID", awsID) -// if err != nil { -// return nil, err -// } -// } -// if awsSecret != "" { -// err := os.Setenv("AWS_SECRET_ACCESS_KEY", awsSecret) -// if err != nil { -// return nil, err -// } -// } - -// return &mcp.GetPromptResult{ -// Description: fmt.Sprintf("AWS credentials set - Access Key ID: %s, Secret Access Key: %s", awsID, maskSecret(awsSecret)), -// Messages: []mcp.PromptMessage{ -// { -// Role: "user", -// Content: mcp.NewTextContent("I have my aws credentials set for BYOC Defang. Now use the defang MCP tool \"deploy\" to deploy my application to set my provider to aws."), -// }, -// }, -// }, nil -// } - -func getStringArg(args map[string]string, key, defaultValue string) string { - if val, exists := args[key]; exists { - return val - } - return defaultValue -} - -// maskSecret masks all but the last 4 characters of a secret for safe logging -func maskSecret(secret string) string { - if len(secret) <= 4 { - return strings.Repeat("*", len(secret)) - } - return strings.Repeat("*", len(secret)-4) + secret[len(secret)-4:] -} - -// func getBoolArg(args map[string]string, key string, defaultValue bool) bool { -// if val, exists := args[key]; exists { -// // Accept "true"/"false" strings -// if val == "true" { -// return true -// } -// if val == "false" { -// return false -// } -// } -// return defaultValue -// } - -// // setupSamplePrompt configures and adds the sample prompt to the MCP server -// func setupSamplePrompt(s *server.MCPServer) { -// samplePrompt := mcp.NewPrompt("Make Dockerfile and compose file", -// mcp.WithPromptDescription("The user should give you a path to a project directory, and you should create a Dockerfile and compose file for that project. If there is an app folder, make the Dockerfile for that folder. Then make a compose file for original project directory or root of that project directory."), -// mcp.WithArgument("project_path", -// mcp.ArgumentDescription("Path to the project directory"), -// mcp.RequiredArgument(), -// ), -// ) - -// s.AddPrompt(samplePrompt, func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { -// projectPath, ok := request.Params.Arguments["project_path"] -// if !ok || projectPath == "" { -// projectPath = "." -// term.Warn("Project path not provided, using current directory", "dir", projectPath) -// } - -// return mcp.NewGetPromptResult( -// "Code assistance to make Dockerfile and compose file", -// []mcp.PromptMessage{ -// mcp.NewPromptMessage( -// mcp.RoleUser, -// mcp.NewTextContent(fmt.Sprintf("You are a helpful code writer. I will give you a path which is %s to a project directory, and you should create a Dockerfile and compose file for that project. If there is an app folder, make the Dockerfile for that folder. Then make a compose file for original project directory or root of that project directory. When creating these files, make sure to use the samples and examples resource for reference of defang. If you need more information, please use the defang documentation resource. When you are creating these files please make sure to scan carefully to expose any ports, start commands, and any other information needed for the project.", projectPath)), -// ), -// mcp.NewPromptMessage( -// mcp.RoleAssistant, -// mcp.NewEmbeddedResource(mcp.TextResourceContents{ -// MIMEType: "application/json", -// URI: "doc:///knowledge_base/knowledge_base.json", -// }), -// ), -// }, -// ), nil -// }) -// } - -// setupAWSBYOPrompt configures and adds the AWS BYOCPrompt to the MCP server -func setupAWSBYOPrompt(s *server.MCPServer, providerId *client.ProviderID) { - awsBYOCPrompt := mcp.NewPrompt("AWS BYOC Setup", - mcp.WithPromptDescription("Bring Your Own Cloud setup for AWS"), - - mcp.WithArgument("AWS_ACCESS_KEY_ID", - mcp.ArgumentDescription("Your AWS Access Key ID"), - mcp.RequiredArgument(), - ), - - mcp.WithArgument("AWS_SECRET_ACCESS_KEY", - mcp.ArgumentDescription("Your AWS Secret Access Key"), - mcp.RequiredArgument(), - ), - ) - - s.AddPrompt(awsBYOCPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") - awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") - - err := os.Setenv("DEFANG_PROVIDER", "aws") - if err != nil { - return nil, err - } - - providerId.Set(client.ProviderAWS.String()) - - // Set environment variables - if awsID != "" { - err := os.Setenv("AWS_ACCESS_KEY_ID", awsID) - if err != nil { - return nil, err - } - } - if awsSecret != "" { - err := os.Setenv("AWS_SECRET_ACCESS_KEY", awsSecret) - if err != nil { - return nil, err - } - } - - return &mcp.GetPromptResult{ - Description: fmt.Sprintf("AWS credentials set - Access Key ID: %s, Secret Access Key: %s", awsID, maskSecret(awsSecret)), - Messages: []mcp.PromptMessage{ - { - Role: "user", - Content: mcp.NewTextContent("I have my aws credentials set for BYOC Defang. Now use the defang MCP tool \"deploy\" to deploy my application to set my provider to aws."), - }, - }, - }, nil - }) -} From bfe12361eae8eb4a4031e6617fff05806fb7609a Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Thu, 4 Sep 2025 12:13:30 -0700 Subject: [PATCH 05/13] Missing region env --- src/pkg/mcp/prompts/awsBYOC.go | 21 ++++++++++++++++++--- src/pkg/mcp/tools/destroy.go | 4 ++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/pkg/mcp/prompts/awsBYOC.go b/src/pkg/mcp/prompts/awsBYOC.go index 01c3c09d3..6a4e57fc4 100644 --- a/src/pkg/mcp/prompts/awsBYOC.go +++ b/src/pkg/mcp/prompts/awsBYOC.go @@ -11,8 +11,8 @@ import ( ) func setupAWSBYOPrompt(s *server.MCPServer, providerId *client.ProviderID) { - awsBYOCPrompt := mcp.NewPrompt("AWS BYOC Setup", - mcp.WithPromptDescription("Bring Your Own Cloud setup for AWS"), + awsBYOCPrompt := mcp.NewPrompt("AWS Setup", + mcp.WithPromptDescription("Setup for AWS"), mcp.WithArgument("AWS_ACCESS_KEY_ID", mcp.ArgumentDescription("Your AWS Access Key ID"), @@ -23,15 +23,30 @@ func setupAWSBYOPrompt(s *server.MCPServer, providerId *client.ProviderID) { mcp.ArgumentDescription("Your AWS Secret Access Key"), mcp.RequiredArgument(), ), + + mcp.WithArgument("AWS_REGION", + mcp.ArgumentDescription("Your AWS Region"), + mcp.RequiredArgument(), + ), ) s.AddPrompt(awsBYOCPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") + region := getStringArg(req.Params.Arguments, "AWS_REGION", "") + + if region == "" { + return nil, errors.New("AWS_REGION is required") + } + + err := os.Setenv("AWS_REGION", region) + if err != nil { + return nil, err + } providerId.Set(client.ProviderAWS.String()) - err := os.Setenv("DEFANG_PROVIDER", "aws") + err = os.Setenv("DEFANG_PROVIDER", "aws") if err != nil { return nil, err } diff --git a/src/pkg/mcp/tools/destroy.go b/src/pkg/mcp/tools/destroy.go index 95f264719..2abbdd47d 100644 --- a/src/pkg/mcp/tools/destroy.go +++ b/src/pkg/mcp/tools/destroy.go @@ -90,10 +90,10 @@ func setupDestroyTool(s *server.MCPServer, cluster string, providerId *cliClient return result, err } - return mcp.NewToolResultErrorFromErr("Failed to destroy project", err), err + return mcp.NewToolResultErrorFromErr("Failed to send destroy request", err), err } - return mcp.NewToolResultText(fmt.Sprintf("Successfully destroyed project: %s, etag: %s", projectName, deployment)), nil + return mcp.NewToolResultText(fmt.Sprintf("The project is in the process of being destroyed: %s, please tail this deployment ID: %s for status updates.", projectName, deployment)), nil }) } From 7adb82fef3f5c5db8ccced5ba076c7f8f50bd45a Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Mon, 8 Sep 2025 15:14:18 -0700 Subject: [PATCH 06/13] Added CheckProviderConfigured and mod CanIUseProvider --- src/pkg/mcp/tools/common.go | 70 ++++++++++++++++++++++++------- src/pkg/mcp/tools/deploy.go | 17 ++++---- src/pkg/mcp/tools/destroy.go | 30 +++---------- src/pkg/mcp/tools/estimate.go | 10 +++-- src/pkg/mcp/tools/listConfig.go | 5 +++ src/pkg/mcp/tools/removeConfig.go | 5 +++ src/pkg/mcp/tools/services.go | 5 +++ src/pkg/mcp/tools/setConfig.go | 5 +++ 8 files changed, 94 insertions(+), 53 deletions(-) diff --git a/src/pkg/mcp/tools/common.go b/src/pkg/mcp/tools/common.go index 415e8fad8..ae3eda385 100644 --- a/src/pkg/mcp/tools/common.go +++ b/src/pkg/mcp/tools/common.go @@ -1,14 +1,16 @@ package tools import ( - "crypto/rand" - "encoding/base64" - "os" - "regexp" + "context" + "errors" "strings" + "github.com/DefangLabs/defang/src/pkg/cli" + "github.com/DefangLabs/defang/src/pkg/cli/client" + cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/cli/compose" "github.com/DefangLabs/defang/src/pkg/term" + defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/bufbuild/connect-go" "github.com/mark3labs/mcp-go/mcp" ) @@ -62,19 +64,55 @@ func HandleConfigError(err error) *mcp.CallToolResult { return nil } -func CreateRandomConfigValue() string { - // Note that no error handling is necessary, as Read always succeeds. - key := make([]byte, 32) - rand.Read(key) - str := base64.StdEncoding.EncodeToString(key) - re := regexp.MustCompile("[+/=]") - str = re.ReplaceAllString(str, "") - return str +func CanIUseProvider(ctx context.Context, grpcClient client.FabricClient, providerId client.ProviderID, projectName string, provider client.Provider, serviceCount int) error { + + canUseReq := defangv1.CanIUseRequest{ + Project: projectName, + Provider: providerId.Value(), + ServiceCount: int32(serviceCount), // #nosec G115 - service count will not overflow int32 + } + term.Debug("Function invoked: client.CanIUse") + resp, err := grpcClient.CanIUse(ctx, &canUseReq) + if err != nil { + return err + } + + term.Debug("Function invoked: provider.SetCanIUseConfig") + provider.SetCanIUseConfig(resp) + return nil } -func Getenv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value +func providerNotConfiguredError(providerId client.ProviderID) error { + if providerId == client.ProviderAuto { + term.Error("No provider configured") + return errors.New("No provider configured, please use the appropriate prompts and type /mcp.defang.AWS_Setup for AWS, /mcp.defang.GCP_Setup for GCP, or /mcp.defang.Playground_Setup for Playground in the chat.") } - return fallback + return nil +} + +func CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, providerId cliClient.ProviderID, projectName string, serviceCount int) (cliClient.Provider, error) { + provider, err := cli.NewProvider(ctx, providerId, client) + if err != nil { + term.Error("Failed to get new provider", "error", err) + // return mcp.NewToolResultErrorFromErr("Failed to get new provider", err), err + return nil, err + } + + _, err = provider.AccountInfo(ctx) + if err != nil { + return nil, err + } + + err = CanIUseProvider(ctx, client, providerId, projectName, provider, serviceCount) + if err != nil { + term.Error("Failed to use provider", "error", err) + // return mcp.NewToolResultErrorFromErr("Failed to use provider", err), err + return nil, err + } + + // if resp == nil { + // term.Error("No provider configured") + // return errors.New("No provider configured, please use the appropriate prompts and type /mcp.defang.AWS_Setup for AWS, /mcp.defang.GCP_Setup for GCP, or /mcp.defang.Playground_Setup for Playground in the chat.") + // } + return provider, nil } diff --git a/src/pkg/mcp/tools/deploy.go b/src/pkg/mcp/tools/deploy.go index edb9ee40d..6a123ce1f 100644 --- a/src/pkg/mcp/tools/deploy.go +++ b/src/pkg/mcp/tools/deploy.go @@ -33,6 +33,12 @@ func setupDeployTool(s *server.MCPServer, cluster string, providerId *cliClient. // Add the deployment tool handler - make it non-blocking term.Debug("Adding deployment tool handler") s.AddTool(composeUpTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + + err := providerNotConfiguredError(*providerId) + if err != nil { + return mcp.NewToolResultErrorFromErr("No provider configured", err), err + } + // Get compose path term.Debug("Compose up tool called - deploying services") track.Evt("MCP Deploy Tool") @@ -70,16 +76,9 @@ func setupDeployTool(s *server.MCPServer, cluster string, providerId *cliClient. term.Debug("Function invoked: cli.NewProvider") - provider, err := cli.NewProvider(ctx, *providerId, client) - if err != nil { - term.Error("Failed to get new provider", "error", err) - return mcp.NewToolResultErrorFromErr("Failed to get new provider", err), err - } - - err = canIUseProvider(ctx, client, project.Name, provider, len(project.Services)) + provider, err := CheckProviderConfigured(ctx, client, *providerId, project.Name, len(project.Services)) if err != nil { - term.Error("Failed to use provider", "error", err) - return mcp.NewToolResultErrorFromErr("Failed to use provider", err), err + return mcp.NewToolResultErrorFromErr("Provider not configured correctly", err), err } // Deploy the services diff --git a/src/pkg/mcp/tools/destroy.go b/src/pkg/mcp/tools/destroy.go index 2abbdd47d..debce9935 100644 --- a/src/pkg/mcp/tools/destroy.go +++ b/src/pkg/mcp/tools/destroy.go @@ -10,7 +10,6 @@ import ( cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/term" "github.com/DefangLabs/defang/src/pkg/track" - defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/bufbuild/connect-go" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -34,6 +33,11 @@ func setupDestroyTool(s *server.MCPServer, cluster string, providerId *cliClient term.Debug("Compose down tool called - removing services") track.Evt("MCP Destroy Tool") + err := providerNotConfiguredError(*providerId) + if err != nil { + return mcp.NewToolResultErrorFromErr("No provider configured", err), err + } + term.Debug("Function invoked: cli.Connect") client, err := cli.Connect(ctx, cluster) if err != nil { @@ -70,7 +74,7 @@ func setupDestroyTool(s *server.MCPServer, cluster string, providerId *cliClient return mcp.NewToolResultErrorFromErr("Failed to load project name", err), err } - err = canIUseProvider(ctx, client, projectName, provider, 0) + err = CanIUseProvider(ctx, client, *providerId, projectName, provider, 0) if err != nil { term.Error("Failed to use provider", "error", err) return mcp.NewToolResultErrorFromErr("Failed to use provider", err), err @@ -96,25 +100,3 @@ func setupDestroyTool(s *server.MCPServer, cluster string, providerId *cliClient return mcp.NewToolResultText(fmt.Sprintf("The project is in the process of being destroyed: %s, please tail this deployment ID: %s for status updates.", projectName, deployment)), nil }) } - -func canIUseProvider(ctx context.Context, grpcClient cliClient.FabricClient, projectName string, provider cliClient.Provider, serviceCount int) error { - info, err := provider.AccountInfo(ctx) - if err != nil { - return err - } - - canUseReq := defangv1.CanIUseRequest{ - Project: projectName, - Provider: info.Provider.Value(), - ServiceCount: int32(serviceCount), // #nosec G115 - service count will not overflow int32 - } - term.Debug("Function invoked: client.CanIUse") - resp, err := grpcClient.CanIUse(ctx, &canUseReq) - if err != nil { - return err - } - - term.Debug("Function invoked: provider.SetCanIUseConfig") - provider.SetCanIUseConfig(resp) - return nil -} diff --git a/src/pkg/mcp/tools/estimate.go b/src/pkg/mcp/tools/estimate.go index f6e93ecc2..42b36edf9 100644 --- a/src/pkg/mcp/tools/estimate.go +++ b/src/pkg/mcp/tools/estimate.go @@ -19,10 +19,6 @@ import ( // setupEstimateTool configures and adds the estimate tool to the MCP server func setupEstimateTool(s *server.MCPServer, cluster string, providerId *cliClient.ProviderID) { - if *providerId == cliClient.ProviderDefang { - *providerId = cliClient.ProviderAWS // Default to AWS - } - term.Debug("Creating estimate tool") estimateTool := mcp.NewTool("estimate", mcp.WithDescription("Estimate the cost of a Defang project deployed to AWS"), @@ -51,6 +47,12 @@ func setupEstimateTool(s *server.MCPServer, cluster string, providerId *cliClien term.Debug("Estimate tool called") track.Evt("MCP Estimate Tool") + if *providerId == cliClient.ProviderDefang || *providerId == cliClient.ProviderAuto { + //We only support estimates for AWS and GCP, not playground; suggest use setup prompt for another provider + err := errors.New("estimates are only supported for AWS and GCP; please configure another provider using the appropriate prompts and type /mcp.defang.AWS_Setup for AWS or /mcp.defang.GCP_Setup for GCP") + return mcp.NewToolResultErrorFromErr("No compatible provider configured", err), err + } + wd, err := request.RequireString("working_directory") if err != nil || wd == "" { term.Error("Invalid working directory", "error", errors.New("working_directory is required")) diff --git a/src/pkg/mcp/tools/listConfig.go b/src/pkg/mcp/tools/listConfig.go index 0e9fa388b..b9857adeb 100644 --- a/src/pkg/mcp/tools/listConfig.go +++ b/src/pkg/mcp/tools/listConfig.go @@ -34,6 +34,11 @@ func setupListConfigTool(s *server.MCPServer, cluster string, providerId *cliCli term.Debug("List Config tool called") track.Evt("MCP List Config Tool") + err := providerNotConfiguredError(*providerId) + if err != nil { + return mcp.NewToolResultErrorFromErr("No provider configured", err), err + } + wd, err := request.RequireString("working_directory") if err != nil || wd == "" { term.Error("Invalid working directory", "error", errors.New("working_directory is required")) diff --git a/src/pkg/mcp/tools/removeConfig.go b/src/pkg/mcp/tools/removeConfig.go index 48540e4e4..87c2b04a7 100644 --- a/src/pkg/mcp/tools/removeConfig.go +++ b/src/pkg/mcp/tools/removeConfig.go @@ -37,6 +37,11 @@ func setupRemoveConfigTool(s *server.MCPServer, cluster string, providerId *cliC term.Debug("Remove Config tool called") track.Evt("MCP Remove Config Tool") + err := providerNotConfiguredError(*providerId) + if err != nil { + return mcp.NewToolResultErrorFromErr("No provider configured", err), err + } + wd, err := request.RequireString("working_directory") if err != nil || wd == "" { term.Error("Invalid working directory", "error", errors.New("working_directory is required")) diff --git a/src/pkg/mcp/tools/services.go b/src/pkg/mcp/tools/services.go index 7b89bf454..f7fe685f2 100644 --- a/src/pkg/mcp/tools/services.go +++ b/src/pkg/mcp/tools/services.go @@ -35,6 +35,11 @@ func setupServicesTool(s *server.MCPServer, cluster string, providerId *cliClien term.Debug("Services tool called - fetching services from Defang") track.Evt("MCP Services Tool") + err := providerNotConfiguredError(*providerId) + if err != nil { + return mcp.NewToolResultErrorFromErr("No provider configured", err), err + } + wd, err := request.RequireString("working_directory") if err != nil || wd == "" { term.Error("Invalid working directory", "error", errors.New("working_directory is required")) diff --git a/src/pkg/mcp/tools/setConfig.go b/src/pkg/mcp/tools/setConfig.go index 7375a11fd..ef1a7e1f5 100644 --- a/src/pkg/mcp/tools/setConfig.go +++ b/src/pkg/mcp/tools/setConfig.go @@ -42,6 +42,11 @@ func setupSetConfigTool(s *server.MCPServer, cluster string, providerId *cliClie term.Debug("Set Config tool called") track.Evt("MCP Set Config Tool") + err := providerNotConfiguredError(*providerId) + if err != nil { + return mcp.NewToolResultErrorFromErr("No provider configured", err), err + } + wd, err := request.RequireString("working_directory") if err != nil || wd == "" { term.Error("Invalid working directory", "error", errors.New("working_directory is required")) From 0049a2eaae72d3d24c5fd3aca690f5d72b9b5d34 Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Mon, 8 Sep 2025 15:16:48 -0700 Subject: [PATCH 07/13] prompts --- src/pkg/mcp/prompts/awsBYOC.go | 104 ++++++++++++++++++------- src/pkg/mcp/prompts/gcpBYOC.go | 65 ++++++++++++++++ src/pkg/mcp/prompts/playgroundSetup.go | 36 +++++++++ src/pkg/mcp/prompts/prompts.go | 10 ++- src/pkg/mcp/tools/common.go | 1 - src/pkg/mcp/tools/deploy.go | 1 - 6 files changed, 184 insertions(+), 33 deletions(-) create mode 100644 src/pkg/mcp/prompts/gcpBYOC.go create mode 100644 src/pkg/mcp/prompts/playgroundSetup.go diff --git a/src/pkg/mcp/prompts/awsBYOC.go b/src/pkg/mcp/prompts/awsBYOC.go index 6a4e57fc4..04554945d 100644 --- a/src/pkg/mcp/prompts/awsBYOC.go +++ b/src/pkg/mcp/prompts/awsBYOC.go @@ -5,66 +5,81 @@ import ( "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, providerId *client.ProviderID) { +func setupAWSBYOPrompt(s *server.MCPServer, cluster string, providerId *client.ProviderID) { awsBYOCPrompt := mcp.NewPrompt("AWS Setup", mcp.WithPromptDescription("Setup for AWS"), - mcp.WithArgument("AWS_ACCESS_KEY_ID", - mcp.ArgumentDescription("Your AWS Access Key ID"), + 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.RequiredArgument(), ), mcp.WithArgument("AWS_REGION", mcp.ArgumentDescription("Your AWS Region"), - mcp.RequiredArgument(), ), ) s.AddPrompt(awsBYOCPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - awsID := getStringArg(req.Params.Arguments, "AWS_ACCESS_KEY_ID", "") - awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "") - region := getStringArg(req.Params.Arguments, "AWS_REGION", "") - - if region == "" { - return nil, errors.New("AWS_REGION is required") + // 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 + } } - err := os.Setenv("AWS_REGION", region) + fabric, err := cli.Connect(ctx, cluster) if err != nil { return nil, err } - providerId.Set(client.ProviderAWS.String()) - - err = os.Setenv("DEFANG_PROVIDER", "aws") + _, err = tools.CheckProviderConfigured(ctx, fabric, client.ProviderAWS, "", 0) if err != nil { return nil, err } - if awsID == "" { - return nil, errors.New("AWS_ACCESS_KEY_ID is required") - } + *providerId = client.ProviderAWS - err = os.Setenv("AWS_ACCESS_KEY_ID", awsID) - if err != nil { - return nil, err - } - - if awsSecret == "" { - return nil, errors.New("AWS_SECRET_ACCESS_KEY is required") - } - - err = os.Setenv("AWS_SECRET_ACCESS_KEY", awsSecret) + //FIXME: Should not be setting both the global and env var + err = os.Setenv("DEFANG_PROVIDER", "aws") if err != nil { return nil, err } @@ -74,9 +89,40 @@ func setupAWSBYOPrompt(s *server.MCPServer, providerId *client.ProviderID) { Messages: []mcp.PromptMessage{ { Role: mcp.RoleUser, - Content: mcp.NewTextContent("Deploy this application to Defang."), + Content: mcp.NewTextContent("Can you deploy my application now."), }, }, }, nil }) } + +// Check if the provided AWS access key ID is valid +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 +} diff --git a/src/pkg/mcp/prompts/gcpBYOC.go b/src/pkg/mcp/prompts/gcpBYOC.go new file mode 100644 index 000000000..79b8e7d16 --- /dev/null +++ b/src/pkg/mcp/prompts/gcpBYOC.go @@ -0,0 +1,65 @@ +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 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) { + projectID := getStringArg(req.Params.Arguments, "GCP_PROJECT_ID", "") + + if projectID == "" { + return nil, errors.New("GCP_PROJECT_ID is required") + } + + 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.RoleAssistant, + Content: mcp.NewTextContent("Can you deploy my application now."), + }, + }, + }, nil + }) +} diff --git a/src/pkg/mcp/prompts/playgroundSetup.go b/src/pkg/mcp/prompts/playgroundSetup.go new file mode 100644 index 000000000..0dafec243 --- /dev/null +++ b/src/pkg/mcp/prompts/playgroundSetup.go @@ -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.RoleAssistant, + Content: mcp.NewTextContent("Can you deploy my application now."), + }, + }, + }, nil + }) +} diff --git a/src/pkg/mcp/prompts/prompts.go b/src/pkg/mcp/prompts/prompts.go index 3612acb68..8ca7cbb3a 100644 --- a/src/pkg/mcp/prompts/prompts.go +++ b/src/pkg/mcp/prompts/prompts.go @@ -6,7 +6,13 @@ import ( ) // SetupPrompts configures and adds all prompts to the MCP server -func SetupPrompts(s *server.MCPServer, providerId *client.ProviderID) { +func SetupPrompts(s *server.MCPServer, cluster string, providerId *client.ProviderID) { //AWS BYOC - setupAWSBYOPrompt(s, providerId) + setupAWSBYOPrompt(s, cluster, providerId) + + //GCP BYOC + setupGCPBYOPrompt(s, cluster, providerId) + + //Playground + setupPlaygroundPrompt(s, providerId) } diff --git a/src/pkg/mcp/tools/common.go b/src/pkg/mcp/tools/common.go index ae3eda385..1a758cf9a 100644 --- a/src/pkg/mcp/tools/common.go +++ b/src/pkg/mcp/tools/common.go @@ -65,7 +65,6 @@ func HandleConfigError(err error) *mcp.CallToolResult { } func CanIUseProvider(ctx context.Context, grpcClient client.FabricClient, providerId client.ProviderID, projectName string, provider client.Provider, serviceCount int) error { - canUseReq := defangv1.CanIUseRequest{ Project: projectName, Provider: providerId.Value(), diff --git a/src/pkg/mcp/tools/deploy.go b/src/pkg/mcp/tools/deploy.go index 6a123ce1f..eec112f26 100644 --- a/src/pkg/mcp/tools/deploy.go +++ b/src/pkg/mcp/tools/deploy.go @@ -33,7 +33,6 @@ func setupDeployTool(s *server.MCPServer, cluster string, providerId *cliClient. // Add the deployment tool handler - make it non-blocking term.Debug("Adding deployment tool handler") s.AddTool(composeUpTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - err := providerNotConfiguredError(*providerId) if err != nil { return mcp.NewToolResultErrorFromErr("No provider configured", err), err From d968066b734a7470363d8e8a5b0e325dc16619eb Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Mon, 8 Sep 2025 15:17:09 -0700 Subject: [PATCH 08/13] cleanup --- src/cmd/cli/command/mcp.go | 10 ++++++---- src/pkg/mcp/resources/resources.go | 2 +- src/pkg/mcp/tools/tools.go | 4 ---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/cmd/cli/command/mcp.go b/src/cmd/cli/command/mcp.go index 2ebdec4aa..4e2150501 100644 --- a/src/cmd/cli/command/mcp.go +++ b/src/cmd/cli/command/mcp.go @@ -71,17 +71,19 @@ 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, &providerID) + resources.SetupResources(s) //setup prompts term.Debug("Setting up prompts") - prompts.SetupPrompts(s, &providerID) + 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 { @@ -89,7 +91,7 @@ set_config - This tool sets or updates configuration variables for a deployed ap 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) } }() diff --git a/src/pkg/mcp/resources/resources.go b/src/pkg/mcp/resources/resources.go index f56bc500f..75b5a762d 100644 --- a/src/pkg/mcp/resources/resources.go +++ b/src/pkg/mcp/resources/resources.go @@ -13,7 +13,7 @@ import ( ) // SetupResources configures and adds all resources to the MCP server -func SetupResources(s *server.MCPServer, providerId *client.ProviderID) { +func SetupResources(s *server.MCPServer) { // Create and add documentation resource setupDocumentationResource(s) diff --git a/src/pkg/mcp/tools/tools.go b/src/pkg/mcp/tools/tools.go index 929ee3a55..2459b27e3 100644 --- a/src/pkg/mcp/tools/tools.go +++ b/src/pkg/mcp/tools/tools.go @@ -8,10 +8,6 @@ import ( // SetupTools configures and adds all the MCP tools to the server func SetupTools(s *server.MCPServer, cluster string, authPort int, providerId *client.ProviderID) { - if providerId == nil || *providerId == client.ProviderAuto { - *providerId = client.ProviderDefang // Default to Defang Playground if not specified - } - // Create a tool for logging in and getting a new token term.Debug("Setting up login tool") setupLoginTool(s, cluster, authPort) From 542109d389a77b7bc29e669e58c63c7159d1a0a0 Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Mon, 8 Sep 2025 15:22:40 -0700 Subject: [PATCH 09/13] Replace the vendorHash in cli.nix --- pkgs/defang/cli.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/defang/cli.nix b/pkgs/defang/cli.nix index 3bcc54b9f..c2e1fef8a 100644 --- a/pkgs/defang/cli.nix +++ b/pkgs/defang/cli.nix @@ -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" ]; From 594962f72df7f1ec82616f45af2d71d4d81b55a6 Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Mon, 8 Sep 2025 15:34:51 -0700 Subject: [PATCH 10/13] Remove unused code --- src/pkg/mcp/prompts/awsBYOC.go | 2 +- src/pkg/mcp/prompts/common.go | 2 ++ src/pkg/mcp/prompts/gcpBYOC.go | 10 +++------- src/pkg/mcp/prompts/playgroundSetup.go | 2 +- src/pkg/mcp/tools/common.go | 4 ---- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/pkg/mcp/prompts/awsBYOC.go b/src/pkg/mcp/prompts/awsBYOC.go index 04554945d..f3dd49ae7 100644 --- a/src/pkg/mcp/prompts/awsBYOC.go +++ b/src/pkg/mcp/prompts/awsBYOC.go @@ -89,7 +89,7 @@ func setupAWSBYOPrompt(s *server.MCPServer, cluster string, providerId *client.P Messages: []mcp.PromptMessage{ { Role: mcp.RoleUser, - Content: mcp.NewTextContent("Can you deploy my application now."), + Content: mcp.NewTextContent(postPrompt), }, }, }, nil diff --git a/src/pkg/mcp/prompts/common.go b/src/pkg/mcp/prompts/common.go index e8ebedb86..a89445fb6 100644 --- a/src/pkg/mcp/prompts/common.go +++ b/src/pkg/mcp/prompts/common.go @@ -1,5 +1,7 @@ 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 diff --git a/src/pkg/mcp/prompts/gcpBYOC.go b/src/pkg/mcp/prompts/gcpBYOC.go index 79b8e7d16..ad3a41aa9 100644 --- a/src/pkg/mcp/prompts/gcpBYOC.go +++ b/src/pkg/mcp/prompts/gcpBYOC.go @@ -2,7 +2,6 @@ package prompts import ( "context" - "errors" "os" "github.com/DefangLabs/defang/src/pkg/cli" @@ -23,11 +22,8 @@ func setupGCPBYOPrompt(s *server.MCPServer, cluster string, providerId *client.P ) s.AddPrompt(gcpBYOPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - projectID := getStringArg(req.Params.Arguments, "GCP_PROJECT_ID", "") - - if projectID == "" { - return nil, errors.New("GCP_PROJECT_ID is required") - } + // 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 { @@ -57,7 +53,7 @@ func setupGCPBYOPrompt(s *server.MCPServer, cluster string, providerId *client.P Messages: []mcp.PromptMessage{ { Role: mcp.RoleAssistant, - Content: mcp.NewTextContent("Can you deploy my application now."), + Content: mcp.NewTextContent(postPrompt), }, }, }, nil diff --git a/src/pkg/mcp/prompts/playgroundSetup.go b/src/pkg/mcp/prompts/playgroundSetup.go index 0dafec243..884f946b7 100644 --- a/src/pkg/mcp/prompts/playgroundSetup.go +++ b/src/pkg/mcp/prompts/playgroundSetup.go @@ -28,7 +28,7 @@ func setupPlaygroundPrompt(s *server.MCPServer, providerId *client.ProviderID) { Messages: []mcp.PromptMessage{ { Role: mcp.RoleAssistant, - Content: mcp.NewTextContent("Can you deploy my application now."), + Content: mcp.NewTextContent(postPrompt), }, }, }, nil diff --git a/src/pkg/mcp/tools/common.go b/src/pkg/mcp/tools/common.go index 1a758cf9a..4acd3ab2e 100644 --- a/src/pkg/mcp/tools/common.go +++ b/src/pkg/mcp/tools/common.go @@ -109,9 +109,5 @@ func CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, return nil, err } - // if resp == nil { - // term.Error("No provider configured") - // return errors.New("No provider configured, please use the appropriate prompts and type /mcp.defang.AWS_Setup for AWS, /mcp.defang.GCP_Setup for GCP, or /mcp.defang.Playground_Setup for Playground in the chat.") - // } return provider, nil } From 88ed489526d9ffc469d5bd1b7070cb3cc2be0f73 Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Mon, 8 Sep 2025 15:37:51 -0700 Subject: [PATCH 11/13] Change role response --- src/pkg/mcp/prompts/gcpBYOC.go | 2 +- src/pkg/mcp/prompts/playgroundSetup.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pkg/mcp/prompts/gcpBYOC.go b/src/pkg/mcp/prompts/gcpBYOC.go index ad3a41aa9..e18b8683a 100644 --- a/src/pkg/mcp/prompts/gcpBYOC.go +++ b/src/pkg/mcp/prompts/gcpBYOC.go @@ -52,7 +52,7 @@ func setupGCPBYOPrompt(s *server.MCPServer, cluster string, providerId *client.P Description: "GCP BYOC Setup Complete", Messages: []mcp.PromptMessage{ { - Role: mcp.RoleAssistant, + Role: mcp.RoleUser, Content: mcp.NewTextContent(postPrompt), }, }, diff --git a/src/pkg/mcp/prompts/playgroundSetup.go b/src/pkg/mcp/prompts/playgroundSetup.go index 884f946b7..2e9e2e313 100644 --- a/src/pkg/mcp/prompts/playgroundSetup.go +++ b/src/pkg/mcp/prompts/playgroundSetup.go @@ -27,7 +27,7 @@ func setupPlaygroundPrompt(s *server.MCPServer, providerId *client.ProviderID) { Description: "Defang playground Setup Complete", Messages: []mcp.PromptMessage{ { - Role: mcp.RoleAssistant, + Role: mcp.RoleUser, Content: mcp.NewTextContent(postPrompt), }, }, From ddca90d004cdaab68488a9c0ea626b0b318c7ef4 Mon Sep 17 00:00:00 2001 From: Kevin Vo Date: Mon, 8 Sep 2025 15:51:48 -0700 Subject: [PATCH 12/13] Add source --- src/pkg/mcp/prompts/awsBYOC.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pkg/mcp/prompts/awsBYOC.go b/src/pkg/mcp/prompts/awsBYOC.go index f3dd49ae7..30bea315d 100644 --- a/src/pkg/mcp/prompts/awsBYOC.go +++ b/src/pkg/mcp/prompts/awsBYOC.go @@ -97,6 +97,7 @@ func setupAWSBYOPrompt(s *server.MCPServer, cluster string, providerId *client.P } // 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{ From cb2a64af7eb586710e8c2c2c56ab67be1217c985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lio=E6=9D=8E=E6=AD=90?= Date: Mon, 8 Sep 2025 16:26:31 -0700 Subject: [PATCH 13/13] Apply suggestions from code review --- src/pkg/mcp/prompts/awsBYOC.go | 6 ++++++ src/pkg/mcp/prompts/common.go | 2 +- src/pkg/mcp/tools/common.go | 2 -- src/pkg/mcp/tools/estimate.go | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pkg/mcp/prompts/awsBYOC.go b/src/pkg/mcp/prompts/awsBYOC.go index 30bea315d..35758bd4d 100644 --- a/src/pkg/mcp/prompts/awsBYOC.go +++ b/src/pkg/mcp/prompts/awsBYOC.go @@ -64,6 +64,12 @@ func setupAWSBYOPrompt(s *server.MCPServer, cluster string, providerId *client.P if err != nil { return nil, err } + + region := getStringArg(req.Params.Arguments, "AWS_REGION", "") + err = os.Setenv("AWS_REGION", region) + if err != nil { + return nil, err + } } fabric, err := cli.Connect(ctx, cluster) diff --git a/src/pkg/mcp/prompts/common.go b/src/pkg/mcp/prompts/common.go index a89445fb6..2f1cc6bcb 100644 --- a/src/pkg/mcp/prompts/common.go +++ b/src/pkg/mcp/prompts/common.go @@ -1,6 +1,6 @@ package prompts -const postPrompt = "Can you deploy my application now." +const postPrompt = "Please deploy my application with Defang now." func getStringArg(args map[string]string, key, defaultValue string) string { if val, exists := args[key]; exists { diff --git a/src/pkg/mcp/tools/common.go b/src/pkg/mcp/tools/common.go index 4acd3ab2e..5079d064f 100644 --- a/src/pkg/mcp/tools/common.go +++ b/src/pkg/mcp/tools/common.go @@ -93,7 +93,6 @@ func CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, provider, err := cli.NewProvider(ctx, providerId, client) if err != nil { term.Error("Failed to get new provider", "error", err) - // return mcp.NewToolResultErrorFromErr("Failed to get new provider", err), err return nil, err } @@ -105,7 +104,6 @@ func CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, err = CanIUseProvider(ctx, client, providerId, projectName, provider, serviceCount) if err != nil { term.Error("Failed to use provider", "error", err) - // return mcp.NewToolResultErrorFromErr("Failed to use provider", err), err return nil, err } diff --git a/src/pkg/mcp/tools/estimate.go b/src/pkg/mcp/tools/estimate.go index 42b36edf9..9d448e2c0 100644 --- a/src/pkg/mcp/tools/estimate.go +++ b/src/pkg/mcp/tools/estimate.go @@ -48,7 +48,7 @@ func setupEstimateTool(s *server.MCPServer, cluster string, providerId *cliClien track.Evt("MCP Estimate Tool") if *providerId == cliClient.ProviderDefang || *providerId == cliClient.ProviderAuto { - //We only support estimates for AWS and GCP, not playground; suggest use setup prompt for another provider + // We only support estimates for AWS and GCP, not playground; suggest use setup prompt for another provider err := errors.New("estimates are only supported for AWS and GCP; please configure another provider using the appropriate prompts and type /mcp.defang.AWS_Setup for AWS or /mcp.defang.GCP_Setup for GCP") return mcp.NewToolResultErrorFromErr("No compatible provider configured", err), err }