66 "os"
77 "strings"
88
9+ "github.com/docker/cli/cli/command"
910 "github.com/docker/mcp-gateway/pkg/desktop"
11+ dockerenv "github.com/docker/mcp-gateway/pkg/docker"
1012 "github.com/docker/mcp-gateway/pkg/tui"
1113)
1214
@@ -19,9 +21,12 @@ type SetOpts struct {
1921}
2022
2123func MappingFromSTDIN (ctx context.Context , key string ) (* Secret , error ) {
24+ // Read the entire secret value from STDIN.
25+ // This allows piping values securely without exposing them
26+ // via command-line arguments or shell history.
2227 data , err := tui .ReadAllWithContext (ctx , os .Stdin )
2328 if err != nil {
24- return nil , err
29+ return nil , fmt . Errorf ( "failed to read secret value from STDIN: %w" , err )
2530 }
2631
2732 return & Secret {
@@ -30,36 +35,93 @@ func MappingFromSTDIN(ctx context.Context, key string) (*Secret, error) {
3035 }, nil
3136}
3237
38+ // Secret represents a key/value pair used by the secret management commands.
39+ // The fields are intentionally unexported to avoid accidental exposure
40+ // outside of the secret management package.
3341type Secret struct {
3442 key string
3543 val string
3644}
3745
3846func ParseArg (arg string , opts SetOpts ) (* Secret , error ) {
39- if ! isDirectValueProvider (opts .Provider ) && strings .Contains (arg , "=" ) {
40- return nil , fmt .Errorf ("provider cannot be used with key=value pairs: %s" , arg )
47+ // Direct-value providers expect secrets in the form key=value.
48+ // Non-direct providers only accept the key, with the value
49+ // being resolved by the provider itself.
50+ directProvider := isDirectValueProvider (opts .Provider )
51+
52+ // Reject key=value syntax when the provider does not accept direct values.
53+ if ! directProvider && strings .Contains (arg , "=" ) {
54+ return nil , fmt .Errorf (
55+ "provider %q does not support key=value syntax: %s" ,
56+ opts .Provider , arg ,
57+ )
4158 }
42- if ! isDirectValueProvider (opts .Provider ) {
59+
60+ // For non-direct providers, only the key is required.
61+ if ! directProvider {
4362 return & Secret {key : arg , val : "" }, nil
4463 }
64+
65+ // Split key=value input for direct providers.
4566 parts := strings .SplitN (arg , "=" , 2 )
4667 if len (parts ) != 2 {
47- return nil , fmt .Errorf ("no key=value pair: %s" , arg )
68+ return nil , fmt .Errorf ("expected key=value pair, got : %s" , arg )
4869 }
49- return & Secret {key : parts [0 ], val : parts [1 ]}, nil
70+
71+ return & Secret {
72+ key : parts [0 ],
73+ val : parts [1 ],
74+ }, nil
5075}
5176
5277func isDirectValueProvider (provider string ) bool {
78+ // Direct-value providers receive the secret value directly
79+ // from the CLI input (key=value).
80+ // Currently supported direct providers:
81+ // - empty provider (default)
82+ // - credstore
5383 return provider == "" || provider == Credstore
5484}
5585
5686func Set (ctx context.Context , s Secret , opts SetOpts ) error {
87+ // Handle the credstore provider first.
88+ // This provider does not depend on Docker Desktop or JFS.
5789 if opts .Provider == Credstore {
5890 p := NewCredStoreProvider ()
5991 if err := p .SetSecret (s .key , s .val ); err != nil {
6092 return err
6193 }
6294 }
95+
96+ // Initialize Docker CLI to detect the runtime environment.
97+ // This is required to determine whether Docker Desktop is available.
98+ dockerCli , err := command .NewDockerCli ()
99+ if err != nil {
100+ return fmt .Errorf ("failed to create Docker CLI: %w" , err )
101+ }
102+
103+ if err := dockerCli .Initialize (nil ); err != nil {
104+ return fmt .Errorf ("failed to initialize Docker CLI: %w" , err )
105+ }
106+
107+ // Detect if we are running on Docker Engine (non-Desktop).
108+ // On headless Docker Engine setups, Docker Desktop services
109+ // (including the JFS secrets backend) are not available.
110+ isCE , err := dockerenv .RunningInDockerCE (ctx , dockerCli )
111+ if err != nil {
112+ return err
113+ }
114+
115+ if isCE {
116+ return fmt .Errorf (
117+ "Docker Desktop is not available. " +
118+ "`docker mcp secret set` requires Docker Desktop to manage secrets. " +
119+ "If you are running Docker Engine in a headless environment, " +
120+ "use --secrets with a .env file instead." ,
121+ )
122+ }
123+
124+ // Docker Desktop is available: proceed with the JFS-backed secrets client.
63125 return desktop .NewSecretsClient ().SetJfsSecret (ctx , desktop.Secret {
64126 Name : s .key ,
65127 Value : s .val ,
@@ -68,14 +130,22 @@ func Set(ctx context.Context, s Secret, opts SetOpts) error {
68130}
69131
70132func IsValidProvider (provider string ) bool {
133+ // An empty provider is valid and represents the default behavior.
71134 if provider == "" {
72135 return true
73136 }
137+
138+ // OAuth-based providers are identified by the "oauth/" prefix.
139+ // The concrete provider implementation is resolved at runtime.
74140 if strings .HasPrefix (provider , "oauth/" ) {
75141 return true
76142 }
143+
144+ // Credstore is a built-in provider that stores secrets locally.
77145 if provider == Credstore {
78146 return true
79147 }
148+
149+ // Any other provider value is considered invalid.
80150 return false
81151}
0 commit comments