Skip to content

Commit 30e4e71

Browse files
KevyVolionello
andauthored
BYOC mcp (#1409)
* spike on byoc mcp * set defang env * Able to set provider * Refactor prompt * Missing region env * Added CheckProviderConfigured and mod CanIUseProvider * prompts * cleanup * Replace the vendorHash in cli.nix * Remove unused code * Change role response * Add source * Apply suggestions from code review --------- Co-authored-by: Lio李歐 <[email protected]>
1 parent 1255bac commit 30e4e71

File tree

19 files changed

+394
-112
lines changed

19 files changed

+394
-112
lines changed

pkgs/defang/cli.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ buildGoModule {
77
pname = "defang-cli";
88
version = "git";
99
src = ../../src;
10-
vendorHash = "sha256-0M0jtBaPE1jSx4nrOR4XMw1Im1tMYTKYCkcKiZ1bj8M="; # TODO: use fetchFromGitHub
10+
vendorHash = "sha256-8caEfevVUKXsYA5YyVDuPZTqMnYXIZnC4kTTGfPmuqA="; # TODO: use fetchFromGitHub
1111

1212
subPackages = [ "cmd/cli" ];
1313

src/cmd/cli/command/mcp.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
99
"github.com/DefangLabs/defang/src/pkg/login"
1010
"github.com/DefangLabs/defang/src/pkg/mcp"
11+
"github.com/DefangLabs/defang/src/pkg/mcp/prompts"
1112
"github.com/DefangLabs/defang/src/pkg/mcp/resources"
1213
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
1314
"github.com/DefangLabs/defang/src/pkg/term"
@@ -70,21 +71,27 @@ set_config - This tool sets or updates configuration variables for a deployed ap
7071
`),
7172
)
7273

74+
cluster := getCluster()
75+
7376
// Setup resources
7477
term.Debug("Setting up resources")
7578
resources.SetupResources(s)
7679

80+
//setup prompts
81+
term.Debug("Setting up prompts")
82+
prompts.SetupPrompts(s, cluster, &providerID)
83+
7784
// Setup tools
7885
term.Debug("Setting up tools")
79-
tools.SetupTools(s, getCluster(), authPort, providerID)
86+
tools.SetupTools(s, cluster, authPort, &providerID)
8087

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

8693
go func() {
87-
if err := login.InteractiveLoginInsideDocker(cmd.Context(), getCluster(), authPort); err != nil {
94+
if err := login.InteractiveLoginInsideDocker(cmd.Context(), cluster, authPort); err != nil {
8895
term.Error("Failed to start auth server", "error", err)
8996
}
9097
}()

src/go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ require (
4242
github.com/hashicorp/go-retryablehttp v0.7.7
4343
github.com/hexops/gotextdiff v1.0.3
4444
github.com/joho/godotenv v1.5.1
45-
github.com/mark3labs/mcp-go v0.29.0
45+
github.com/mark3labs/mcp-go v0.38.0
4646
github.com/miekg/dns v1.1.59
4747
github.com/moby/patternmatcher v0.6.0
4848
github.com/muesli/termenv v0.15.2
@@ -77,6 +77,8 @@ require (
7777
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect
7878
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect
7979
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
80+
github.com/bahlo/generic-list-go v0.2.0 // indirect
81+
github.com/buger/jsonparser v1.1.1 // indirect
8082
github.com/cespare/xxhash/v2 v2.3.0 // indirect
8183
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
8284
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
@@ -93,7 +95,9 @@ require (
9395
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
9496
github.com/hashicorp/go-multierror v1.1.1 // indirect
9597
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
98+
github.com/invopop/jsonschema v0.13.0 // indirect
9699
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
100+
github.com/mailru/easyjson v0.7.7 // indirect
97101
github.com/mattn/go-runewidth v0.0.14 // indirect
98102
github.com/morikuni/aec v1.0.0 // indirect
99103
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
@@ -105,6 +109,7 @@ require (
105109
github.com/spf13/cast v1.7.1 // indirect
106110
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
107111
github.com/stretchr/objx v0.5.2 // indirect
112+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
108113
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
109114
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
110115
github.com/zeebo/errs v1.4.0 // indirect

src/go.sum

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,12 @@ github.com/awslabs/goformation/v7 v7.13.1 h1:QlPn8qwNCqYhrb4GW8kLjT4j1J49n5Qh/an
108108
github.com/awslabs/goformation/v7 v7.13.1/go.mod h1:FTCFMNesubEX0LAd6kIR+YkDD1U+5UaMbXtgPUgsck0=
109109
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
110110
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
111+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
112+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
111113
github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
112114
github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
115+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
116+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
113117
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
114118
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
115119
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -213,10 +217,13 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
213217
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
214218
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
215219
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
220+
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
221+
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
216222
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
217223
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
218224
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
219225
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
226+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
220227
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
221228
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
222229
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -232,8 +239,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
232239
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
233240
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
234241
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
235-
github.com/mark3labs/mcp-go v0.29.0 h1:sH1NBcumKskhxqYzhXfGc201D7P76TVXiT0fGVhabeI=
236-
github.com/mark3labs/mcp-go v0.29.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
242+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
243+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
244+
github.com/mark3labs/mcp-go v0.38.0 h1:E5tmJiIXkhwlV0pLAwAT0O5ZjUZSISE/2Jxg+6vpq4I=
245+
github.com/mark3labs/mcp-go v0.38.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
237246
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
238247
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
239248
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -304,6 +313,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
304313
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
305314
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
306315
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
316+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
317+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
307318
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
308319
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
309320
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=

src/pkg/mcp/prompts/awsBYOC.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package prompts
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
8+
"github.com/DefangLabs/defang/src/pkg/cli"
9+
"github.com/DefangLabs/defang/src/pkg/cli/client"
10+
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
11+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
)
14+
15+
func setupAWSBYOPrompt(s *server.MCPServer, cluster string, providerId *client.ProviderID) {
16+
awsBYOCPrompt := mcp.NewPrompt("AWS Setup",
17+
mcp.WithPromptDescription("Setup for AWS"),
18+
19+
mcp.WithArgument("AWS_Credential",
20+
mcp.ArgumentDescription("Your AWS Access Key ID or AWS Profile Name"),
21+
mcp.RequiredArgument(),
22+
),
23+
24+
mcp.WithArgument("AWS_SECRET_ACCESS_KEY",
25+
mcp.ArgumentDescription("Your AWS Secret Access Key"),
26+
),
27+
28+
mcp.WithArgument("AWS_REGION",
29+
mcp.ArgumentDescription("Your AWS Region"),
30+
),
31+
)
32+
33+
s.AddPrompt(awsBYOCPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
34+
// Can never be nil or empty due to RequiredArgument
35+
awsID := req.Params.Arguments["AWS_Credential"]
36+
if isValidAWSKey(awsID) {
37+
err := os.Setenv("AWS_ACCESS_KEY_ID", awsID)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
awsSecret := getStringArg(req.Params.Arguments, "AWS_SECRET_ACCESS_KEY", "")
43+
region := getStringArg(req.Params.Arguments, "AWS_REGION", "")
44+
45+
if awsSecret == "" {
46+
return nil, errors.New("AWS_SECRET_ACCESS_KEY is required")
47+
}
48+
49+
err = os.Setenv("AWS_SECRET_ACCESS_KEY", awsSecret)
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
if region == "" {
55+
return nil, errors.New("AWS_REGION is required")
56+
}
57+
58+
err = os.Setenv("AWS_REGION", region)
59+
if err != nil {
60+
return nil, err
61+
}
62+
} else {
63+
err := os.Setenv("AWS_PROFILE", awsID)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
region := getStringArg(req.Params.Arguments, "AWS_REGION", "")
69+
err = os.Setenv("AWS_REGION", region)
70+
if err != nil {
71+
return nil, err
72+
}
73+
}
74+
75+
fabric, err := cli.Connect(ctx, cluster)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
_, err = tools.CheckProviderConfigured(ctx, fabric, client.ProviderAWS, "", 0)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
*providerId = client.ProviderAWS
86+
87+
//FIXME: Should not be setting both the global and env var
88+
err = os.Setenv("DEFANG_PROVIDER", "aws")
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
return &mcp.GetPromptResult{
94+
Description: "AWS BYOC Setup Complete",
95+
Messages: []mcp.PromptMessage{
96+
{
97+
Role: mcp.RoleUser,
98+
Content: mcp.NewTextContent(postPrompt),
99+
},
100+
},
101+
}, nil
102+
})
103+
}
104+
105+
// Check if the provided AWS access key ID is valid
106+
// https://medium.com/@TalBeerySec/a-short-note-on-aws-key-id-f88cc4317489
107+
func isValidAWSKey(key string) bool {
108+
// Define accepted AWS access key prefixes
109+
acceptedPrefixes := map[string]bool{
110+
"ABIA": true,
111+
"ACCA": true,
112+
"AGPA": true,
113+
"AIDA": true,
114+
"AKPA": true,
115+
"AKIA": true,
116+
"ANPA": true,
117+
"ANVA": true,
118+
"APKA": true,
119+
"AROA": true,
120+
"ASCA": true,
121+
"ASIA": true,
122+
}
123+
124+
if len(key) < 16 {
125+
return false
126+
}
127+
128+
prefix := key[:4]
129+
_, ok := acceptedPrefixes[prefix]
130+
if !ok {
131+
return false
132+
}
133+
134+
return true
135+
}

src/pkg/mcp/prompts/common.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package prompts
2+
3+
const postPrompt = "Please deploy my application with Defang now."
4+
5+
func getStringArg(args map[string]string, key, defaultValue string) string {
6+
if val, exists := args[key]; exists {
7+
return val
8+
}
9+
return defaultValue
10+
}

src/pkg/mcp/prompts/gcpBYOC.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package prompts
2+
3+
import (
4+
"context"
5+
"os"
6+
7+
"github.com/DefangLabs/defang/src/pkg/cli"
8+
"github.com/DefangLabs/defang/src/pkg/cli/client"
9+
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
func setupGCPBYOPrompt(s *server.MCPServer, cluster string, providerId *client.ProviderID) {
15+
gcpBYOPrompt := mcp.NewPrompt("GCP Setup",
16+
mcp.WithPromptDescription("Setup for GCP"),
17+
18+
mcp.WithArgument("GCP_PROJECT_ID",
19+
mcp.ArgumentDescription("Your GCP Project ID"),
20+
mcp.RequiredArgument(),
21+
),
22+
)
23+
24+
s.AddPrompt(gcpBYOPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
25+
// Can never be nil or empty due to RequiredArgument
26+
projectID := req.Params.Arguments["GCP_PROJECT_ID"]
27+
28+
err := os.Setenv("GCP_PROJECT_ID", projectID)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
fabric, err := cli.Connect(ctx, cluster)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
_, err = tools.CheckProviderConfigured(ctx, fabric, client.ProviderGCP, "", 0)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
*providerId = client.ProviderGCP
44+
45+
//FIXME: Should not be setting both the global var and env var
46+
err = os.Setenv("DEFANG_PROVIDER", "gcp")
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return &mcp.GetPromptResult{
52+
Description: "GCP BYOC Setup Complete",
53+
Messages: []mcp.PromptMessage{
54+
{
55+
Role: mcp.RoleUser,
56+
Content: mcp.NewTextContent(postPrompt),
57+
},
58+
},
59+
}, nil
60+
})
61+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package prompts
2+
3+
import (
4+
"context"
5+
"os"
6+
7+
"github.com/DefangLabs/defang/src/pkg/cli/client"
8+
"github.com/mark3labs/mcp-go/mcp"
9+
"github.com/mark3labs/mcp-go/server"
10+
)
11+
12+
func setupPlaygroundPrompt(s *server.MCPServer, providerId *client.ProviderID) {
13+
playgroundPrompt := mcp.NewPrompt("Playground Setup",
14+
mcp.WithPromptDescription("Setup for Playground"),
15+
)
16+
17+
s.AddPrompt(playgroundPrompt, func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
18+
*providerId = client.ProviderDefang
19+
20+
//FIXME: Should not be setting both the global and env var
21+
err := os.Setenv("DEFANG_PROVIDER", "defang")
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
return &mcp.GetPromptResult{
27+
Description: "Defang playground Setup Complete",
28+
Messages: []mcp.PromptMessage{
29+
{
30+
Role: mcp.RoleUser,
31+
Content: mcp.NewTextContent(postPrompt),
32+
},
33+
},
34+
}, nil
35+
})
36+
}

src/pkg/mcp/prompts/prompts.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package prompts
2+
3+
import (
4+
"github.com/DefangLabs/defang/src/pkg/cli/client"
5+
"github.com/mark3labs/mcp-go/server"
6+
)
7+
8+
// SetupPrompts configures and adds all prompts to the MCP server
9+
func SetupPrompts(s *server.MCPServer, cluster string, providerId *client.ProviderID) {
10+
//AWS BYOC
11+
setupAWSBYOPrompt(s, cluster, providerId)
12+
13+
//GCP BYOC
14+
setupGCPBYOPrompt(s, cluster, providerId)
15+
16+
//Playground
17+
setupPlaygroundPrompt(s, providerId)
18+
}

0 commit comments

Comments
 (0)