Skip to content

Commit df09f1e

Browse files
nullfunclionello
andauthored
add mcp tool test (#1422)
Co-authored-by: Lio李歐 <[email protected]>
1 parent c365707 commit df09f1e

21 files changed

+3410
-554
lines changed

src/pkg/mcp/tools/common_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package tools
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/DefangLabs/defang/src/pkg/cli/client"
8+
"github.com/bufbuild/connect-go"
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestConfigureLoaderBranches(t *testing.T) {
14+
makeReq := func(args map[string]any) mcp.CallToolRequest {
15+
return mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: args}}
16+
}
17+
// project_name path
18+
loader1 := configureLoader(makeReq(map[string]any{"project_name": "myproj"}))
19+
assert.NotNil(t, loader1)
20+
21+
// compose_file_paths path
22+
loader2 := configureLoader(makeReq(map[string]any{"compose_file_paths": []string{"a.yml", "b.yml"}}))
23+
assert.NotNil(t, loader2)
24+
25+
// default path
26+
loader3 := configureLoader(makeReq(map[string]any{}))
27+
assert.NotNil(t, loader3)
28+
}
29+
30+
func TestHandleTermsOfServiceError(t *testing.T) {
31+
origErr := connect.NewError(connect.CodeFailedPrecondition, errors.New("terms of service not accepted"))
32+
res := HandleTermsOfServiceError(origErr)
33+
assert.NotNil(t, res)
34+
35+
otherErr := errors.New("some other error")
36+
res2 := HandleTermsOfServiceError(otherErr)
37+
assert.Nil(t, res2)
38+
}
39+
40+
func TestHandleConfigError(t *testing.T) {
41+
cfgErr := errors.New("missing configs: DB_PASSWORD")
42+
res := HandleConfigError(cfgErr)
43+
assert.NotNil(t, res)
44+
45+
otherErr := errors.New("another error")
46+
res2 := HandleConfigError(otherErr)
47+
assert.Nil(t, res2)
48+
}
49+
50+
func TestProviderNotConfiguredError(t *testing.T) {
51+
// provider auto should error
52+
err := providerNotConfiguredError(client.ProviderAuto)
53+
assert.Error(t, err)
54+
55+
// a real provider (simulate AWS value 'aws') should not error
56+
var pid client.ProviderID
57+
_ = pid.Set("aws")
58+
err2 := providerNotConfiguredError(pid)
59+
assert.NoError(t, err2)
60+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package tools
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"os"
7+
"strconv"
8+
9+
"github.com/DefangLabs/defang/src/pkg/cli"
10+
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
11+
"github.com/DefangLabs/defang/src/pkg/cli/compose"
12+
"github.com/DefangLabs/defang/src/pkg/login"
13+
"github.com/DefangLabs/defang/src/pkg/mcp/deployment_info"
14+
"github.com/DefangLabs/defang/src/pkg/term"
15+
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
16+
"github.com/mark3labs/mcp-go/mcp"
17+
)
18+
19+
// DefaultToolCLI implements all tool interfaces as passthroughs to the real CLI logic
20+
// This consolidates all Default<tool>CLI structs into one
21+
// Implements: DeployCLIInterface, DestroyCLIInterface, EstimateCLIInterface, ListConfigCLIInterface, LoginCLIInterface, RemoveConfigCLIInterface, SetConfigCLIInterface, CLIInterface, DeploymentInfoInterface
22+
23+
type DefaultToolCLI struct{}
24+
25+
// --- DefaultToolCLI: Core tool passthrough methods ---
26+
// All methods for DefaultToolCLI are grouped here for clarity and maintainability.
27+
// These implement the various tool interfaces as passthroughs to the real CLI logic
28+
29+
func (c *DefaultToolCLI) CanIUseProvider(ctx context.Context, client *cliClient.GrpcClient, providerId cliClient.ProviderID, projectName string, provider cliClient.Provider, serviceCount int) error {
30+
// If there is a real implementation, call it; otherwise, return nil or a stub error
31+
return nil
32+
}
33+
34+
func (c *DefaultToolCLI) ConfigSet(ctx context.Context, projectName string, provider cliClient.Provider, name, value string) error {
35+
return cli.ConfigSet(ctx, projectName, provider, name, value)
36+
}
37+
38+
func (c *DefaultToolCLI) RunEstimate(ctx context.Context, project *compose.Project, client *cliClient.GrpcClient, provider cliClient.Provider, providerId cliClient.ProviderID, region string, mode defangv1.DeploymentMode) (*defangv1.EstimateResponse, error) {
39+
return cli.RunEstimate(ctx, project, client, provider, providerId, region, mode)
40+
}
41+
func (c *DefaultToolCLI) PrintEstimate(mode defangv1.DeploymentMode, estimate *defangv1.EstimateResponse) {
42+
cli.PrintEstimate(mode, estimate)
43+
}
44+
45+
func (c *DefaultToolCLI) ListConfig(ctx context.Context, provider cliClient.Provider, projectName string) (*defangv1.Secrets, error) {
46+
req := &defangv1.ListConfigsRequest{Project: projectName}
47+
return provider.ListConfig(ctx, req)
48+
}
49+
50+
func (c *DefaultToolCLI) Connect(ctx context.Context, cluster string) (*cliClient.GrpcClient, error) {
51+
return cli.Connect(ctx, cluster)
52+
}
53+
54+
func (c *DefaultToolCLI) NewProviderGrpc(ctx context.Context, providerId cliClient.ProviderID, client *cliClient.GrpcClient) (cliClient.Provider, error) {
55+
return cli.NewProvider(ctx, providerId, client)
56+
}
57+
58+
func (c *DefaultToolCLI) NewProviderFabric(ctx context.Context, providerId cliClient.ProviderID, client cliClient.FabricClient) (cliClient.Provider, error) {
59+
return cli.NewProvider(ctx, providerId, client)
60+
}
61+
62+
func (c *DefaultToolCLI) ComposeUp(ctx context.Context, project *compose.Project, client *cliClient.GrpcClient, provider cliClient.Provider, uploadMode compose.UploadMode, mode defangv1.DeploymentMode) (*defangv1.DeployResponse, *compose.Project, error) {
63+
return cli.ComposeUp(ctx, project, client, provider, uploadMode, mode)
64+
}
65+
66+
func (c *DefaultToolCLI) ConfigureLoader(request mcp.CallToolRequest) cliClient.Loader {
67+
return configureLoader(request)
68+
}
69+
70+
func (c *DefaultToolCLI) ComposeDown(ctx context.Context, projectName string, client *cliClient.GrpcClient, provider cliClient.Provider) (string, error) {
71+
return cli.ComposeDown(ctx, projectName, client, provider)
72+
}
73+
74+
func (c *DefaultToolCLI) LoadProjectNameWithFallback(ctx context.Context, loader cliClient.Loader, provider cliClient.Provider) (string, error) {
75+
return cliClient.LoadProjectNameWithFallback(ctx, loader, provider)
76+
}
77+
78+
func (c *DefaultToolCLI) ConfigDelete(ctx context.Context, projectName string, provider cliClient.Provider, name string) error {
79+
return cli.ConfigDelete(ctx, projectName, provider, name)
80+
}
81+
82+
func (c *DefaultToolCLI) GetServices(ctx context.Context, projectName string, provider cliClient.Provider) ([]deployment_info.Service, error) {
83+
return deployment_info.GetServices(ctx, projectName, provider)
84+
}
85+
86+
func (c *DefaultToolCLI) CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, providerId cliClient.ProviderID, projectName string, serviceCount int) (cliClient.Provider, error) {
87+
return CheckProviderConfigured(ctx, client, providerId, projectName, serviceCount)
88+
}
89+
90+
func (c *DefaultToolCLI) CaptureTermOutput(mode defangv1.DeploymentMode, estimate *defangv1.EstimateResponse) string {
91+
// Use the same logic as DefaultEstimateCLI
92+
oldTerm := term.DefaultTerm
93+
stdout := new(bytes.Buffer)
94+
term.DefaultTerm = term.NewTerm(
95+
os.Stdin,
96+
stdout,
97+
new(bytes.Buffer),
98+
)
99+
100+
cli.PrintEstimate(mode, estimate)
101+
102+
term.DefaultTerm = oldTerm
103+
return stdout.String()
104+
}
105+
106+
func (c *DefaultToolCLI) LoadProject(ctx context.Context, loader cliClient.Loader) (*compose.Project, error) {
107+
return loader.LoadProject(ctx)
108+
}
109+
110+
func (c *DefaultToolCLI) CreatePlaygroundProvider(client *cliClient.GrpcClient) cliClient.Provider {
111+
return &cliClient.PlaygroundProvider{FabricClient: client}
112+
}
113+
114+
func (c *DefaultToolCLI) NewProvider(ctx context.Context, providerId cliClient.ProviderID, client *cliClient.GrpcClient) (cliClient.Provider, error) {
115+
return c.NewProviderGrpc(ctx, providerId, client)
116+
}
117+
118+
func (c *DefaultToolCLI) GetRegion(providerId cliClient.ProviderID) string {
119+
return cliClient.GetRegion(providerId)
120+
}
121+
122+
func (c *DefaultToolCLI) OpenBrowser(url string) error {
123+
// No-op stub implementation
124+
return nil
125+
}
126+
127+
func (c *DefaultToolCLI) SetProviderID(providerId *cliClient.ProviderID, providerString string) error {
128+
return providerId.Set(providerString)
129+
}
130+
131+
// --- Adapter types for tool interfaces ---
132+
// The following adapter types embed DefaultToolCLI to implement specific tool interfaces.
133+
type DeployCLIAdapter struct{ *DefaultToolCLI }
134+
type DestroyCLIAdapter struct{ *DefaultToolCLI }
135+
type SetConfigCLIAdapter struct{ *DefaultToolCLI }
136+
type RemoveConfigCLIAdapter struct{ *DefaultToolCLI }
137+
type ListConfigCLIAdapter struct{ *DefaultToolCLI }
138+
type LoginCLIAdapter struct{ *DefaultToolCLI }
139+
140+
// --- DestroyCLIInterface ---
141+
func (a *DestroyCLIAdapter) LoadProjectNameWithFallback(ctx context.Context, loader cliClient.Loader, provider cliClient.Provider) (string, error) {
142+
return cliClient.LoadProjectNameWithFallback(ctx, loader, provider)
143+
}
144+
func (a *DestroyCLIAdapter) ConfigureLoader(request mcp.CallToolRequest) cliClient.Loader {
145+
return configureLoader(request)
146+
}
147+
148+
// --- SetConfigCLIInterface ---
149+
func (a *SetConfigCLIAdapter) NewProvider(ctx context.Context, providerId cliClient.ProviderID, client cliClient.FabricClient) (cliClient.Provider, error) {
150+
return a.DefaultToolCLI.NewProviderFabric(ctx, providerId, client)
151+
}
152+
func (a *SetConfigCLIAdapter) ConfigSet(ctx context.Context, projectName string, provider cliClient.Provider, name, value string) error {
153+
return a.DefaultToolCLI.ConfigSet(ctx, projectName, provider, name, value)
154+
}
155+
func (a *SetConfigCLIAdapter) LoadProjectNameWithFallback(ctx context.Context, loader cliClient.Loader, provider cliClient.Provider) (string, error) {
156+
return cliClient.LoadProjectNameWithFallback(ctx, loader, provider)
157+
}
158+
func (a *SetConfigCLIAdapter) ConfigureLoader(request mcp.CallToolRequest) cliClient.Loader {
159+
return a.DefaultToolCLI.ConfigureLoader(request)
160+
}
161+
162+
// --- RemoveConfigCLIInterface ---
163+
func (a *RemoveConfigCLIAdapter) ConfigureLoader(request mcp.CallToolRequest) cliClient.Loader {
164+
return configureLoader(request)
165+
}
166+
func (a *RemoveConfigCLIAdapter) LoadProjectNameWithFallback(ctx context.Context, loader cliClient.Loader, provider cliClient.Provider) (string, error) {
167+
return cliClient.LoadProjectNameWithFallback(ctx, loader, provider)
168+
}
169+
func (a *RemoveConfigCLIAdapter) ConfigDelete(ctx context.Context, projectName string, provider cliClient.Provider, name string) error {
170+
return cli.ConfigDelete(ctx, projectName, provider, name)
171+
}
172+
173+
// --- ListConfigCLIInterface ---
174+
func (a *ListConfigCLIAdapter) ListConfig(ctx context.Context, provider cliClient.Provider, projectName string) (*defangv1.Secrets, error) {
175+
return a.DefaultToolCLI.ListConfig(ctx, provider, projectName)
176+
}
177+
178+
// --- LoginCLIInterface ---
179+
func (a *LoginCLIAdapter) InteractiveLoginMCP(ctx context.Context, client *cliClient.GrpcClient, cluster string) error {
180+
// Delegate to login.InteractiveLoginMCP from the login package
181+
return login.InteractiveLoginMCP(ctx, client, cluster)
182+
}
183+
func (a *LoginCLIAdapter) GenerateAuthURL(authPort int) string {
184+
// Use the same logic as the old DefaultLoginCLI
185+
return "Please open this URL in your browser: http://127.0.0.1:" + strconv.Itoa(authPort) + " to login"
186+
}

0 commit comments

Comments
 (0)