fix(setup): resolve OpenCode MCP path in generated config#114
fix(setup): resolve OpenCode MCP path in generated config#114Alan-TheGentleman merged 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Updates OpenCode MCP config generation to write a resolved engram command (not a hardcoded string) and adds regression tests for OS-specific behavior, addressing headless OpenCode spawn failures discussed in #112.
Changes:
- Use
resolveEngramCommand()when injecting OpenCode MCP config so the generatedcommandpoints at the appropriate binary. - Improve the manual-config warning snippet to use the resolved command.
- Add tests covering Windows absolute-path behavior and Unix fallback behavior for OpenCode MCP config + warnings.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| internal/setup/setup.go | Switch OpenCode MCP injection + warning snippet to use resolveEngramCommand() and update package/docs comments. |
| internal/setup/setup_test.go | Add focused tests validating command resolution in generated OpenCode config and in fallback warning output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // path (absolute on Windows, bare "engram" on Unix) so the MCP subprocess | ||
| // never requires PATH resolution in headless/systemd environments. |
There was a problem hiding this comment.
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).
| // path (absolute on Windows, bare "engram" on Unix) so the MCP subprocess | |
| // never requires PATH resolution in headless/systemd environments. | |
| // path (absolute on Windows to avoid PATH resolution issues; bare "engram" | |
| // on Unix, where the MCP subprocess relies on the environment's PATH). |
| 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) |
There was a problem hiding this comment.
The warning snippet is meant to be pasted as JSON, but formatting the command with %q uses Go string escaping (e.g., it can emit \UXXXXXXXX escapes) which is not guaranteed to be valid JSON. Consider JSON-escaping the command via json.Marshal(cmd) and printing the resulting string bytes (without introducing double-quotes) so the snippet is always valid JSON.
| 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) | |
| } |
| // Use resolveEngramCommand() so Windows users (and headless Linux setups | ||
| // where PATH is not inherited) get the absolute binary path. |
There was a problem hiding this comment.
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. |
| 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) |
There was a problem hiding this comment.
These tests use chained type assertions like cfg["mcp"].(map[string]any) / engram["command"].([]any). If the JSON shape changes or the key is missing, the test will panic instead of producing a clear failure. Using the comma-ok form (and failing with t.Fatalf) makes failures easier to diagnose.
| 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]) | |
| } |
Linked Issue
Closes #112
Summary
engramcommand when generating OpenCode MCP config instead of hardcodedengramProblem
Headless OpenCode setups can fail to spawn
engram mcpwhen the generated MCP config relies on PATH resolution. New evidence on #112 showed stdio itself is still viable in headless mode if the config points to the correct binary.Fix
This keeps stdio as the default transport and improves setup ergonomics by writing the resolved command into the OpenCode MCP config.
Test Plan
go test ./internal/setup