@@ -46,8 +46,8 @@ type CookedConfig struct {
// MCPConfig contains MCP transport configuration.
type MCPConfig struct {
- HTTPAddr string `toml:"http_addr"`
- HTTPToken string `toml:"http_token"`
+ HTTPAddr string `toml:"http_addr"`
+ HTTPToken *string `toml:"http_token"`
}
// Config is fully resolved runtime configuration.
@@ -99,12 +99,17 @@ func Load(ctx context.Context, path string, resolver *configvalue.Resolver) (Con
if envHTTPAddr := os.Getenv("COOKED_MCP_HTTP_ADDR"); envHTTPAddr != "" {
httpAddr = envHTTPAddr
}
+ httpToken, err := httpToken(ctx, resolver, fileConfig.MCP.HTTPToken)
+ if err != nil {
+ return Config{}, err
+ }
return Config{
- Username: username,
- Password: password,
- BaseURL: baseURL,
- HTTPAddr: httpAddr,
+ Username: username,
+ Password: password,
+ BaseURL: baseURL,
+ HTTPAddr: httpAddr,
+ HTTPToken: httpToken,
}, nil
}
@@ -197,6 +202,29 @@ func baseURL(ctx context.Context, resolver *configvalue.Resolver, config string)
return parsed, nil
}
+func httpToken(ctx context.Context, resolver *configvalue.Resolver, config *string) (string, error) {
+ if value, ok := os.LookupEnv("COOKED_MCP_HTTP_TOKEN"); ok {
+ if value == "" {
+ return "", fmt.Errorf("MCP HTTP token must not be empty")
+ }
+
+ return value, nil
+ }
+ if config == nil {
+ return "", nil
+ }
+
+ value, err := resolver.ResolveOrThrow(ctx, *config, "MCP HTTP token")
+ if err != nil {
+ return "", err
+ }
+ if value == "" {
+ return "", fmt.Errorf("MCP HTTP token must not be empty")
+ }
+
+ return value, nil
+}
+
func isLoopbackHost(host string) bool {
if host == "localhost" {
return true
@@ -120,6 +120,91 @@ http_addr = "127.0.0.1:9000"
}
}
+// server.SECURITY.4 server.SECURITY.9
+func TestLoadACIDServerSecurity4And9EnvironmentOverridesTOMLHTTPToken(t *testing.T) {
+ t.Setenv("COOKED_MCP_HTTP_TOKEN", "env-token")
+
+ configPath := filepath.Join(t.TempDir(), "config.toml")
+ writeConfig(t, configPath, `[user]
+name = "toml-user"
+password = "toml-password"
+
+[mcp]
+http_token = "toml-token"
+`)
+
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ if got.HTTPToken != "env-token" {
+ t.Fatalf("HTTPToken = %q, want environment token", got.HTTPToken)
+ }
+}
+
+// server.SECURITY.5 server.SECURITY.10 server.SECURITY.12 server.SECURITY.13
+func TestLoadACIDServerSecurity5And10ResolvesTOMLHTTPTokenConfigValue(t *testing.T) {
+ t.Setenv("TEST_HTTP_TOKEN", "shell-token")
+
+ configPath := filepath.Join(t.TempDir(), "config.toml")
+ writeConfig(t, configPath, `[user]
+name = "toml-user"
+password = "toml-password"
+
+[mcp]
+http_token = "$TEST_HTTP_TOKEN"
+`)
+
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ if got.HTTPToken != "shell-token" {
+ t.Fatalf("HTTPToken = %q, want resolved TOML token", got.HTTPToken)
+ }
+}
+
+// server.SECURITY.11
+func TestLoadACIDServerSecurity11RejectsEmptyEnvironmentHTTPToken(t *testing.T) {
+ t.Setenv("COOKED_MCP_HTTP_TOKEN", "")
+
+ configPath := filepath.Join(t.TempDir(), "config.toml")
+ writeConfig(t, configPath, `[user]
+name = "toml-user"
+password = "toml-password"
+`)
+
+ _, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ if err == nil {
+ t.Fatal("Load() error = nil, want empty HTTP token error")
+ }
+ if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") {
+ t.Fatalf("Load() error = %v, want empty HTTP token error", err)
+ }
+}
+
+// server.SECURITY.11
+func TestLoadACIDServerSecurity11RejectsEmptyTOMLHTTPToken(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.toml")
+ writeConfig(t, configPath, `[user]
+name = "toml-user"
+password = "toml-password"
+
+[mcp]
+http_token = ""
+`)
+
+ _, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ if err == nil {
+ t.Fatal("Load() error = nil, want empty HTTP token error")
+ }
+ if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") {
+ t.Fatalf("Load() error = %v, want empty HTTP token error", err)
+ }
+}
+
func TestLoadRejectsMissingExplicitConfigFile(t *testing.T) {
_, err := Load(context.Background(), filepath.Join(t.TempDir(), "missing.toml"), configvalue.NewResolver())
if err == nil {