-
Notifications
You must be signed in to change notification settings - Fork 185
fix(setup): resolve OpenCode MCP path in generated config #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,9 @@ | ||||||||||||||||||||
| // Package setup handles agent plugin installation. | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // - OpenCode: copies embedded plugin file to ~/.config/opencode/plugins/ | ||||||||||||||||||||
| // and injects MCP registration in opencode.json using the resolved binary | ||||||||||||||||||||
| // path (absolute on Windows, bare "engram" on Unix) so the MCP subprocess | ||||||||||||||||||||
| // never requires PATH resolution in headless/systemd environments. | ||||||||||||||||||||
| // - Claude Code: runs `claude plugin marketplace add` + `claude plugin install`, | ||||||||||||||||||||
| // then writes a durable MCP config to ~/.claude/mcp/engram.json using the | ||||||||||||||||||||
| // absolute binary path so the subprocess never needs PATH resolution. | ||||||||||||||||||||
|
|
@@ -268,9 +271,10 @@ func installOpenCode() (*Result, error) { | |||||||||||||||||||
| files := 1 | ||||||||||||||||||||
| if err := injectOpenCodeMCPFn(); err != nil { | ||||||||||||||||||||
| // Non-fatal: plugin works, MCP just needs manual config | ||||||||||||||||||||
| cmd := resolveEngramCommand() | ||||||||||||||||||||
| fmt.Fprintf(os.Stderr, "warning: could not auto-register MCP server in opencode.json: %v\n", err) | ||||||||||||||||||||
| fmt.Fprintf(os.Stderr, " Add manually to your opencode.json under \"mcp\":\n") | ||||||||||||||||||||
| fmt.Fprintf(os.Stderr, " \"engram\": { \"type\": \"local\", \"command\": [\"engram\", \"mcp\", \"--tools=agent\"], \"enabled\": true }\n") | ||||||||||||||||||||
| fmt.Fprintf(os.Stderr, " \"engram\": { \"type\": \"local\", \"command\": [%q, \"mcp\", \"--tools=agent\"], \"enabled\": true }\n", cmd) | ||||||||||||||||||||
|
||||||||||||||||||||
| fmt.Fprintf(os.Stderr, " \"engram\": { \"type\": \"local\", \"command\": [%q, \"mcp\", \"--tools=agent\"], \"enabled\": true }\n", cmd) | |
| // Use JSON escaping for the command so the snippet is always valid JSON. | |
| if cmdJSON, errJSON := jsonMarshalFn(cmd); errJSON == nil { | |
| fmt.Fprintf(os.Stderr, " \"engram\": { \"type\": \"local\", \"command\": [%s, \"mcp\", \"--tools=agent\"], \"enabled\": true }\n", cmdJSON) | |
| } else { | |
| // Fallback: preserve original behavior if JSON marshalling fails. | |
| fmt.Fprintf(os.Stderr, " \"engram\": { \"type\": \"local\", \"command\": [%q, \"mcp\", \"--tools=agent\"], \"enabled\": true }\n", cmd) | |
| } |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment mentions "headless Linux setups where PATH is not inherited" as a reason to use an absolute binary path, but resolveEngramCommand() currently returns bare "engram" on non-Windows. Please clarify the comment so it matches the implementation (or update resolveEngramCommand() if Linux headless PATH inheritance is a real requirement).
| // Use resolveEngramCommand() so Windows users (and headless Linux setups | |
| // where PATH is not inherited) get the absolute binary path. | |
| // Use resolveEngramCommand() so Windows users get an absolute binary path, | |
| // while Unix-like systems use the bare "engram" command resolved via PATH. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2014,3 +2014,241 @@ func TestInjectOpenCodeMCPHandlesJSONC(t *testing.T) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("expected existing 'other' entry to be preserved") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ─── Issue #112: OpenCode MCP absolute-path config ─────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TestInjectOpenCodeMCPUsesResolvedCommand verifies that injectOpenCodeMCP() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // writes the correct command based on the OS: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Windows: absolute path from os.Executable() so headless MCP subprocesses | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // don't need PATH. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Unix: bare "engram" (PATH is reliably inherited by child processes). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func TestInjectOpenCodeMCPUsesResolvedCommand(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Run("windows writes absolute path in command array", func(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resetSetupSeams(t) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| home := useTestHome(t) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runtimeGOOS = "windows" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| osExecutable = func() (string, error) { return `C:\Users\user\bin\engram.exe`, nil } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Setenv("XDG_CONFIG_HOME", "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| configDir := filepath.Join(home, ".config", "opencode") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := os.MkdirAll(configDir, 0755); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("mkdir config dir: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := injectOpenCodeMCP(); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("injectOpenCodeMCP failed: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw, err := os.ReadFile(filepath.Join(configDir, "opencode.json")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("read config: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var cfg map[string]any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := json.Unmarshal(raw, &cfg); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("parse config: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcp := cfg["mcp"].(map[string]any) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engram := mcp["engram"].(map[string]any) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cmd := engram["command"].([]any) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(cmd) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("expected non-empty command array") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| first := cmd[0].(string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2050
to
+2056
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcp := cfg["mcp"].(map[string]any) | |
| engram := mcp["engram"].(map[string]any) | |
| cmd := engram["command"].([]any) | |
| if len(cmd) == 0 { | |
| t.Fatalf("expected non-empty command array") | |
| } | |
| first := cmd[0].(string) | |
| mcpVal, ok := cfg["mcp"] | |
| if !ok { | |
| t.Fatalf("config missing %q key", "mcp") | |
| } | |
| mcp, ok := mcpVal.(map[string]any) | |
| if !ok { | |
| t.Fatalf("config %q key has unexpected type %T, want map[string]any", "mcp", mcpVal) | |
| } | |
| engramVal, ok := mcp["engram"] | |
| if !ok { | |
| t.Fatalf("mcp config missing %q key", "engram") | |
| } | |
| engram, ok := engramVal.(map[string]any) | |
| if !ok { | |
| t.Fatalf("mcp %q key has unexpected type %T, want map[string]any", "engram", engramVal) | |
| } | |
| cmdVal, ok := engram["command"] | |
| if !ok { | |
| t.Fatalf("engram config missing %q key", "command") | |
| } | |
| cmd, ok := cmdVal.([]any) | |
| if !ok { | |
| t.Fatalf("engram %q key has unexpected type %T, want []any", "command", cmdVal) | |
| } | |
| if len(cmd) == 0 { | |
| t.Fatalf("expected non-empty command array") | |
| } | |
| first, ok := cmd[0].(string) | |
| if !ok { | |
| t.Fatalf("first command element has unexpected type %T, want string", cmd[0]) | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Package doc comment says the OpenCode MCP config uses a resolved binary path so the subprocess "never requires PATH resolution in headless/systemd environments", but on Unix resolveEngramCommand() intentionally returns the bare "engram" name (PATH-based). Please adjust the comment to reflect the actual behavior (or change the implementation if the intent is truly to avoid PATH on Unix too).