Skip to content

Commit f48ac8e

Browse files
committed
Add mcp init command to configure MCP clients
1 parent b36a616 commit f48ac8e

15 files changed

Lines changed: 1435 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Note: Integration tests require `LOCALSTACK_AUTH_TOKEN` environment variable for
4444
- `update/` - Self-update logic: version check via GitHub API, binary/Homebrew/npm update paths, archive extraction
4545
- `log/` - Internal diagnostic logging (not for user-facing output — use `output/` for that)
4646
- `iac/` - Wrappers for third-party infrastructure as code tools (Terraform, AWS CDK, AWS SAM CLI).
47+
- `mcpconfig/` - Native configuration of MCP clients (Cursor, Claude Code, VS Code, ...) to launch the LocalStack MCP server. Domain logic for `lstk mcp init`.
4748

4849
# Logging
4950

@@ -103,6 +104,18 @@ A REF is parsed by helpers in `internal/snapshot/destination.go`:
103104

104105
`ParseDestination` (save), `ParseSource` (load), `ParseRemovable` (remove), and `ParseShowable` (show) share pod-name validation; `ParseRemovable` and `ParseShowable` reject local paths (via the shared `parseCloudOnly` helper) so those cloud-only commands never touch local files.
105106

107+
# MCP Integration
108+
109+
`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.
110+
111+
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.
112+
113+
- 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.
114+
- Reuses the resolved auth token (`cfg.AuthToken` from env or keyring); errors early if absent. The command needs no `initConfig`/config.toml.
115+
- 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).
116+
- 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).
117+
- 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.
118+
106119
# Code Style
107120

108121
- Don't add comments for self-explanatory code. Only comment when the "why" isn't obvious from the code itself.

cmd/mcp.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func NewRootCmd(cfg *env.Env, tel *telemetry.Client, logger log.Logger) *cobra.C
7878
newStatusCmd(cfg),
7979
newLogsCmd(cfg),
8080
newSetupCmd(cfg),
81+
newMCPCmd(cfg),
8182
newConfigCmd(cfg),
8283
newVolumeCmd(cfg),
8384
newUpdateCmd(cfg),

0 commit comments

Comments
 (0)