Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/mcp/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. 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:")
pterm.Println(" 1. Open Windsurf settings and navigate to MCP servers")
Expand Down
62 changes: 54 additions & 8 deletions cmd/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,6 +47,7 @@ func AllTargets() []Target {
TargetCursor,
TargetClaude,
TargetClaudeCode,
TargetAntigravity,
TargetWindsurf,
TargetVSCode,
TargetGoose,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -327,6 +331,46 @@ 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'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{})
}
// 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, "--static-oauth-client-metadata", `{"client_name":"Antigravity"}`}
delete(kernel, "serverUrl")
delete(kernel, "url")
delete(kernel, "httpUrl")

mcpServers["kernel"] = kernel
Comment thread
cursor[bot] marked this conversation as resolved.
config["mcpServers"] = mcpServers

// 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
func installForWindsurf(configPath string) error {
config, err := readJSONFile(configPath)
Expand Down Expand Up @@ -453,6 +497,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:
Expand Down
185 changes: 185 additions & 0 deletions cmd/mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,188 @@ func TestInstallForFxClean(t *testing.T) {
t.Fatalf("config directory permissions = %o, want 700", got)
}
}

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, "--static-oauth-client-metadata", `{"client_name":"Antigravity"}`}
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)
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"])
}
assertMCPRemoteShape(t, 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"])
}
assertMCPRemoteShape(t, kernel)
}

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": {
"serverUrl": "https://mcp.onkernel.com/stale",
"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"])
}

assertMCPRemoteShape(t, kernel)

// The preserved headers block can hold an API key, so the file must not stay
// world-readable after a reinstall.
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)
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Loading