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
111 changes: 111 additions & 0 deletions cmd/codebase-memory-mcp/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ func runInstall(args []string) int {
// Zed (uses "context_servers" key with "source" field)
installZedMCP(binaryPath, zedConfigPath(), cfg)

// OpenCode (uses "mcp" key with "type":"local" and command array)
installOpenCodeMCP(binaryPath, opencodeConfigPath(), cfg)

fmt.Println("\nDone. Restart your editor/CLI to activate.")
return 0
}
Expand Down Expand Up @@ -124,6 +127,9 @@ func runUninstall(args []string) int {
// Zed
removeZedMCP(zedConfigPath(), cfg)

// OpenCode
removeOpenCodeMCP(opencodeConfigPath(), cfg)

fmt.Println("\nDone. Binary and databases were NOT removed.")
return 0
}
Expand Down Expand Up @@ -847,3 +853,108 @@ func removeZedMCP(configPath string, cfg installConfig) {
}
fmt.Printf(" ✓ Removed %s from %s\n", mcpServerKey, configPath)
}

// --- OpenCode ---

// opencodeConfigPath returns the OpenCode global config path.
func opencodeConfigPath() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".config", "opencode", "opencode.json")
}

// installOpenCodeMCP upserts our MCP server in OpenCode's config (uses "mcp" key with "type":"local").
func installOpenCodeMCP(binaryPath, configPath string, cfg installConfig) {
if configPath == "" {
return
}

fmt.Printf("[OpenCode] MCP config: %s\n", configPath)

if cfg.dryRun {
fmt.Printf(" [dry-run] Would upsert %s in %s\n", mcpServerKey, configPath)
return
}

root := make(map[string]any)
if data, err := os.ReadFile(configPath); err == nil {
if jsonErr := json.Unmarshal(data, &root); jsonErr != nil {
fmt.Printf(" ⚠ Invalid JSON in %s, overwriting\n", configPath)
root = make(map[string]any)
}
}

servers, ok := root["mcp"].(map[string]any)
if !ok {
servers = make(map[string]any)
}

servers[mcpServerKey] = map[string]any{
"type": "local",
"command": []string{binaryPath},
}
root["mcp"] = servers

if err := os.MkdirAll(filepath.Dir(configPath), 0o750); err != nil {
fmt.Printf(" ⚠ mkdir %s: %v\n", filepath.Dir(configPath), err)
return
}
out, err := json.MarshalIndent(root, "", " ")
if err != nil {
fmt.Printf(" ⚠ marshal JSON: %v\n", err)
return
}
if err := os.WriteFile(configPath, append(out, '\n'), 0o600); err != nil {
fmt.Printf(" ⚠ write %s: %v\n", configPath, err)
return
}
fmt.Printf(" ✓ MCP server registered in %s\n", configPath)
}

// removeOpenCodeMCP removes our MCP server from OpenCode's config.
func removeOpenCodeMCP(configPath string, cfg installConfig) {
if configPath == "" {
return
}

data, err := os.ReadFile(configPath)
if err != nil {
return
}

var root map[string]any
if err := json.Unmarshal(data, &root); err != nil {
return
}

servers, ok := root["mcp"].(map[string]any)
if !ok {
return
}
if _, exists := servers[mcpServerKey]; !exists {
return
}

fmt.Printf("[OpenCode] MCP config: %s\n", configPath)

if cfg.dryRun {
fmt.Printf(" [dry-run] Would remove %s from %s\n", mcpServerKey, configPath)
return
}

delete(servers, mcpServerKey)
root["mcp"] = servers

out, err := json.MarshalIndent(root, "", " ")
if err != nil {
fmt.Printf(" ⚠ marshal JSON: %v\n", err)
return
}
if err := os.WriteFile(configPath, append(out, '\n'), 0o600); err != nil {
fmt.Printf(" ⚠ write %s: %v\n", configPath, err)
return
}
fmt.Printf(" ✓ Removed %s from %s\n", mcpServerKey, configPath)
}
116 changes: 116 additions & 0 deletions cmd/codebase-memory-mcp/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,122 @@ func TestZedMCPUninstall(t *testing.T) {
}
}

func TestOpenCodeConfigPath(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)

path := opencodeConfigPath()
if path == "" {
t.Fatal("opencodeConfigPath returned empty")
}
if !strings.HasSuffix(path, filepath.Join(".config", "opencode", "opencode.json")) {
t.Fatalf("unexpected path: %s", path)
}
}

func TestOpenCodeMCPInstall(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)

configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
binaryPath := "/usr/local/bin/codebase-memory-mcp"

installOpenCodeMCP(binaryPath, configPath, installConfig{})

data, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read config: %v", err)
}
var root map[string]any
if err := json.Unmarshal(data, &root); err != nil {
t.Fatalf("unmarshal: %v", err)
}
servers, ok := root["mcp"].(map[string]any)
if !ok {
t.Fatal("expected mcp key")
}
entry, ok := servers["codebase-memory-mcp"].(map[string]any)
if !ok {
t.Fatal("codebase-memory-mcp not registered")
}
if entry["type"] != "local" {
t.Fatalf("expected type=local, got %v", entry["type"])
}
cmd, ok := entry["command"].([]any)
if !ok || len(cmd) != 1 || cmd[0] != binaryPath {
t.Fatalf("expected command=[%s], got %v", binaryPath, entry["command"])
}
}

func TestOpenCodeMCPPreservesSettings(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)

configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
if err := os.MkdirAll(filepath.Dir(configPath), 0o750); err != nil {
t.Fatal(err)
}

// Pre-existing OpenCode settings
existing := `{"provider": "anthropic", "model": "claude-sonnet-4-20250514"}`
if err := os.WriteFile(configPath, []byte(existing), 0o600); err != nil {
t.Fatal(err)
}

installOpenCodeMCP("/usr/local/bin/codebase-memory-mcp", configPath, installConfig{})

data, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
var root map[string]any
if err := json.Unmarshal(data, &root); err != nil {
t.Fatal(err)
}
// Original settings preserved
if root["provider"] != "anthropic" {
t.Fatal("provider setting was lost")
}
if root["model"] != "claude-sonnet-4-20250514" {
t.Fatal("model setting was lost")
}
// MCP server added
servers, ok := root["mcp"].(map[string]any)
if !ok {
t.Fatal("mcp key missing")
}
if _, ok := servers["codebase-memory-mcp"]; !ok {
t.Fatal("codebase-memory-mcp not added")
}
}

func TestOpenCodeMCPUninstall(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)

configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
binaryPath := "/usr/local/bin/codebase-memory-mcp"

installOpenCodeMCP(binaryPath, configPath, installConfig{})
removeOpenCodeMCP(configPath, installConfig{})

data, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
var root map[string]any
if err := json.Unmarshal(data, &root); err != nil {
t.Fatal(err)
}
servers, ok := root["mcp"].(map[string]any)
if !ok {
t.Fatal("mcp key missing")
}
if _, exists := servers["codebase-memory-mcp"]; exists {
t.Fatal("codebase-memory-mcp should be removed")
}
}

func TestRemoveOldMonolithicSkill(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)
Expand Down