From 3210f0ffec3c9d792dfaaee347a6107d76932e9b Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:16:29 +0000 Subject: [PATCH 1/8] Name stdio OAuth clients and isolate auth caches --- cmd/mcp/config.go | 132 +++++++++++++++++++++++++++++++++++++--------- cmd/mcp/mcp.go | 25 ++++++--- 2 files changed, 126 insertions(+), 31 deletions(-) diff --git a/cmd/mcp/config.go b/cmd/mcp/config.go index 589e3d3f..779b4a35 100644 --- a/cmd/mcp/config.go +++ b/cmd/mcp/config.go @@ -35,7 +35,7 @@ type targetSpec struct { section string transport transport fields []configField - stdioArgs []string + clientName string remove []string legacyName string legacyKey string @@ -80,18 +80,17 @@ func appDataPath(home string) string { var targetSpecs = []targetSpec{ {target: TargetCursor, description: "Cursor editor", path: homePath(".cursor", "mcp.json"), section: "mcpServers", transport: http, fields: []configField{{name: "url", value: KernelMCPURL}}, remove: []string{"type"}}, - {target: TargetClaude, description: "Claude Desktop app", path: claudePath, section: "mcpServers", transport: stdio}, + {target: TargetClaude, description: "Claude Desktop app", path: claudePath, section: "mcpServers", transport: stdio, clientName: "Claude Desktop"}, {target: TargetClaudeCode, description: "Claude Code CLI", path: homePath(".claude.json"), section: "mcpServers", transport: http, fields: []configField{{name: "type", value: "http"}, {name: "url", value: KernelMCPURL}}}, - {target: TargetAntigravity, description: "Google Antigravity", path: homePath(".gemini", "config", "mcp_config.json"), section: "mcpServers", transport: stdio, - stdioArgs: []string{"-y", "mcp-remote", KernelMCPURL, "--static-oauth-client-metadata", "{\"client_name\":\"Antigravity\"}"}}, - {target: TargetWindsurf, description: "Windsurf editor", path: homePath(".codeium", "windsurf", "mcp_config.json"), section: "mcpServers", transport: stdio}, + {target: TargetAntigravity, description: "Google Antigravity", path: homePath(".gemini", "config", "mcp_config.json"), section: "mcpServers", transport: stdio, clientName: "Antigravity"}, + {target: TargetWindsurf, description: "Windsurf editor", path: homePath(".codeium", "windsurf", "mcp_config.json"), section: "mcpServers", transport: stdio, clientName: "Windsurf"}, {target: TargetVSCode, description: "Visual Studio Code", path: vsCodePath, section: "servers", transport: http, legacyName: "settings.json", legacyKey: "mcp.servers", fields: []configField{{name: "url", value: KernelMCPURL}, {name: "type", value: "http"}}}, - {target: TargetGoose, description: "Goose AI", path: homePath(".config", "goose", "config.yaml"), transport: stdio, printOnly: true}, + {target: TargetGoose, description: "Goose AI", path: homePath(".config", "goose", "config.yaml"), transport: stdio, clientName: "Goose", printOnly: true}, // Current Zed settings omit source; its settings migrator removes that old field. - {target: TargetZed, description: "Zed editor", path: homePath(".config", "zed", "settings.json"), section: "context_servers", transport: stdio, + {target: TargetZed, description: "Zed editor", path: homePath(".config", "zed", "settings.json"), section: "context_servers", transport: stdio, clientName: "Zed", remove: []string{"source"}}, {target: TargetFx, description: "fx coding agent", path: homePath(".fx", "mcp.json"), section: "mcp", transport: http, fields: []configField{{name: "type", value: "http"}, {name: "url", value: KernelMCPURL}, {name: "oauth", value: map[string]any{}, ifMissing: true, skipWhen: "bearer_token_env"}}}, @@ -126,6 +125,14 @@ func getConfigPath(target Target) (string, error) { return spec.path(home), nil } +func clientCacheDir(target Target) (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %w", err) + } + return filepath.Join(home, ".mcp-auth", "kernel-"+string(target)), nil +} + func GetConfigPath(target Target) (string, error) { return getConfigPath(target) } @@ -140,7 +147,7 @@ func Install(target Target) error { return err } if spec.printOnly { - return installForGoose(path) + return installForGoose(path, spec) } return installConfig(path, spec) } @@ -319,11 +326,7 @@ func mergeConfig(data []byte, spec targetSpec, legacy map[string]json.RawMessage } fields := spec.fields if spec.transport == stdio { - args := spec.stdioArgs - if args == nil { - args = []string{"-y", "mcp-remote", KernelMCPURL} - } - args, err = mergeStdioArgs(kernel, args) + args, err := mergeStdioArgs(kernel, spec.clientName) if err != nil { return nil, err } @@ -367,10 +370,57 @@ func mergeConfig(data []byte, spec targetSpec, legacy map[string]json.RawMessage return nil, err } } + if spec.transport == stdio { + if err := addClientCacheDir(&root, kernelPath, spec.target); err != nil { + return nil, err + } + } return root.Pack(), nil } -func mergeStdioArgs(kernel *hujson.Object, defaults []string) ([]string, error) { +func addClientCacheDir(root *hujson.Value, kernelPath string, target Target) error { + kernel, err := objectAt(root, kernelPath) + if err != nil { + return err + } + if _, exists := member(kernel, "env"); !exists { + if err := patch(root, "add", kernelPath+"/env", map[string]any{}); err != nil { + return err + } + } + env, err := objectAt(root, kernelPath+"/env") + if err != nil { + return err + } + current, exists := member(env, "MCP_REMOTE_CONFIG_DIR") + if exists { + normalized := current.Clone() + normalized.Standardize() + var path string + if err := json.Unmarshal(normalized.Pack(), &path); err != nil { + return fmt.Errorf("invalid MCP_REMOTE_CONFIG_DIR: %w", err) + } + if path != "" { + return nil + } + } + cacheDir, err := clientCacheDir(target) + if err != nil { + return err + } + return patch(root, "add", kernelPath+"/env/MCP_REMOTE_CONFIG_DIR", cacheDir) +} + +func stdioArgs(clientName string) []string { + return []string{"-y", "mcp-remote", KernelMCPURL, "--static-oauth-client-metadata", clientMetadata(clientName)} +} + +func clientMetadata(clientName string) string { + return fmt.Sprintf(`{"client_name":%q}`, clientName) +} + +func mergeStdioArgs(kernel *hujson.Object, clientName string) ([]string, error) { + defaults := stdioArgs(clientName) value, exists := member(kernel, "args") if !exists { return defaults, nil @@ -385,22 +435,54 @@ func mergeStdioArgs(kernel *hujson.Object, defaults []string) ([]string, error) return defaults, nil } args[2] = KernelMCPURL - if len(defaults) > 3 { - flag := defaults[3] - for i := 3; i < len(args); i++ { - if args[i] == flag { - if i+1 >= len(args) { - return nil, fmt.Errorf("kernel args missing value for %s", flag) - } - args[i+1] = defaults[4] - return args, nil - } + flagIndex := -1 + for i := 3; i < len(args); i++ { + if args[i] != defaults[3] { + continue + } + if flagIndex != -1 || i+1 >= len(args) { + return nil, fmt.Errorf("invalid kernel args for %s", defaults[3]) } - args = append(args, defaults[3:]...) + flagIndex = i } + if flagIndex == -1 { + return append(args, defaults[3:]...), nil + } + metadata, err := mergeClientMetadata(args[flagIndex+1], clientName) + if err != nil { + return nil, err + } + args[flagIndex+1] = metadata return args, nil } +func mergeClientMetadata(raw, clientName string) (string, error) { + if strings.HasPrefix(raw, "@") { + return "", fmt.Errorf("kernel OAuth metadata is in a separate file; set client_name to %q there", clientName) + } + if !json.Valid([]byte(raw)) { + return "", fmt.Errorf("invalid kernel OAuth metadata: expected JSON object") + } + metadata, err := hujson.Parse([]byte(raw)) + if err != nil { + return "", fmt.Errorf("invalid kernel OAuth metadata: %w", err) + } + obj, err := objectAt(&metadata, "") + if err != nil { + return "", err + } + if current, exists := member(obj, "client_name"); exists { + var name string + if err := json.Unmarshal(current.Pack(), &name); err == nil && name == clientName { + return raw, nil + } + } + if err := patch(&metadata, "add", "/client_name", clientName); err != nil { + return "", err + } + return string(metadata.Pack()), nil +} + func pointerName(name string) string { return strings.ReplaceAll(strings.ReplaceAll(name, "~", "~0"), "/", "~1") } diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index 1dc4fab2..d5c0ec5c 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -45,21 +45,34 @@ type MCPServerConfig struct { } // installForGoose installs MCP config for Goose (YAML format) -func installForGoose(configPath string) error { +func installForGoose(configPath string, spec targetSpec) error { + cacheDir, err := clientCacheDir(spec.target) + if err != nil { + return err + } // For Goose, we'll output instructions since it uses YAML format // and we don't want to add a YAML dependency pterm.Info.Println("Goose uses YAML configuration. Add the following to your Goose config:") pterm.Println() - fmt.Println(`extensions: + fmt.Println(gooseConfig(spec.clientName, cacheDir)) + pterm.Println() + pterm.Info.Printf("Config file location: %s\n", configPath) + return nil +} + +func gooseConfig(clientName, cacheDir string) string { + return `extensions: kernel: name: Kernel type: stdio + enabled: true cmd: npx args: - -y - mcp-remote - - ` + KernelMCPURL) - pterm.Println() - pterm.Info.Printf("Config file location: %s\n", configPath) - return nil + - ` + KernelMCPURL + ` + - --static-oauth-client-metadata + - '` + clientMetadata(clientName) + `' + envs: + MCP_REMOTE_CONFIG_DIR: ` + fmt.Sprintf("%q", cacheDir) } From 377acb4f69a0f8286b9a2ba0d352b9c881dac3e2 Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:16:29 +0000 Subject: [PATCH 2/8] Cover stdio OAuth metadata and cache migration --- cmd/mcp/config_test.go | 146 +++++++++++++++++++++++++++++++++++++---- cmd/mcp/mcp_test.go | 14 ++++ 2 files changed, 147 insertions(+), 13 deletions(-) diff --git a/cmd/mcp/config_test.go b/cmd/mcp/config_test.go index 694933d0..d5632a44 100644 --- a/cmd/mcp/config_test.go +++ b/cmd/mcp/config_test.go @@ -43,15 +43,16 @@ func TestInstallPreservesTargetConfigs(t *testing.T) { target Target section string stdio bool + name string }{ - {TargetCursor, "mcpServers", false}, - {TargetClaude, "mcpServers", true}, - {TargetClaudeCode, "mcpServers", false}, - {TargetAntigravity, "mcpServers", true}, - {TargetWindsurf, "mcpServers", true}, - {TargetVSCode, "servers", false}, - {TargetZed, "context_servers", true}, - {TargetFx, "mcp", false}, + {TargetCursor, "mcpServers", false, ""}, + {TargetClaude, "mcpServers", true, "Claude Desktop"}, + {TargetClaudeCode, "mcpServers", false, ""}, + {TargetAntigravity, "mcpServers", true, "Antigravity"}, + {TargetWindsurf, "mcpServers", true, "Windsurf"}, + {TargetVSCode, "servers", false, ""}, + {TargetZed, "context_servers", true, "Zed"}, + {TargetFx, "mcp", false, ""}, } if got := AllTargets(); len(got) != len(targets)+1 { t.Fatalf("registered targets = %d, want %d including Goose", len(got), len(targets)+1) @@ -126,8 +127,17 @@ func TestInstallPreservesTargetConfigs(t *testing.T) { if args[0] != "-y" || args[1] != "mcp-remote" || args[2] != KernelMCPURL { t.Fatalf("stdio args = %#v", args) } - if tc.target == TargetAntigravity && (len(args) != 5 || args[3] != "--static-oauth-client-metadata") { - t.Fatalf("Antigravity args = %#v", args) + if len(args) != 5 || args[3] != "--static-oauth-client-metadata" { + t.Fatalf("stdio metadata args = %#v", args) + } + var metadata map[string]string + if err := json.Unmarshal([]byte(args[4].(string)), &metadata); err != nil || metadata["client_name"] != tc.name { + t.Fatalf("stdio client metadata = %#v: %v", metadata, err) + } + cacheDir := kernel["env"].(map[string]any)["MCP_REMOTE_CONFIG_DIR"] + wantDir := filepath.Join(os.Getenv("HOME"), ".mcp-auth", "kernel-"+string(tc.target)) + if cacheDir != wantDir { + t.Fatalf("auth cache = %v, want %s", cacheDir, wantDir) } if tc.target == TargetZed { if _, exists := kernel["source"]; exists { @@ -632,7 +642,8 @@ func TestInstallPreservesMCPRemoteOptions(t *testing.T) { spec, _ := specFor(target) seed := map[string]any{spec.section: map[string]any{"kernel": map[string]any{ "command": "npx", - "args": []string{"-y", "mcp-remote@latest", "https://old.example", "--header-file", "/private/headers.txt"}, + "args": []string{"-y", "mcp-remote@latest", "https://old.example", "--header-file", "/private/headers.txt", "--static-oauth-client-metadata", `{"client_name":"Old","scope":"read","token_endpoint_auth_method":"none"}`}, + "env": map[string]string{"CUSTOM": "keep"}, }}} data, err := json.Marshal(seed) if err != nil { @@ -649,13 +660,122 @@ func TestInstallPreservesMCPRemoteOptions(t *testing.T) { if args[1] != "mcp-remote@latest" || args[2] != KernelMCPURL || args[3] != "--header-file" || args[4] != "/private/headers.txt" { t.Fatalf("custom args lost: %#v", args) } - if target == TargetAntigravity && (len(args) != 7 || args[5] != "--static-oauth-client-metadata") { - t.Fatalf("Antigravity metadata missing: %#v", args) + if len(args) != 7 || args[5] != "--static-oauth-client-metadata" { + t.Fatalf("client metadata missing: %#v", args) + } + var metadata map[string]any + if err := json.Unmarshal([]byte(args[6].(string)), &metadata); err != nil { + t.Fatal(err) + } + if metadata["client_name"] != spec.clientName || metadata["scope"] != "read" || metadata["token_endpoint_auth_method"] != "none" { + t.Fatalf("client metadata changed: %#v", metadata) + } + env := config[spec.section].(map[string]any)["kernel"].(map[string]any)["env"].(map[string]any) + if env["CUSTOM"] != "keep" || env["MCP_REMOTE_CONFIG_DIR"] == nil { + t.Fatalf("stdio environment changed: %#v", env) } }) } } +func TestInstallPreservesCustomClientCache(t *testing.T) { + testHome(t) + path, err := GetConfigPath(TargetClaude) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + seed := []byte(`{"mcpServers":{"kernel":{"env":{"MCP_REMOTE_CONFIG_DIR":"/custom/cache"}}}}`) + if err := os.WriteFile(path, seed, 0600); err != nil { + t.Fatal(err) + } + if err := Install(TargetClaude); err != nil { + t.Fatal(err) + } + env := readTestConfig(t, path)["mcpServers"].(map[string]any)["kernel"].(map[string]any)["env"].(map[string]any) + if env["MCP_REMOTE_CONFIG_DIR"] != "/custom/cache" { + t.Fatalf("custom auth cache changed: %#v", env) + } +} + +func TestInstallReplacesEmptyClientCache(t *testing.T) { + testHome(t) + path, err := GetConfigPath(TargetClaude) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + seed := []byte(`{"mcpServers":{"kernel":{"env":{"MCP_REMOTE_CONFIG_DIR":""}}}}`) + if err := os.WriteFile(path, seed, 0600); err != nil { + t.Fatal(err) + } + if err := Install(TargetClaude); err != nil { + t.Fatal(err) + } + env := readTestConfig(t, path)["mcpServers"].(map[string]any)["kernel"].(map[string]any)["env"].(map[string]any) + want, err := clientCacheDir(TargetClaude) + if err != nil { + t.Fatal(err) + } + if env["MCP_REMOTE_CONFIG_DIR"] != want { + t.Fatalf("auth cache = %v, want %s", env["MCP_REMOTE_CONFIG_DIR"], want) + } +} + +func TestInstallRejectsExternalClientMetadata(t *testing.T) { + testHome(t) + path, err := GetConfigPath(TargetClaude) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + seed := []byte(`{"mcpServers":{"kernel":{"args":["-y","mcp-remote","https://old.example","--static-oauth-client-metadata","@/custom/metadata.json"]}}}`) + if err := os.WriteFile(path, seed, 0600); err != nil { + t.Fatal(err) + } + if err := Install(TargetClaude); err == nil { + t.Fatal("expected external metadata to require manual update") + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, seed) { + t.Fatal("external metadata config changed") + } +} + +func TestInstallRejectsNonObjectEnvironment(t *testing.T) { + testHome(t) + path, err := GetConfigPath(TargetClaude) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + seed := []byte(`{"mcpServers":{"kernel":{"env":"invalid"}}}`) + if err := os.WriteFile(path, seed, 0600); err != nil { + t.Fatal(err) + } + if err := Install(TargetClaude); err == nil { + t.Fatal("expected invalid environment to fail") + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, seed) { + t.Fatal("invalid config changed") + } +} + func TestInstallFxKeepsBearerTokenAuth(t *testing.T) { testHome(t) path, err := GetConfigPath(TargetFx) diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index 72fcd86b..36395ea4 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -5,9 +5,23 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" ) +func TestGooseConfigNamesOAuthClient(t *testing.T) { + config := gooseConfig("Goose", "/home/example/.mcp-auth/kernel-goose") + if !strings.Contains(config, " - --static-oauth-client-metadata\n - '{\"client_name\":\"Goose\"}'") { + t.Fatalf("Goose config missing client metadata: %s", config) + } + if !strings.Contains(config, "MCP_REMOTE_CONFIG_DIR: \"/home/example/.mcp-auth/kernel-goose\"") { + t.Fatalf("Goose config missing separate auth cache: %s", config) + } + if !strings.Contains(config, " enabled: true") { + t.Fatalf("Goose config missing enabled flag: %s", config) + } +} + func TestInstallForFx(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) From 424c1fed31dd45ddaae44058b95f1729b37467d6 Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:36:33 +0000 Subject: [PATCH 3/8] Isolate MCP OAuth callback ports by client --- cmd/mcp/config.go | 59 ++++++++++++++++++++++++++--------------------- cmd/mcp/mcp.go | 7 +++--- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/cmd/mcp/config.go b/cmd/mcp/config.go index 779b4a35..da786c0d 100644 --- a/cmd/mcp/config.go +++ b/cmd/mcp/config.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "sort" "strconv" "strings" @@ -29,17 +30,18 @@ type configField struct { } type targetSpec struct { - target Target - description string - path func(string) string - section string - transport transport - fields []configField - clientName string - remove []string - legacyName string - legacyKey string - printOnly bool + target Target + description string + path func(string) string + section string + transport transport + fields []configField + clientName string + callbackPort int + remove []string + legacyName string + legacyKey string + printOnly bool } func homePath(parts ...string) func(string) string { @@ -80,17 +82,17 @@ func appDataPath(home string) string { var targetSpecs = []targetSpec{ {target: TargetCursor, description: "Cursor editor", path: homePath(".cursor", "mcp.json"), section: "mcpServers", transport: http, fields: []configField{{name: "url", value: KernelMCPURL}}, remove: []string{"type"}}, - {target: TargetClaude, description: "Claude Desktop app", path: claudePath, section: "mcpServers", transport: stdio, clientName: "Claude Desktop"}, + {target: TargetClaude, description: "Claude Desktop app", path: claudePath, section: "mcpServers", transport: stdio, clientName: "Claude Desktop", callbackPort: 46093}, {target: TargetClaudeCode, description: "Claude Code CLI", path: homePath(".claude.json"), section: "mcpServers", transport: http, fields: []configField{{name: "type", value: "http"}, {name: "url", value: KernelMCPURL}}}, - {target: TargetAntigravity, description: "Google Antigravity", path: homePath(".gemini", "config", "mcp_config.json"), section: "mcpServers", transport: stdio, clientName: "Antigravity"}, - {target: TargetWindsurf, description: "Windsurf editor", path: homePath(".codeium", "windsurf", "mcp_config.json"), section: "mcpServers", transport: stdio, clientName: "Windsurf"}, + {target: TargetAntigravity, description: "Google Antigravity", path: homePath(".gemini", "config", "mcp_config.json"), section: "mcpServers", transport: stdio, clientName: "Antigravity", callbackPort: 46094}, + {target: TargetWindsurf, description: "Windsurf editor", path: homePath(".codeium", "windsurf", "mcp_config.json"), section: "mcpServers", transport: stdio, clientName: "Windsurf", callbackPort: 46095}, {target: TargetVSCode, description: "Visual Studio Code", path: vsCodePath, section: "servers", transport: http, legacyName: "settings.json", legacyKey: "mcp.servers", fields: []configField{{name: "url", value: KernelMCPURL}, {name: "type", value: "http"}}}, - {target: TargetGoose, description: "Goose AI", path: homePath(".config", "goose", "config.yaml"), transport: stdio, clientName: "Goose", printOnly: true}, + {target: TargetGoose, description: "Goose AI", path: homePath(".config", "goose", "config.yaml"), transport: stdio, clientName: "Goose", callbackPort: 46096, printOnly: true}, // Current Zed settings omit source; its settings migrator removes that old field. - {target: TargetZed, description: "Zed editor", path: homePath(".config", "zed", "settings.json"), section: "context_servers", transport: stdio, clientName: "Zed", + {target: TargetZed, description: "Zed editor", path: homePath(".config", "zed", "settings.json"), section: "context_servers", transport: stdio, clientName: "Zed", callbackPort: 46097, remove: []string{"source"}}, {target: TargetFx, description: "fx coding agent", path: homePath(".fx", "mcp.json"), section: "mcp", transport: http, fields: []configField{{name: "type", value: "http"}, {name: "url", value: KernelMCPURL}, {name: "oauth", value: map[string]any{}, ifMissing: true, skipWhen: "bearer_token_env"}}}, @@ -326,7 +328,7 @@ func mergeConfig(data []byte, spec targetSpec, legacy map[string]json.RawMessage } fields := spec.fields if spec.transport == stdio { - args, err := mergeStdioArgs(kernel, spec.clientName) + args, err := mergeStdioArgs(kernel, spec) if err != nil { return nil, err } @@ -411,16 +413,16 @@ func addClientCacheDir(root *hujson.Value, kernelPath string, target Target) err return patch(root, "add", kernelPath+"/env/MCP_REMOTE_CONFIG_DIR", cacheDir) } -func stdioArgs(clientName string) []string { - return []string{"-y", "mcp-remote", KernelMCPURL, "--static-oauth-client-metadata", clientMetadata(clientName)} +func stdioArgs(spec targetSpec) []string { + return []string{"-y", "mcp-remote", KernelMCPURL, strconv.Itoa(spec.callbackPort), "--static-oauth-client-metadata", clientMetadata(spec.clientName)} } func clientMetadata(clientName string) string { return fmt.Sprintf(`{"client_name":%q}`, clientName) } -func mergeStdioArgs(kernel *hujson.Object, clientName string) ([]string, error) { - defaults := stdioArgs(clientName) +func mergeStdioArgs(kernel *hujson.Object, spec targetSpec) ([]string, error) { + defaults := stdioArgs(spec) value, exists := member(kernel, "args") if !exists { return defaults, nil @@ -435,20 +437,25 @@ func mergeStdioArgs(kernel *hujson.Object, clientName string) ([]string, error) return defaults, nil } args[2] = KernelMCPURL + if len(args) == 3 { + args = append(args, defaults[3]) + } else if _, err := strconv.Atoi(args[3]); err != nil { + args = slices.Insert(args, 3, defaults[3]) + } flagIndex := -1 - for i := 3; i < len(args); i++ { - if args[i] != defaults[3] { + for i := 4; i < len(args); i++ { + if args[i] != defaults[4] { continue } if flagIndex != -1 || i+1 >= len(args) { - return nil, fmt.Errorf("invalid kernel args for %s", defaults[3]) + return nil, fmt.Errorf("invalid kernel args for %s", defaults[4]) } flagIndex = i } if flagIndex == -1 { - return append(args, defaults[3:]...), nil + return append(args, defaults[4:]...), nil } - metadata, err := mergeClientMetadata(args[flagIndex+1], clientName) + metadata, err := mergeClientMetadata(args[flagIndex+1], spec.clientName) if err != nil { return nil, err } diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index d5c0ec5c..c9b3ddcc 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -54,13 +54,13 @@ func installForGoose(configPath string, spec targetSpec) error { // and we don't want to add a YAML dependency pterm.Info.Println("Goose uses YAML configuration. Add the following to your Goose config:") pterm.Println() - fmt.Println(gooseConfig(spec.clientName, cacheDir)) + fmt.Println(gooseConfig(spec, cacheDir)) pterm.Println() pterm.Info.Printf("Config file location: %s\n", configPath) return nil } -func gooseConfig(clientName, cacheDir string) string { +func gooseConfig(spec targetSpec, cacheDir string) string { return `extensions: kernel: name: Kernel @@ -71,8 +71,9 @@ func gooseConfig(clientName, cacheDir string) string { - -y - mcp-remote - ` + KernelMCPURL + ` + - "` + fmt.Sprint(spec.callbackPort) + `" - --static-oauth-client-metadata - - '` + clientMetadata(clientName) + `' + - '` + clientMetadata(spec.clientName) + `' envs: MCP_REMOTE_CONFIG_DIR: ` + fmt.Sprintf("%q", cacheDir) } From 6f3909f00537eac43171e5e685fcca881e2819e6 Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:36:33 +0000 Subject: [PATCH 4/8] Test MCP callback port defaults and custom ports --- cmd/mcp/config_test.go | 65 +++++++++++++++++++++++++++++++++--------- cmd/mcp/mcp_test.go | 8 ++++-- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/cmd/mcp/config_test.go b/cmd/mcp/config_test.go index d5632a44..8d505682 100644 --- a/cmd/mcp/config_test.go +++ b/cmd/mcp/config_test.go @@ -44,15 +44,16 @@ func TestInstallPreservesTargetConfigs(t *testing.T) { section string stdio bool name string + port string }{ - {TargetCursor, "mcpServers", false, ""}, - {TargetClaude, "mcpServers", true, "Claude Desktop"}, - {TargetClaudeCode, "mcpServers", false, ""}, - {TargetAntigravity, "mcpServers", true, "Antigravity"}, - {TargetWindsurf, "mcpServers", true, "Windsurf"}, - {TargetVSCode, "servers", false, ""}, - {TargetZed, "context_servers", true, "Zed"}, - {TargetFx, "mcp", false, ""}, + {TargetCursor, "mcpServers", false, "", ""}, + {TargetClaude, "mcpServers", true, "Claude Desktop", "46093"}, + {TargetClaudeCode, "mcpServers", false, "", ""}, + {TargetAntigravity, "mcpServers", true, "Antigravity", "46094"}, + {TargetWindsurf, "mcpServers", true, "Windsurf", "46095"}, + {TargetVSCode, "servers", false, "", ""}, + {TargetZed, "context_servers", true, "Zed", "46097"}, + {TargetFx, "mcp", false, "", ""}, } if got := AllTargets(); len(got) != len(targets)+1 { t.Fatalf("registered targets = %d, want %d including Goose", len(got), len(targets)+1) @@ -127,11 +128,11 @@ func TestInstallPreservesTargetConfigs(t *testing.T) { if args[0] != "-y" || args[1] != "mcp-remote" || args[2] != KernelMCPURL { t.Fatalf("stdio args = %#v", args) } - if len(args) != 5 || args[3] != "--static-oauth-client-metadata" { + if len(args) != 6 || args[3] != tc.port || args[4] != "--static-oauth-client-metadata" { t.Fatalf("stdio metadata args = %#v", args) } var metadata map[string]string - if err := json.Unmarshal([]byte(args[4].(string)), &metadata); err != nil || metadata["client_name"] != tc.name { + if err := json.Unmarshal([]byte(args[5].(string)), &metadata); err != nil || metadata["client_name"] != tc.name { t.Fatalf("stdio client metadata = %#v: %v", metadata, err) } cacheDir := kernel["env"].(map[string]any)["MCP_REMOTE_CONFIG_DIR"] @@ -629,7 +630,9 @@ func TestInstallVSCodeWriteFailureDoesNotChmodLegacy(t *testing.T) { } func TestInstallPreservesMCPRemoteOptions(t *testing.T) { - for _, target := range []Target{TargetClaude, TargetAntigravity, TargetWindsurf, TargetZed} { + for target, port := range map[Target]string{ + TargetClaude: "46093", TargetAntigravity: "46094", TargetWindsurf: "46095", TargetZed: "46097", + } { t.Run(string(target), func(t *testing.T) { testHome(t) path, err := GetConfigPath(target) @@ -657,14 +660,14 @@ func TestInstallPreservesMCPRemoteOptions(t *testing.T) { } config := readTestConfig(t, path) args := config[spec.section].(map[string]any)["kernel"].(map[string]any)["args"].([]any) - if args[1] != "mcp-remote@latest" || args[2] != KernelMCPURL || args[3] != "--header-file" || args[4] != "/private/headers.txt" { + if len(args) != 8 || args[1] != "mcp-remote@latest" || args[2] != KernelMCPURL || args[3] != port || args[4] != "--header-file" || args[5] != "/private/headers.txt" { t.Fatalf("custom args lost: %#v", args) } - if len(args) != 7 || args[5] != "--static-oauth-client-metadata" { + if args[6] != "--static-oauth-client-metadata" { t.Fatalf("client metadata missing: %#v", args) } var metadata map[string]any - if err := json.Unmarshal([]byte(args[6].(string)), &metadata); err != nil { + if err := json.Unmarshal([]byte(args[7].(string)), &metadata); err != nil { t.Fatal(err) } if metadata["client_name"] != spec.clientName || metadata["scope"] != "read" || metadata["token_endpoint_auth_method"] != "none" { @@ -678,6 +681,40 @@ func TestInstallPreservesMCPRemoteOptions(t *testing.T) { } } +func TestInstallPreservesExplicitCallbackPort(t *testing.T) { + for _, target := range []Target{TargetClaude, TargetAntigravity, TargetWindsurf, TargetZed} { + t.Run(string(target), func(t *testing.T) { + testHome(t) + path, err := GetConfigPath(target) + if err != nil { + t.Fatal(err) + } + if err := Install(target); err != nil { + t.Fatal(err) + } + config := readTestConfig(t, path) + spec, _ := specFor(target) + kernel := config[spec.section].(map[string]any)["kernel"].(map[string]any) + args := []any{"-y", "mcp-remote@latest", KernelMCPURL, "54321", "--host", "127.0.0.1", "--static-oauth-client-metadata", clientMetadata(spec.clientName)} + kernel["args"] = args + data, err := json.Marshal(config) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0600); err != nil { + t.Fatal(err) + } + if err := Install(target); err != nil { + t.Fatal(err) + } + got := readTestConfig(t, path)[spec.section].(map[string]any)["kernel"].(map[string]any)["args"] + if !reflect.DeepEqual(got, args) { + t.Fatalf("custom callback args = %#v, want %#v", got, args) + } + }) + } +} + func TestInstallPreservesCustomClientCache(t *testing.T) { testHome(t) path, err := GetConfigPath(TargetClaude) diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index 36395ea4..fc5a5915 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -10,7 +10,11 @@ import ( ) func TestGooseConfigNamesOAuthClient(t *testing.T) { - config := gooseConfig("Goose", "/home/example/.mcp-auth/kernel-goose") + spec, _ := specFor(TargetGoose) + config := gooseConfig(spec, "/home/example/.mcp-auth/kernel-goose") + if !strings.Contains(config, " - https://mcp.onkernel.com/mcp\n - \"46096\"\n") { + t.Fatalf("Goose config missing separate callback port: %s", config) + } if !strings.Contains(config, " - --static-oauth-client-metadata\n - '{\"client_name\":\"Goose\"}'") { t.Fatalf("Goose config missing client metadata: %s", config) } @@ -144,7 +148,7 @@ func assertMCPRemoteShape(t *testing.T, kernel map[string]interface{}) { if !ok { t.Fatalf("args = %#v, want array", kernel["args"]) } - want := []string{"-y", "mcp-remote", KernelMCPURL, "--static-oauth-client-metadata", `{"client_name":"Antigravity"}`} + want := []string{"-y", "mcp-remote", KernelMCPURL, "46094", "--static-oauth-client-metadata", `{"client_name":"Antigravity"}`} if len(args) != len(want) { t.Fatalf("args = %v, want %v", args, want) } From 8a56da61727afd857f8f8deee9d0be99855a156b Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:32:03 +0000 Subject: [PATCH 5/8] Render Goose MCP arguments from stdio defaults --- cmd/mcp/mcp.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index c9b3ddcc..3111e271 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -2,6 +2,7 @@ package mcp import ( "fmt" + "strings" "github.com/pterm/pterm" "github.com/spf13/cobra" @@ -61,19 +62,18 @@ func installForGoose(configPath string, spec targetSpec) error { } func gooseConfig(spec targetSpec, cacheDir string) string { - return `extensions: + var config strings.Builder + config.WriteString(`extensions: kernel: name: Kernel type: stdio enabled: true cmd: npx args: - - -y - - mcp-remote - - ` + KernelMCPURL + ` - - "` + fmt.Sprint(spec.callbackPort) + `" - - --static-oauth-client-metadata - - '` + clientMetadata(spec.clientName) + `' - envs: - MCP_REMOTE_CONFIG_DIR: ` + fmt.Sprintf("%q", cacheDir) +`) + for _, arg := range stdioArgs(spec) { + fmt.Fprintf(&config, " - %q\n", arg) + } + fmt.Fprintf(&config, " envs:\n MCP_REMOTE_CONFIG_DIR: %q", cacheDir) + return config.String() } From 25d9d107379c826e754f5edfcd1c785c7e620b26 Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:32:03 +0000 Subject: [PATCH 6/8] Verify Goose YAML matches stdio arguments --- cmd/mcp/mcp_test.go | 35 ++++++++++++++++++++++++----------- go.mod | 2 +- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index fc5a5915..586079e7 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -4,25 +4,38 @@ import ( "encoding/json" "os" "path/filepath" + "reflect" "runtime" - "strings" "testing" + + "gopkg.in/yaml.v3" ) -func TestGooseConfigNamesOAuthClient(t *testing.T) { +func TestGooseConfigMatchesStdioArgs(t *testing.T) { spec, _ := specFor(TargetGoose) - config := gooseConfig(spec, "/home/example/.mcp-auth/kernel-goose") - if !strings.Contains(config, " - https://mcp.onkernel.com/mcp\n - \"46096\"\n") { - t.Fatalf("Goose config missing separate callback port: %s", config) + cacheDir := "/home/example/.mcp-auth/kernel-goose" + var config struct { + Extensions map[string]struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Enabled bool `yaml:"enabled"` + Cmd string `yaml:"cmd"` + Args []string `yaml:"args"` + Envs map[string]string `yaml:"envs"` + } `yaml:"extensions"` + } + if err := yaml.Unmarshal([]byte(gooseConfig(spec, cacheDir)), &config); err != nil { + t.Fatal(err) } - if !strings.Contains(config, " - --static-oauth-client-metadata\n - '{\"client_name\":\"Goose\"}'") { - t.Fatalf("Goose config missing client metadata: %s", config) + goose := config.Extensions["kernel"] + if !reflect.DeepEqual(goose.Args, stdioArgs(spec)) { + t.Fatalf("Goose args = %q, want %q", goose.Args, stdioArgs(spec)) } - if !strings.Contains(config, "MCP_REMOTE_CONFIG_DIR: \"/home/example/.mcp-auth/kernel-goose\"") { - t.Fatalf("Goose config missing separate auth cache: %s", config) + if goose.Name != "Kernel" || goose.Type != "stdio" || !goose.Enabled || goose.Cmd != "npx" { + t.Fatalf("Goose extension = %+v", goose) } - if !strings.Contains(config, " enabled: true") { - t.Fatalf("Goose config missing enabled flag: %s", config) + if goose.Envs["MCP_REMOTE_CONFIG_DIR"] != cacheDir { + t.Fatalf("Goose cache = %q, want %q", goose.Envs["MCP_REMOTE_CONFIG_DIR"], cacheDir) } } diff --git a/go.mod b/go.mod index f5367d34..8655cc53 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.20.0 golang.org/x/term v0.43.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -59,5 +60,4 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) From d5cf75c2b3b0f731824934035abe82b0d1141bba Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:17:37 +0000 Subject: [PATCH 7/8] Reject duplicate OAuth metadata keys --- cmd/mcp/config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/mcp/config.go b/cmd/mcp/config.go index da786c0d..cfb19be0 100644 --- a/cmd/mcp/config.go +++ b/cmd/mcp/config.go @@ -474,6 +474,9 @@ func mergeClientMetadata(raw, clientName string) (string, error) { if err != nil { return "", fmt.Errorf("invalid kernel OAuth metadata: %w", err) } + if err := validateConfigKeys(&metadata, ""); err != nil { + return "", err + } obj, err := objectAt(&metadata, "") if err != nil { return "", err From 9039534433535d44447051a31be3593a0180cf5c Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:17:37 +0000 Subject: [PATCH 8/8] Cover ambiguous OAuth metadata on reinstall --- cmd/mcp/config_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cmd/mcp/config_test.go b/cmd/mcp/config_test.go index 8d505682..1fe7f24c 100644 --- a/cmd/mcp/config_test.go +++ b/cmd/mcp/config_test.go @@ -788,6 +788,36 @@ func TestInstallRejectsExternalClientMetadata(t *testing.T) { } } +func TestInstallRejectsDuplicateClientMetadata(t *testing.T) { + testHome(t) + path, err := GetConfigPath(TargetClaude) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + seed, err := json.Marshal(map[string]any{"mcpServers": map[string]any{"kernel": map[string]any{ + "args": []string{"-y", "mcp-remote", KernelMCPURL, "--static-oauth-client-metadata", `{"client_name":"Claude Desktop","client_name":"Other"}`}, + }}}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, seed, 0600); err != nil { + t.Fatal(err) + } + if err := Install(TargetClaude); err == nil || !strings.Contains(err.Error(), `duplicate config key "client_name"`) { + t.Fatalf("install error = %v, want duplicate client name", err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, seed) { + t.Fatal("duplicate client metadata changed config") + } +} + func TestInstallRejectsNonObjectEnvironment(t *testing.T) { testHome(t) path, err := GetConfigPath(TargetClaude)