|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "path/filepath" |
| 7 | + "strings" |
| 8 | + |
| 9 | + "github.com/localstack/lstk/internal/env" |
| 10 | + "github.com/localstack/lstk/internal/mcpconfig" |
| 11 | + "github.com/localstack/lstk/internal/output" |
| 12 | + "github.com/localstack/lstk/internal/ui" |
| 13 | + "github.com/spf13/cobra" |
| 14 | +) |
| 15 | + |
| 16 | +func newMCPCmd(cfg *env.Env) *cobra.Command { |
| 17 | + cmd := &cobra.Command{ |
| 18 | + Use: "mcp", |
| 19 | + Short: "Manage the LocalStack MCP server integration", |
| 20 | + Long: "Manage the LocalStack Model Context Protocol (MCP) server integration so coding agents can drive LocalStack. Use 'lstk mcp init' to configure your installed MCP clients.", |
| 21 | + } |
| 22 | + cmd.AddCommand(newMCPInitCmd(cfg)) |
| 23 | + return cmd |
| 24 | +} |
| 25 | + |
| 26 | +func newMCPInitCmd(cfg *env.Env) *cobra.Command { |
| 27 | + var ( |
| 28 | + method string |
| 29 | + token string |
| 30 | + imageTag string |
| 31 | + cacheDir string |
| 32 | + workspace string |
| 33 | + clients []string |
| 34 | + extraEnv []string |
| 35 | + ) |
| 36 | + |
| 37 | + cmd := &cobra.Command{ |
| 38 | + Use: "init", |
| 39 | + Short: "Configure MCP clients to use the LocalStack MCP server", |
| 40 | + Long: "Configure your installed MCP clients (Cursor, Claude Code, Claude Desktop, VS Code, Codex) to launch the LocalStack MCP server. Defaults to running the server in Docker (with access to your Docker socket so it can manage LocalStack containers), so no Node toolchain is required; use --method npx to run it via Node instead. The auth token is reused from your environment or 'lstk login'. By default every detected client is configured; pass --client to narrow the selection.", |
| 41 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 42 | + resolvedToken := token |
| 43 | + if resolvedToken == "" { |
| 44 | + resolvedToken = cfg.AuthToken |
| 45 | + } |
| 46 | + |
| 47 | + parsedEnv, err := parseEnvAssignments(extraEnv) |
| 48 | + if err != nil { |
| 49 | + return err |
| 50 | + } |
| 51 | + |
| 52 | + resolvedCacheDir := cacheDir |
| 53 | + if resolvedCacheDir == "" { |
| 54 | + home, err := os.UserHomeDir() |
| 55 | + if err != nil { |
| 56 | + return fmt.Errorf("could not resolve home directory: %w", err) |
| 57 | + } |
| 58 | + resolvedCacheDir = filepath.Join(home, ".localstack-mcp") |
| 59 | + } |
| 60 | + |
| 61 | + opts := mcpconfig.Options{ |
| 62 | + Token: resolvedToken, |
| 63 | + Method: mcpconfig.Method(method), |
| 64 | + ExtraEnv: parsedEnv, |
| 65 | + ClientIDs: clients, |
| 66 | + Docker: mcpconfig.DockerOptions{ |
| 67 | + CacheDir: resolvedCacheDir, |
| 68 | + WorkspaceDir: workspace, |
| 69 | + ImageTag: imageTag, |
| 70 | + }, |
| 71 | + } |
| 72 | + |
| 73 | + if isInteractiveMode(cfg) { |
| 74 | + return ui.RunMCPInit(cmd.Context(), opts) |
| 75 | + } |
| 76 | + return mcpconfig.RunInit(cmd.Context(), output.NewPlainSink(os.Stdout), opts) |
| 77 | + }, |
| 78 | + } |
| 79 | + |
| 80 | + cmd.Flags().StringVar(&method, "method", string(mcpconfig.MethodDocker), "How clients launch the server: docker or npx") |
| 81 | + cmd.Flags().StringSliceVar(&clients, "client", nil, "MCP clients to configure (default: all detected); repeatable or comma-separated: "+strings.Join(mcpconfig.SupportedClientIDs(), ", ")) |
| 82 | + cmd.Flags().StringVar(&token, "token", "", "LocalStack auth token (default: from environment or 'lstk login')") |
| 83 | + cmd.Flags().StringVar(&imageTag, "image-tag", "latest", "Docker image tag for the MCP server (docker method)") |
| 84 | + cmd.Flags().StringVar(&cacheDir, "cache-dir", "", "Host directory for the server's cache (docker method; default: ~/.localstack-mcp)") |
| 85 | + cmd.Flags().StringVar(&workspace, "workspace", "", "Host directory to mount into the server so its IaC tools can see your project (docker method; default: none)") |
| 86 | + // StringArray (not StringSlice) so values containing commas — e.g. |
| 87 | + // SERVICES=s3,sqs,lambda — are kept verbatim instead of being split. |
| 88 | + cmd.Flags().StringArrayVar(&extraEnv, "config", nil, "Extra LocalStack env var forwarded to the server, as KEY=VALUE; repeat the flag for multiple") |
| 89 | + |
| 90 | + return cmd |
| 91 | +} |
| 92 | + |
| 93 | +// parseEnvAssignments parses KEY=VALUE pairs into a map, erroring on malformed input. |
| 94 | +func parseEnvAssignments(pairs []string) (map[string]string, error) { |
| 95 | + if len(pairs) == 0 { |
| 96 | + return nil, nil |
| 97 | + } |
| 98 | + out := make(map[string]string, len(pairs)) |
| 99 | + for _, pair := range pairs { |
| 100 | + key, value, found := strings.Cut(pair, "=") |
| 101 | + if !found || key == "" { |
| 102 | + return nil, fmt.Errorf("invalid --config %q: expected KEY=VALUE", pair) |
| 103 | + } |
| 104 | + out[key] = value |
| 105 | + } |
| 106 | + return out, nil |
| 107 | +} |
0 commit comments