From bcc3db7b399a49ba6020db31df03761aff4c2a99 Mon Sep 17 00:00:00 2001 From: robertjamesprior <83608739+robertjamesprior@users.noreply.github.com> Date: Sun, 20 Sep 2026 08:20:00 +0000 Subject: [PATCH 1/5] Add antigravity MCP install target Antigravity keys remote servers off serverUrl rather than url, so the existing HTTP target shapes don't work for it. Config lives at ~/.gemini/config/mcp_config.json. Co-Authored-By: Claude Opus 5 --- cmd/mcp/install.go | 7 ++++ cmd/mcp/mcp.go | 44 ++++++++++++++++++---- cmd/mcp/mcp_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/cmd/mcp/install.go b/cmd/mcp/install.go index f1cd880e..1ce4a4e1 100644 --- a/cmd/mcp/install.go +++ b/cmd/mcp/install.go @@ -20,6 +20,7 @@ Supported targets: cursor - Cursor editor claude - Claude Desktop app claude-code - Claude Code CLI + antigravity - Google Antigravity windsurf - Windsurf editor vscode - Visual Studio Code goose - Goose AI @@ -119,6 +120,12 @@ func printPostInstallInstructions(target Target) { pterm.Println(" 1. Run '/mcp' in the Claude Code REPL to authenticate") pterm.Println(" 2. The Kernel tools will then be available") + case TargetAntigravity: + pterm.Info.Println("Next steps:") + pterm.Println(" 1. Reload the Antigravity window") + pterm.Println(" 2. Open agent settings and click 'Authenticate' next to kernel on the Customizations tab") + pterm.Println(" 3. Paste the authorization code from your browser back into the settings panel") + case TargetWindsurf: pterm.Info.Println("Next steps:") pterm.Println(" 1. Open Windsurf settings and navigate to MCP servers") diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index 0b871d3c..06c9ce8d 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -27,14 +27,15 @@ var MCPCmd = &cobra.Command{ type Target string const ( - TargetCursor Target = "cursor" - TargetClaude Target = "claude" - TargetClaudeCode Target = "claude-code" - TargetWindsurf Target = "windsurf" - TargetVSCode Target = "vscode" - TargetGoose Target = "goose" - TargetZed Target = "zed" - TargetFx Target = "fx" + TargetCursor Target = "cursor" + TargetClaude Target = "claude" + TargetClaudeCode Target = "claude-code" + TargetAntigravity Target = "antigravity" + TargetWindsurf Target = "windsurf" + TargetVSCode Target = "vscode" + TargetGoose Target = "goose" + TargetZed Target = "zed" + TargetFx Target = "fx" ) // KernelMCPURL is the URL for the Kernel MCP server @@ -46,6 +47,7 @@ func AllTargets() []Target { TargetCursor, TargetClaude, TargetClaudeCode, + TargetAntigravity, TargetWindsurf, TargetVSCode, TargetGoose, @@ -86,6 +88,8 @@ func getConfigPath(target Target) (string, error) { case TargetClaudeCode: // Claude Code uses the ~/.claude.json file return filepath.Join(homeDir, ".claude.json"), nil + case TargetAntigravity: + return filepath.Join(homeDir, ".gemini", "config", "mcp_config.json"), nil case TargetWindsurf: return filepath.Join(homeDir, ".codeium", "windsurf", "mcp_config.json"), nil case TargetVSCode: @@ -327,6 +331,28 @@ func installForClaudeCode(configPath string) error { return writeJSONFile(configPath, config) } +// installForAntigravity installs MCP config for Google Antigravity +func installForAntigravity(configPath string) error { + config, err := readJSONFile(configPath) + if err != nil { + return err + } + + // Get or create mcpServers section + mcpServers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + mcpServers = make(map[string]interface{}) + } + + // Antigravity keys remote servers off serverUrl; it ignores url and httpUrl + mcpServers["kernel"] = map[string]interface{}{ + "serverUrl": KernelMCPURL, + } + config["mcpServers"] = mcpServers + + return writeJSONFile(configPath, config) +} + // installForWindsurf installs MCP config for Windsurf func installForWindsurf(configPath string) error { config, err := readJSONFile(configPath) @@ -453,6 +479,8 @@ func Install(target Target) error { return installForClaude(configPath) case TargetClaudeCode: return installForClaudeCode(configPath) + case TargetAntigravity: + return installForAntigravity(configPath) case TargetWindsurf: return installForWindsurf(configPath) case TargetVSCode: diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index 86d1befb..59257c87 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -119,3 +119,95 @@ func TestInstallForFxClean(t *testing.T) { t.Fatalf("config directory permissions = %o, want 700", got) } } + +func TestInstallForAntigravity(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + configPath := filepath.Join(home, ".gemini", "config", "mcp_config.json") + if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { + t.Fatal(err) + } + + existing := `{ + "mcpServers": { + "existing": { + "serverUrl": "https://example.com/mcp" + } + }, + "setting": "preserved" +}` + if err := os.WriteFile(configPath, []byte(existing), 0644); err != nil { + t.Fatal(err) + } + + if err := Install(TargetAntigravity); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatal(err) + } + if config["setting"] != "preserved" { + t.Fatalf("setting = %v, want preserved", config["setting"]) + } + + servers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + t.Fatalf("mcpServers = %#v, want object", config["mcpServers"]) + } + if _, ok := servers["existing"]; !ok { + t.Fatal("existing MCP server was removed") + } + + kernel, ok := servers["kernel"].(map[string]interface{}) + if !ok { + t.Fatalf("kernel = %#v, want object", servers["kernel"]) + } + if kernel["serverUrl"] != KernelMCPURL { + t.Fatalf("serverUrl = %v, want %s", kernel["serverUrl"], KernelMCPURL) + } + // Antigravity ignores url and httpUrl, so writing either would silently do nothing + if _, ok := kernel["url"]; ok { + t.Fatalf("kernel = %#v, want no url key", kernel) + } +} + +func TestInstallForAntigravityClean(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + if err := Install(TargetAntigravity); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(filepath.Join(home, ".gemini", "config", "mcp_config.json")) + if err != nil { + t.Fatal(err) + } + + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatal(err) + } + + servers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + t.Fatalf("mcpServers = %#v, want object", config["mcpServers"]) + } + kernel, ok := servers["kernel"].(map[string]interface{}) + if !ok { + t.Fatalf("kernel = %#v, want object", servers["kernel"]) + } + if kernel["serverUrl"] != KernelMCPURL { + t.Fatalf("serverUrl = %v, want %s", kernel["serverUrl"], KernelMCPURL) + } +} From b7b3caa6aa433b451af8d0f1ae545388cb900641 Mon Sep 17 00:00:00 2001 From: robertjamesprior <83608739+robertjamesprior@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:51:41 +0000 Subject: [PATCH 2/5] Merge into the existing kernel entry on antigravity install Replacing mcpServers.kernel wholesale dropped any field a user had added by hand, including the headers block that carries the API-key workaround, so rerunning the install silently broke a working config. Merge into the existing object instead: set serverUrl, and delete only url and httpUrl, which antigravity ignores. Co-Authored-By: Claude Opus 5 --- cmd/mcp/mcp.go | 14 ++++++++-- cmd/mcp/mcp_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index 06c9ce8d..4900a673 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -344,10 +344,18 @@ func installForAntigravity(configPath string) error { mcpServers = make(map[string]interface{}) } - // Antigravity keys remote servers off serverUrl; it ignores url and httpUrl - mcpServers["kernel"] = map[string]interface{}{ - "serverUrl": KernelMCPURL, + // Merge into any existing entry so hand-added fields survive a reinstall. + // Antigravity keys remote servers off serverUrl and ignores url and httpUrl, + // so those are dropped rather than left behind looking meaningful. + kernel, ok := mcpServers["kernel"].(map[string]interface{}) + if !ok { + kernel = make(map[string]interface{}) } + kernel["serverUrl"] = KernelMCPURL + delete(kernel, "url") + delete(kernel, "httpUrl") + + mcpServers["kernel"] = kernel config["mcpServers"] = mcpServers return writeJSONFile(configPath, config) diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index 59257c87..eb8a196a 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -211,3 +211,71 @@ func TestInstallForAntigravityClean(t *testing.T) { t.Fatalf("serverUrl = %v, want %s", kernel["serverUrl"], KernelMCPURL) } } + +func TestInstallForAntigravityPreservesExistingKernelFields(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + + configPath := filepath.Join(home, ".gemini", "config", "mcp_config.json") + if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { + t.Fatal(err) + } + + // headers carries the documented API-key workaround, and url is a stale key + // from an earlier install that Antigravity ignores. + existing := `{ + "mcpServers": { + "kernel": { + "url": "https://mcp.onkernel.com/stale", + "httpUrl": "https://mcp.onkernel.com/stale", + "headers": { + "Authorization": "Bearer sk-test" + } + } + } +}` + if err := os.WriteFile(configPath, []byte(existing), 0644); err != nil { + t.Fatal(err) + } + + if err := Install(TargetAntigravity); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatal(err) + } + + servers, ok := config["mcpServers"].(map[string]interface{}) + if !ok { + t.Fatalf("mcpServers = %#v, want object", config["mcpServers"]) + } + kernel, ok := servers["kernel"].(map[string]interface{}) + if !ok { + t.Fatalf("kernel = %#v, want object", servers["kernel"]) + } + + headers, ok := kernel["headers"].(map[string]interface{}) + if !ok { + t.Fatalf("headers = %#v, want the existing block preserved", kernel["headers"]) + } + if headers["Authorization"] != "Bearer sk-test" { + t.Fatalf("Authorization = %v, want the existing value", headers["Authorization"]) + } + + if kernel["serverUrl"] != KernelMCPURL { + t.Fatalf("serverUrl = %v, want %s", kernel["serverUrl"], KernelMCPURL) + } + for _, key := range []string{"url", "httpUrl"} { + if _, ok := kernel[key]; ok { + t.Fatalf("kernel = %#v, want %s removed", kernel, key) + } + } +} From 8e7d21601e65f6bf2eaa372aafcb808bb4bbebec Mon Sep 17 00:00:00 2001 From: robertjamesprior <83608739+robertjamesprior@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:03:44 +0000 Subject: [PATCH 3/5] Write the antigravity MCP config 0600 Preserving the headers block means the file can now hold an API key, and writeJSONFile leaves it world-readable at 0644. Use writePrivateJSONFile, which already backs the fx target and chmods an existing file. Co-Authored-By: Claude Opus 5 --- cmd/mcp/mcp.go | 4 +++- cmd/mcp/mcp_test.go | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index 4900a673..5c943faa 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -358,7 +358,9 @@ func installForAntigravity(configPath string) error { mcpServers["kernel"] = kernel config["mcpServers"] = mcpServers - return writeJSONFile(configPath, config) + // Preserving headers means this file can now hold an API key, so it is + // written 0600 rather than world-readable. + return writePrivateJSONFile(configPath, config) } // installForWindsurf installs MCP config for Windsurf diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index eb8a196a..4ea42f78 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -278,4 +278,14 @@ func TestInstallForAntigravityPreservesExistingKernelFields(t *testing.T) { t.Fatalf("kernel = %#v, want %s removed", kernel, key) } } + + // The preserved headers block can hold an API key, so the file must not stay + // world-readable after a reinstall. + info, err := os.Stat(configPath) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0600 { + t.Fatalf("config permissions = %o, want 600", got) + } } From 554f75b5d66ada5d8ab45bcd9f191ec62d3d32ca Mon Sep 17 00:00:00 2001 From: robertjamesprior <83608739+robertjamesprior@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:32:19 +0000 Subject: [PATCH 4/5] Install antigravity over stdio via mcp-remote Antigravity's remote-server client can complete the OAuth flow and still send initialize without the bearer token, so the serverUrl config this target wrote fails on first contact with 401 Unauthorized. Write the npx mcp-remote stdio entry instead, matching Claude Desktop, Windsurf and Zed. mcp-remote runs the OAuth flow itself, so the token never depends on Antigravity's client. Reinstalling over a previous serverUrl entry strips it along with the other remote-transport keys. Post-install steps now describe the reload-and-authorize path that the stdio entry actually takes. Co-Authored-By: Claude Opus 5 --- cmd/mcp/install.go | 6 +++--- cmd/mcp/mcp.go | 13 ++++++++---- cmd/mcp/mcp_test.go | 49 ++++++++++++++++++++++++++++----------------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/cmd/mcp/install.go b/cmd/mcp/install.go index 1ce4a4e1..9ba67a18 100644 --- a/cmd/mcp/install.go +++ b/cmd/mcp/install.go @@ -122,9 +122,9 @@ func printPostInstallInstructions(target Target) { case TargetAntigravity: pterm.Info.Println("Next steps:") - pterm.Println(" 1. Reload the Antigravity window") - pterm.Println(" 2. Open agent settings and click 'Authenticate' next to kernel on the Customizations tab") - pterm.Println(" 3. Paste the authorization code from your browser back into the settings panel") + pterm.Println(" 1. Go to Settings -> Customizations, scroll to Installed MCP Servers, click the reload button") + pterm.Println(" 2. Complete the OAuth flow that opens in your browser") + pterm.Println(" 3. Verify that Kernel appears connected in Antigravity") case TargetWindsurf: pterm.Info.Println("Next steps:") diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index 5c943faa..cbed7ffc 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -344,14 +344,19 @@ func installForAntigravity(configPath string) error { mcpServers = make(map[string]interface{}) } - // Merge into any existing entry so hand-added fields survive a reinstall. - // Antigravity keys remote servers off serverUrl and ignores url and httpUrl, - // so those are dropped rather than left behind looking meaningful. + // Antigravity's remote-server client can finish OAuth and still send + // initialize without the bearer token, so Kernel goes over stdio via + // mcp-remote like Claude Desktop, Windsurf and Zed. + // + // Merge into any existing entry so hand-added fields survive a reinstall; + // the remote-transport keys are dropped now that nothing reads them. kernel, ok := mcpServers["kernel"].(map[string]interface{}) if !ok { kernel = make(map[string]interface{}) } - kernel["serverUrl"] = KernelMCPURL + kernel["command"] = "npx" + kernel["args"] = []string{"-y", "mcp-remote", KernelMCPURL} + delete(kernel, "serverUrl") delete(kernel, "url") delete(kernel, "httpUrl") diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index 4ea42f78..13bcfb16 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -120,6 +120,33 @@ func TestInstallForFxClean(t *testing.T) { } } +func assertMCPRemoteShape(t *testing.T, kernel map[string]interface{}) { + t.Helper() + + if kernel["command"] != "npx" { + t.Fatalf("command = %v, want npx", kernel["command"]) + } + args, ok := kernel["args"].([]interface{}) + if !ok { + t.Fatalf("args = %#v, want array", kernel["args"]) + } + want := []string{"-y", "mcp-remote", KernelMCPURL} + if len(args) != len(want) { + t.Fatalf("args = %v, want %v", args, want) + } + for i, w := range want { + if args[i] != w { + t.Fatalf("args[%d] = %v, want %s", i, args[i], w) + } + } + // A leftover remote-transport key would misrepresent how the server is reached + for _, key := range []string{"serverUrl", "url", "httpUrl"} { + if _, ok := kernel[key]; ok { + t.Fatalf("kernel = %#v, want %s removed", kernel, key) + } + } +} + func TestInstallForAntigravity(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) @@ -171,13 +198,7 @@ func TestInstallForAntigravity(t *testing.T) { if !ok { t.Fatalf("kernel = %#v, want object", servers["kernel"]) } - if kernel["serverUrl"] != KernelMCPURL { - t.Fatalf("serverUrl = %v, want %s", kernel["serverUrl"], KernelMCPURL) - } - // Antigravity ignores url and httpUrl, so writing either would silently do nothing - if _, ok := kernel["url"]; ok { - t.Fatalf("kernel = %#v, want no url key", kernel) - } + assertMCPRemoteShape(t, kernel) } func TestInstallForAntigravityClean(t *testing.T) { @@ -207,9 +228,7 @@ func TestInstallForAntigravityClean(t *testing.T) { if !ok { t.Fatalf("kernel = %#v, want object", servers["kernel"]) } - if kernel["serverUrl"] != KernelMCPURL { - t.Fatalf("serverUrl = %v, want %s", kernel["serverUrl"], KernelMCPURL) - } + assertMCPRemoteShape(t, kernel) } func TestInstallForAntigravityPreservesExistingKernelFields(t *testing.T) { @@ -227,6 +246,7 @@ func TestInstallForAntigravityPreservesExistingKernelFields(t *testing.T) { existing := `{ "mcpServers": { "kernel": { + "serverUrl": "https://mcp.onkernel.com/stale", "url": "https://mcp.onkernel.com/stale", "httpUrl": "https://mcp.onkernel.com/stale", "headers": { @@ -270,14 +290,7 @@ func TestInstallForAntigravityPreservesExistingKernelFields(t *testing.T) { t.Fatalf("Authorization = %v, want the existing value", headers["Authorization"]) } - if kernel["serverUrl"] != KernelMCPURL { - t.Fatalf("serverUrl = %v, want %s", kernel["serverUrl"], KernelMCPURL) - } - for _, key := range []string{"url", "httpUrl"} { - if _, ok := kernel[key]; ok { - t.Fatalf("kernel = %#v, want %s removed", kernel, key) - } - } + assertMCPRemoteShape(t, kernel) // The preserved headers block can hold an API key, so the file must not stay // world-readable after a reinstall. From 631ce2917dccb611917ba7cabdba1a4632a703bf Mon Sep 17 00:00:00 2001 From: robertjamesprior <83608739+robertjamesprior@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:16:01 +0000 Subject: [PATCH 5/5] Register the antigravity OAuth client as Antigravity Without static client metadata, mcp-remote registers as "MCP CLI Proxy", so the consent screen reads "mcp cli proxy wants to access kernel" and asks the user to trust that name rather than the application they are connecting. Also skip the config permission assertion on Windows, where Mode().Perm() reports 0666 for a writable file, matching TestInstallForFx. Co-Authored-By: Claude Opus 5 --- cmd/mcp/mcp.go | 5 ++++- cmd/mcp/mcp_test.go | 16 +++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index cbed7ffc..354e9d48 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -354,8 +354,11 @@ func installForAntigravity(configPath string) error { if !ok { kernel = make(map[string]interface{}) } + // Without static client metadata, mcp-remote registers itself as "MCP CLI + // Proxy" and the consent screen asks the user to trust that rather than the + // application they are connecting. kernel["command"] = "npx" - kernel["args"] = []string{"-y", "mcp-remote", KernelMCPURL} + kernel["args"] = []string{"-y", "mcp-remote", KernelMCPURL, "--static-oauth-client-metadata", `{"client_name":"Antigravity"}`} delete(kernel, "serverUrl") delete(kernel, "url") delete(kernel, "httpUrl") diff --git a/cmd/mcp/mcp_test.go b/cmd/mcp/mcp_test.go index 13bcfb16..72fcd86b 100644 --- a/cmd/mcp/mcp_test.go +++ b/cmd/mcp/mcp_test.go @@ -130,7 +130,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} + want := []string{"-y", "mcp-remote", KernelMCPURL, "--static-oauth-client-metadata", `{"client_name":"Antigravity"}`} if len(args) != len(want) { t.Fatalf("args = %v, want %v", args, want) } @@ -294,11 +294,13 @@ func TestInstallForAntigravityPreservesExistingKernelFields(t *testing.T) { // The preserved headers block can hold an API key, so the file must not stay // world-readable after a reinstall. - info, err := os.Stat(configPath) - if err != nil { - t.Fatal(err) - } - if got := info.Mode().Perm(); got != 0600 { - t.Fatalf("config permissions = %o, want 600", got) + if runtime.GOOS != "windows" { + info, err := os.Stat(configPath) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0600 { + t.Fatalf("config permissions = %o, want 600", got) + } } }