@@ -22,6 +22,12 @@ import (
var version = "dev"
+type runOptions struct {
+ configPath string
+ transport string
+ httpAddr string
+}
+
func init() {
buildVersion := builtVersion()
if buildVersion != "" {
@@ -31,24 +37,34 @@ func init() {
func main() {
if err := run(); err != nil {
+ if errors.Is(err, flag.ErrHelp) {
+ os.Exit(0)
+ }
+
fmt.Fprintf(os.Stderr, "cooked-mcp: %v\n", err)
os.Exit(1)
}
}
func run() error {
- configPath := flag.String("config", "", "path to TOML configuration file")
- transport := flag.String("transport", "stdio", "MCP transport: stdio")
- flag.Parse()
+ options, err := parseRunOptions(os.Args[1:])
+ if err != nil {
+ return err
+ }
- if err := validateTransport(*transport); err != nil {
+ if err := validateTransport(options.transport); err != nil {
return err
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
- config, err := appconfig.Load(ctx, *configPath, configvalue.NewResolver())
+ config, err := appconfig.Load(
+ ctx,
+ options.configPath,
+ configvalue.NewResolver(),
+ appconfig.LoadOptions{HTTPAddr: options.httpAddr},
+ )
if err != nil {
return err
}
@@ -69,6 +85,21 @@ func run() error {
return nil
}
+func parseRunOptions(args []string) (runOptions, error) {
+ flagSet := flag.NewFlagSet("cooked-mcp", flag.ContinueOnError)
+
+ var options runOptions
+ flagSet.StringVar(&options.configPath, "config", "", "path to TOML configuration file")
+ flagSet.StringVar(&options.transport, "transport", "stdio", "MCP transport: stdio")
+ flagSet.StringVar(&options.httpAddr, "http-addr", "", "MCP HTTP listen address")
+
+ if err := flagSet.Parse(args); err != nil {
+ return runOptions{}, err
+ }
+
+ return options, nil
+}
+
func validateTransport(transport string) error {
if transport == "stdio" {
return nil
@@ -23,7 +23,7 @@ name = "$COOKED_USERNAME_FROM_MY_SHELL"
password = "!printf shell-password"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -46,7 +46,7 @@ name = "toml-user"
password = "toml-password"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -66,7 +66,7 @@ name = "toml-user"
password = "toml-password"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -87,7 +87,7 @@ password = "toml-password"
http_addr = "127.0.0.1:9000"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -110,7 +110,7 @@ password = "toml-password"
http_addr = "127.0.0.1:9000"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -120,6 +120,34 @@ http_addr = "127.0.0.1:9000"
}
}
+// server.CONFIG.8
+func TestLoadACIDServerConfig8HTTPAddrOverridesEnvironmentAndTOML(t *testing.T) {
+ t.Setenv("COOKED_MCP_HTTP_ADDR", "127.0.0.1:9001")
+
+ configPath := filepath.Join(t.TempDir(), "config.toml")
+ writeConfig(t, configPath, `[user]
+name = "toml-user"
+password = "toml-password"
+
+[mcp]
+http_addr = "127.0.0.1:9000"
+`)
+
+ got, err := Load(
+ context.Background(),
+ configPath,
+ configvalue.NewResolver(),
+ LoadOptions{HTTPAddr: "127.0.0.1:9002"},
+ )
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ if got.HTTPAddr != "127.0.0.1:9002" {
+ t.Fatalf("HTTPAddr = %q, want explicit address", got.HTTPAddr)
+ }
+}
+
// server.SECURITY.4 server.SECURITY.9
func TestLoadACIDServerSecurity4And9EnvironmentOverridesTOMLHTTPToken(t *testing.T) {
t.Setenv("COOKED_MCP_HTTP_TOKEN", "env-token")
@@ -133,7 +161,7 @@ password = "toml-password"
http_token = "toml-token"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -156,7 +184,7 @@ password = "toml-password"
http_token = "$TEST_HTTP_TOKEN"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -176,7 +204,7 @@ name = "toml-user"
password = "toml-password"
`)
- _, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ _, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err == nil {
t.Fatal("Load() error = nil, want empty HTTP token error")
}
@@ -196,7 +224,7 @@ password = "toml-password"
http_token = ""
`)
- _, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ _, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err == nil {
t.Fatal("Load() error = nil, want empty HTTP token error")
}
@@ -227,7 +255,7 @@ password = "toml-password"
http_addr = "`+tt.addr+`"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -252,7 +280,7 @@ password = "toml-password"
http_addr = "0.0.0.0:8123"
`)
- _, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ _, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err == nil {
t.Fatal("Load() error = nil, want non-loopback HTTP token error")
}
@@ -273,7 +301,7 @@ http_addr = "0.0.0.0:8123"
http_token = "secret-token"
`)
- got, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err != nil {
t.Fatalf("Load() error = %v", err)
}
@@ -285,8 +313,35 @@ http_token = "secret-token"
}
}
+// server.SECURITY.1
+func TestLoadACIDServerSecurity1RejectsNonLoopbackHTTPAddrOverrideWithoutToken(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.toml")
+ writeConfig(t, configPath, `[user]
+name = "toml-user"
+password = "toml-password"
+`)
+
+ _, err := Load(
+ context.Background(),
+ configPath,
+ configvalue.NewResolver(),
+ LoadOptions{HTTPAddr: "0.0.0.0:8123"},
+ )
+ if err == nil {
+ t.Fatal("Load() error = nil, want non-loopback HTTP token error")
+ }
+ if !strings.Contains(err.Error(), "MCP HTTP token is required") {
+ t.Fatalf("Load() error = %v, want HTTP token required error", err)
+ }
+}
+
func TestLoadRejectsMissingExplicitConfigFile(t *testing.T) {
- _, err := Load(context.Background(), filepath.Join(t.TempDir(), "missing.toml"), configvalue.NewResolver())
+ _, err := Load(
+ context.Background(),
+ filepath.Join(t.TempDir(), "missing.toml"),
+ configvalue.NewResolver(),
+ LoadOptions{},
+ )
if err == nil {
t.Fatal("Load() error = nil, want missing config error")
}
@@ -303,7 +358,7 @@ func TestLoadACIDServerConfig72RejectsMissingDefaultConfigHome(t *testing.T) {
userHomeDir = originalUserHomeDir
})
- _, err := Load(context.Background(), "", configvalue.NewResolver())
+ _, err := Load(context.Background(), "", configvalue.NewResolver(), LoadOptions{})
if err == nil {
t.Fatal("Load() error = nil, want default config path error")
}
@@ -322,7 +377,7 @@ password = "toml-password"
base_url = "http://example.com"
`)
- _, err := Load(context.Background(), configPath, configvalue.NewResolver())
+ _, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{})
if err == nil {
t.Fatal("Load() error = nil, want non-local HTTP base URL error")
}