Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pkg/mcp/tests/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ var expectedToolsList = []string{
"services",
"deploy",
"destroy",
"logs",
"estimate",
"set_config",
"remove_config",
Expand Down
4 changes: 4 additions & 0 deletions src/pkg/mcp/tools/default_tool_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func (DefaultToolCLI) ComposeUp(ctx context.Context, project *compose.Project, c
return cli.ComposeUp(ctx, project, client, provider, uploadMode, mode)
}

func (c *DefaultToolCLI) Tail(ctx context.Context, provider cliClient.Provider, project *compose.Project, options cli.TailOptions) error {
return cli.Tail(ctx, provider, project.Name, options)
}

func (DefaultToolCLI) ConfigureLoader(request mcp.CallToolRequest) cliClient.Loader {
return configureLoader(request)
}
Expand Down
11 changes: 11 additions & 0 deletions src/pkg/mcp/tools/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package tools
import (
"context"

cliTypes "github.com/DefangLabs/defang/src/pkg/cli"
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/cli/compose"
"github.com/DefangLabs/defang/src/pkg/mcp/deployment_info"
Expand Down Expand Up @@ -40,6 +41,16 @@ type DeployCLIInterface interface {
OpenBrowser(url string) error
}

type LogsCLIInterface interface {
Connecter
ProviderFactory
LoaderConfigurator
// Unique methods
Tail(ctx context.Context, provider cliClient.Provider, project *compose.Project, options cliTypes.TailOptions) error
CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, providerId cliClient.ProviderID, projectName string, serviceCount int) (cliClient.Provider, error)
LoadProject(ctx context.Context, loader cliClient.Loader) (*compose.Project, error)
}

type DestroyCLIInterface interface {
Connecter
ProviderFactory
Expand Down
115 changes: 115 additions & 0 deletions src/pkg/mcp/tools/tail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package tools

import (
"context"
"errors"
"fmt"
"os"
"time"

"github.com/DefangLabs/defang/src/pkg/cli"
cliTypes "github.com/DefangLabs/defang/src/pkg/cli"
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/cli/compose"
"github.com/DefangLabs/defang/src/pkg/term"
"github.com/DefangLabs/defang/src/pkg/track"
"github.com/mark3labs/mcp-go/mcp"
)

// DefaultLogsCLI implements LogsCLIInterface using actual CLI functions
type DefaultLogsCLI struct{}

func (c *DefaultLogsCLI) Connect(ctx context.Context, cluster string) (*cliClient.GrpcClient, error) {
return cli.Connect(ctx, cluster)
}

func (c *DefaultLogsCLI) NewProvider(ctx context.Context, providerId cliClient.ProviderID, client *cliClient.GrpcClient) (cliClient.Provider, error) {
return cli.NewProvider(ctx, providerId, client)
}

func (c *DefaultLogsCLI) Tail(ctx context.Context, provider cliClient.Provider, project *compose.Project, options cliTypes.TailOptions) error {
return cli.Tail(ctx, provider, project.Name, options)
}

func (c *DefaultLogsCLI) CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, providerId cliClient.ProviderID, projectName string, serviceCount int) (cliClient.Provider, error) {
return CheckProviderConfigured(ctx, client, providerId, projectName, serviceCount)
}

func (c *DefaultLogsCLI) LoadProject(ctx context.Context, loader cliClient.Loader) (*compose.Project, error) {
return loader.LoadProject(ctx)
}

func (c *DefaultLogsCLI) ConfigureLoader(request mcp.CallToolRequest) cliClient.Loader {
return configureLoader(request)
}

func handleLogsTool(ctx context.Context, request mcp.CallToolRequest, cluster string, providerId *cliClient.ProviderID, cli LogsCLIInterface) (*mcp.CallToolResult, error) {
term.Debug("Tail tool called - opening logs in browser")
track.Evt("MCP Tail Tool")
wd, err := request.RequireString("working_directory")
if err != nil || wd == "" {
term.Error("Invalid working directory", "error", errors.New("working_directory is required"))
return mcp.NewToolResultErrorFromErr("Invalid working directory", errors.New("working_directory is required")), err
}
deploymentId, err := request.RequireString("deployment_id")
if err != nil || deploymentId == "" {
term.Error("Invalid deployment ID", "error", errors.New("deployment_id is required"))
return mcp.NewToolResultErrorFromErr("Invalid deployment ID", errors.New("deployment_id is required")), err
}
since := request.GetString("since", "")
until := request.GetString("until", "")
sinceTime, err := time.Parse(time.RFC3339, since)
if err != nil {
term.Error("Invalid parameter 'since', must be in RFC3339 format", "error", err)
return mcp.NewToolResultErrorFromErr("Invalid parameter 'since', must be in RFC3339 format", err), err
}
untilTime, err := time.Parse(time.RFC3339, until)
if err != nil {
term.Error("Invalid parameter 'until', must be in RFC3339 format", "error", err)
return mcp.NewToolResultErrorFromErr("Invalid parameter 'until', must be in RFC3339 format", err), err
}

err = os.Chdir(wd)
if err != nil {
term.Error("Failed to change working directory", "error", err)
return mcp.NewToolResultErrorFromErr("Failed to change working directory", err), err
}

loader := cli.ConfigureLoader(request)

term.Debug("Function invoked: loader.LoadProject")
project, err := cli.LoadProject(ctx, loader)
if err != nil {
err = fmt.Errorf("failed to parse compose file: %w", err)
term.Error("Failed to deploy services", "error", err)

return mcp.NewToolResultText(fmt.Sprintf("Local deployment failed: %v. Please provide a valid compose file path.", err)), err
}

term.Debug("Function invoked: cli.Connect")
client, err := cli.Connect(ctx, cluster)
if err != nil {
return mcp.NewToolResultErrorFromErr("Could not connect", err), err
}

term.Debug("Function invoked: cli.NewProvider")

provider, err := cli.CheckProviderConfigured(ctx, client, *providerId, project.Name, len(project.Services))
if err != nil {
return mcp.NewToolResultErrorFromErr("Provider not configured correctly", err), err
}

err = cli.Tail(ctx, provider, project, cliTypes.TailOptions{
Deployment: deploymentId,
Since: sinceTime,
Until: untilTime,
})

if err != nil {
err = fmt.Errorf("failed to tail logs: %w", err)
term.Error("Failed to tail logs", "error", err)
return mcp.NewToolResultErrorFromErr("Failed to tail logs", err), err
}

return mcp.NewToolResultText("Done"), nil
}
32 changes: 24 additions & 8 deletions src/pkg/mcp/tools/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ package tools
import (
"context"
"strings"
"time"

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

var workingDirectoryOption = mcp.WithString("working_directory",
mcp.Description("Path to project's working directory"),
mcp.Required(),
)

var multipleComposeFilesOptions = mcp.WithArray("compose_file_paths",
Expand Down Expand Up @@ -46,7 +45,6 @@ func CollectTools(cluster string, authPort int, providerId *client.ProviderID) [
Tool: mcp.NewTool("deploy",
mcp.WithDescription("Deploy services using defang"),
workingDirectoryOption,
multipleComposeFilesOptions,
),
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
cli := &DefaultToolCLI{}
Expand All @@ -57,29 +55,47 @@ func CollectTools(cluster string, authPort int, providerId *client.ProviderID) [
Tool: mcp.NewTool("destroy",
mcp.WithDescription("Destroy deployed services for the project in the current working directory"),
workingDirectoryOption,
multipleComposeFilesOptions,
),
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
cli := &DefaultToolCLI{}
track.Evt("MCP Destroy Tool", track.P("provider", *providerId), track.P("cluster", cluster), track.P("client", MCPDevelopmentClient))
return handleDestroyTool(ctx, request, providerId, cluster, cli)
},
},
{
Tool: mcp.NewTool("logs",
mcp.WithDescription("Fetch logs for a deployment."),
workingDirectoryOption,
mcp.WithString("deployment_id",
mcp.Description("The deployment ID for which to fetch logs"),
),
mcp.WithString("since",
mcp.Description("The start time in RFC3339 format (e.g., 2006-01-02T15:04:05Z07:00)"),
mcp.DefaultString(time.Now().Add(-1*time.Hour).Format(time.RFC3339)),
),
mcp.WithString("until",
mcp.Description("The end time in RFC3339 format (e.g., 2006-01-02T15:04:05Z07:00)"),
mcp.DefaultString(time.Now().Format(time.RFC3339)),
),
),
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
cli := &DefaultToolCLI{}
return handleLogsTool(ctx, request, cluster, providerId, cli)
},
},
{
Tool: mcp.NewTool("estimate",
mcp.WithDescription("Estimate the cost of deployed a Defang project."),
workingDirectoryOption,
multipleComposeFilesOptions,
mcp.WithString("provider",
mcp.Description("The cloud provider to estimate costs for. Supported options are AWS or GCP"),
mcp.DefaultString(strings.ToUpper(providerId.String())),
mcp.Enum("AWS", "GCP"),
),

mcp.WithString("deployment_mode",
mcp.Description("The deployment mode for the estimate. Options are AFFORDABLE, BALANCED or HIGH_AVAILABILITY."),
mcp.Description("The deployment mode for the estimate. Options are AFFORDABLE, BALANCED or HIGH AVAILABILITY."),
mcp.DefaultString("AFFORDABLE"),
mcp.Enum("AFFORDABLE", "BALANCED", "HIGH_AVAILABILITY"),
mcp.Enum("AFFORDABLE", "BALANCED", "HIGH AVAILABILITY"),
),
),
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Expand Down
Loading