From 7435f6425b577195dc4c2fb78120ebd3bb370e5a Mon Sep 17 00:00:00 2001 From: Simon KP Date: Mon, 16 Mar 2026 12:40:42 +1100 Subject: [PATCH] fix: resolve --host flag in MCP serve mode MCP serve mode ignored the --host flag for API requests because tools.go and schemas.go called ResolveHost(nil, cfg) with a nil cobra command, making the flag invisible. All API URLs resolved to the default prod host regardless of --host value. Add BaseURL func to Factory that uses the same priority chain as HTTPClient (flag > env > config > default). Replace inline ResolveHost calls in serve mode with f.BaseURL(). --- cmd/kh/main.go | 38 ++++++++++++++++++++++++++++---------- cmd/serve/schemas.go | 7 +------ cmd/serve/serve_test.go | 1 + cmd/serve/tools.go | 15 ++------------- pkg/cmdutil/factory.go | 5 +++++ 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/cmd/kh/main.go b/cmd/kh/main.go index 71c02ca..4a95b18 100644 --- a/cmd/kh/main.go +++ b/cmd/kh/main.go @@ -20,28 +20,46 @@ func main() { // rootCmd is created first so the HTTPClient closure can read --host after flag parsing. var rootCmd *cobra.Command + // resolveActiveHost returns the effective host using the priority chain: + // --host flag > KH_HOST env > hosts.yml default > config.yml > built-in default. + resolveActiveHost := func() string { + var flagHost string + if rootCmd != nil { + if f := rootCmd.PersistentFlags().Lookup("host"); f != nil { + flagHost = f.Value.String() + } + } + envHost := os.Getenv("KH_HOST") + hosts, err := config.ReadHosts() + if err != nil { + if flagHost != "" { + return flagHost + } + if envHost != "" { + return envHost + } + return "app.keeperhub.com" + } + return hosts.ActiveHost(flagHost, envHost) + } + f := &cmdutil.Factory{ AppVersion: version.Version, IOStreams: ios, Config: func() (config.Config, error) { return config.ReadConfig() }, + BaseURL: func() string { + return khhttp.BuildBaseURL(resolveActiveHost()) + }, HTTPClient: func() (*khhttp.Client, error) { + activeHost := resolveActiveHost() + hosts, err := config.ReadHosts() if err != nil { return nil, err } - // Priority: --host flag > KH_HOST env > hosts.yml default > built-in default - var flagHost string - if rootCmd != nil { - if f := rootCmd.PersistentFlags().Lookup("host"); f != nil { - flagHost = f.Value.String() - } - } - envHost := os.Getenv("KH_HOST") - activeHost := hosts.ActiveHost(flagHost, envHost) - // Resolve token using the auth chain: KH_API_KEY > keyring > hosts.yml resolved, err := auth.ResolveToken(activeHost) if err != nil { diff --git a/cmd/serve/schemas.go b/cmd/serve/schemas.go index 96de4c5..2ddb1d0 100644 --- a/cmd/serve/schemas.go +++ b/cmd/serve/schemas.go @@ -38,12 +38,7 @@ func fetchMCPSchemas(f *cmdutil.Factory) (*SchemasResponse, error) { return nil, fmt.Errorf("creating HTTP client: %w", err) } - cfg, err := f.Config() - if err != nil { - return nil, fmt.Errorf("reading config: %w", err) - } - - url := khhttp.BuildBaseURL(cmdutil.ResolveHost(nil, cfg)) + "/api/mcp/schemas" + url := f.BaseURL() + "/api/mcp/schemas" req, err := client.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("building request: %w", err) diff --git a/cmd/serve/serve_test.go b/cmd/serve/serve_test.go index 82628a7..a182749 100644 --- a/cmd/serve/serve_test.go +++ b/cmd/serve/serve_test.go @@ -46,6 +46,7 @@ func newServeFactory(server *httptest.Server, ios *iostreams.IOStreams) *cmdutil IOStreams: ios, HTTPClient: func() (*khhttp.Client, error) { return client, nil }, Config: func() (config.Config, error) { return config.Config{DefaultHost: server.URL}, nil }, + BaseURL: func() string { return server.URL }, } } diff --git a/cmd/serve/tools.go b/cmd/serve/tools.go index d5ae677..1cd9292 100644 --- a/cmd/serve/tools.go +++ b/cmd/serve/tools.go @@ -9,7 +9,6 @@ import ( "net/http" "strings" - khhttp "github.com/keeperhub/cli/internal/http" "github.com/keeperhub/cli/pkg/cmdutil" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -89,12 +88,7 @@ func MakeToolHandler(f *cmdutil.Factory, actionType string) mcp.ToolHandler { return nil, fmt.Errorf("creating HTTP client: %w", err) } - cfg, err := f.Config() - if err != nil { - return nil, fmt.Errorf("reading config: %w", err) - } - - url := khhttp.BuildBaseURL(cmdutil.ResolveHost(nil, cfg)) + "/api/execute/" + actionType + url := f.BaseURL() + "/api/execute/" + actionType httpReq, err := client.NewRequest(http.MethodPost, url, bytes.NewReader(bodyBytes)) if err != nil { return nil, fmt.Errorf("building request: %w", err) @@ -163,12 +157,7 @@ func makeStaticHandler( return nil, fmt.Errorf("creating HTTP client: %w", err) } - cfg, err := f.Config() - if err != nil { - return nil, fmt.Errorf("reading config: %w", err) - } - - baseURL := khhttp.BuildBaseURL(cmdutil.ResolveHost(nil, cfg)) + baseURL := f.BaseURL() targetURL := buildURL(args, baseURL) var body io.Reader diff --git a/pkg/cmdutil/factory.go b/pkg/cmdutil/factory.go index bf32a80..039f930 100644 --- a/pkg/cmdutil/factory.go +++ b/pkg/cmdutil/factory.go @@ -20,6 +20,11 @@ type Factory struct { // The client automatically injects version headers and per-host credentials. HTTPClient func() (*khhttp.Client, error) + // BaseURL returns the resolved base URL for API requests, accounting for + // --host flag, KH_HOST env, and config defaults. Use this instead of + // ResolveHost when a cobra.Command is not available (e.g. MCP serve mode). + BaseURL func() string + // IOStreams provides the standard input/output streams. IOStreams *iostreams.IOStreams }