Skip to content
Draft
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
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Note: Integration tests require `LOCALSTACK_AUTH_TOKEN` environment variable for
- `update/` - Self-update logic: version check via GitHub API, binary/Homebrew/npm update paths, archive extraction
- `log/` - Internal diagnostic logging (not for user-facing output — use `output/` for that)
- `iac/` - Wrappers for third-party infrastructure as code tools, such as Terraform and CDK.
- `mcpconfig/` - Native configuration of MCP clients (Cursor, Claude Code, VS Code, ...) to launch the LocalStack MCP server. Domain logic for `lstk mcp init`.

# Logging

Expand Down Expand Up @@ -97,6 +98,18 @@ A REF is parsed by helpers in `internal/snapshot/destination.go`:

`ParseDestination` (save), `ParseSource` (load), and `ParseRemovable` (remove) share pod-name validation; `ParseRemovable` rejects local paths so the CLI cannot delete local files.

# MCP Integration

`lstk mcp init` configures installed MCP clients to launch the LocalStack MCP server (`@localstack/localstack-mcp-server`) so coding agents can drive LocalStack. Domain logic lives in `internal/mcpconfig/`; `cmd/mcp.go` is wiring + output-mode selection. `lstk mcp` is a namespace parent (matching `claude mcp`/`gemini mcp`/`codex mcp` conventions); bare `lstk mcp` prints help.

This is a native Go reimplementation of the standalone setup wizard shipped in the `localstack-mcp-server` repo — NOT a wrapper around `npx … init`. The rationale: lstk is a self-contained Go binary that has never required Node, and the users most likely to run `lstk mcp init` (Homebrew/raw-binary installs) often have no Node. Reimplementing natively keeps that property, reuses the auth token lstk already resolves (no token prompt), and matches lstk's output/sink house style. The entry it writes is kept byte-compatible with the npm wizard's (literally named `localstack`, same `LOCALSTACK_AUTH_TOKEN` convention) so the two installers are interchangeable.

- Defaults to **Docker mode** (`command: docker run … localstack/localstack-mcp-server`) so the lstk user needs no Node at all — neither to run init nor to run the server. `--method npx` switches to the host-Node launcher.
- Reuses the resolved auth token (`cfg.AuthToken` from env or keyring); errors early if absent. The command needs no `initConfig`/config.toml.
- Two adapter kinds in `internal/mcpconfig/clients.go`: **file-based** (Cursor, Claude Desktop, VS Code) merge a JSON entry into the client's config (0600, token-bearing); **CLI-managed** (Claude Code, Codex) shell out to the client's own `mcp add` via an injectable `cliRunner`. VS Code uses the divergent top-level `servers` key + `type: "stdio"`. Adding a client = add an adapter to `allAdapters` (OpenCode and Amazon Q/Kiro are intentionally deferred).
- Per-client config paths/schemas were ported from the wizard source; `internal/mcpconfig/paths.go` resolves them per-OS. Limitation: the JSON merge reformats existing files and drops JSONC comments (acceptable for v1).
- By default every detected client is configured; `--client` narrows the selection (and bypasses detection). `--config KEY=VALUE` forwards extra server env; `--cache-dir`/`--workspace`/`--image-tag` tune Docker mode.

# Code Style

- Don't add comments for self-explanatory code. Only comment when the "why" isn't obvious from the code itself.
Expand Down
107 changes: 107 additions & 0 deletions cmd/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package cmd

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/localstack/lstk/internal/env"
"github.com/localstack/lstk/internal/mcpconfig"
"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/ui"
"github.com/spf13/cobra"
)

func newMCPCmd(cfg *env.Env) *cobra.Command {
cmd := &cobra.Command{
Use: "mcp",
Short: "Manage the LocalStack MCP server integration",
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.",
}
cmd.AddCommand(newMCPInitCmd(cfg))
return cmd
}

func newMCPInitCmd(cfg *env.Env) *cobra.Command {
var (
method string
token string
imageTag string
cacheDir string
workspace string
clients []string
extraEnv []string
)

cmd := &cobra.Command{
Use: "init",
Short: "Configure MCP clients to use the LocalStack MCP server",
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.",
RunE: func(cmd *cobra.Command, args []string) error {
resolvedToken := token
if resolvedToken == "" {
resolvedToken = cfg.AuthToken
}

parsedEnv, err := parseEnvAssignments(extraEnv)
if err != nil {
return err
}

resolvedCacheDir := cacheDir
if resolvedCacheDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("could not resolve home directory: %w", err)
}
resolvedCacheDir = filepath.Join(home, ".localstack-mcp")
}

opts := mcpconfig.Options{
Token: resolvedToken,
Method: mcpconfig.Method(method),
ExtraEnv: parsedEnv,
ClientIDs: clients,
Docker: mcpconfig.DockerOptions{
CacheDir: resolvedCacheDir,
WorkspaceDir: workspace,
ImageTag: imageTag,
},
}

if isInteractiveMode(cfg) {
return ui.RunMCPInit(cmd.Context(), opts)
}
return mcpconfig.RunInit(cmd.Context(), output.NewPlainSink(os.Stdout), opts)
},
}

cmd.Flags().StringVar(&method, "method", string(mcpconfig.MethodDocker), "How clients launch the server: docker or npx")
cmd.Flags().StringSliceVar(&clients, "client", nil, "MCP clients to configure (default: all detected); repeatable or comma-separated: "+strings.Join(mcpconfig.SupportedClientIDs(), ", "))
cmd.Flags().StringVar(&token, "token", "", "LocalStack auth token (default: from environment or 'lstk login')")
cmd.Flags().StringVar(&imageTag, "image-tag", "latest", "Docker image tag for the MCP server (docker method)")
cmd.Flags().StringVar(&cacheDir, "cache-dir", "", "Host directory for the server's cache (docker method; default: ~/.localstack-mcp)")
cmd.Flags().StringVar(&workspace, "workspace", "", "Host directory to mount into the server so its IaC tools can see your project (docker method; default: none)")
// StringArray (not StringSlice) so values containing commas — e.g.
// SERVICES=s3,sqs,lambda — are kept verbatim instead of being split.
cmd.Flags().StringArrayVar(&extraEnv, "config", nil, "Extra LocalStack env var forwarded to the server, as KEY=VALUE; repeat the flag for multiple")

return cmd
}

// parseEnvAssignments parses KEY=VALUE pairs into a map, erroring on malformed input.
func parseEnvAssignments(pairs []string) (map[string]string, error) {
if len(pairs) == 0 {
return nil, nil
}
out := make(map[string]string, len(pairs))
for _, pair := range pairs {
key, value, found := strings.Cut(pair, "=")
if !found || key == "" {
return nil, fmt.Errorf("invalid --config %q: expected KEY=VALUE", pair)
}
out[key] = value
}
return out, nil
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func NewRootCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra.C
newStatusCmd(cfg),
newLogsCmd(cfg),
newSetupCmd(cfg),
newMCPCmd(cfg),
newConfigCmd(cfg),
newVolumeCmd(cfg),
newUpdateCmd(cfg),
Expand Down
Loading
Loading