fix(opencode): use type=local + environment field for MCP config (#1380) (#1388)

## Summary

Fixes #1380 — OpenCode MCP config was written with the wrong schema in
both `mcp install` and `wrap opencode`.

### Root Cause

`_spec_to_entry` and `build_opencode_config_content` both generated:
```json
{
  "type": "remote",
  "url": "http://127.0.0.1:<port>/mcp",
  "env": {...}
}
```

OpenCode's local-stdio MCP schema requires:
```json
{
  "type": "local",
  "command": ["headroom", "mcp", "serve"],
  "environment": {...}
}
```

The proxy does not expose `/mcp`; it returns 404. The `env` field is
also the wrong key — OpenCode expects `environment`.

### Changes

- **`headroom/mcp_registry/opencode.py`** — `_spec_to_entry`:
`type=local`, remove `url`, command always a list, env vars under
`environment`; `_entry_to_spec`: read `environment` first, fall back to
legacy `env` for existing configs
- **`headroom/providers/opencode/runtime.py`** —
`build_opencode_config_content`: local stdio entry with
`HEADROOM_PROXY_URL` env var pointing to the proxy port (headroom mcp
serve picks it up at startup)
- **Tests** — updated two assertions to match corrected schema; added 3
regression tests for `type=local`, `environment` field, and legacy `env`
fallback

## Test Plan

- [x] `pytest tests/test_mcp_registry_opencode.py` — 64 passed
- [x] `pytest tests/test_cli/test_wrap_opencode.py` — 64 passed
- [x] All previously-passing tests remain green

## Remaining items from #1380

- `--no-mcp` still writes persistent MCP via
`inject_opencode_provider_config()` — tracked in the issue, separate PR
- `mcp uninstall/status` symmetry — tracked in the issue, larger scope
- `--target opencode` CLI addition — tracked in the issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

## Real behavior proof

**Setup:** macOS 14, Python 3.12, headroom-ai 0.27.0-dev, OpenCode 0.1.x

**Steps after patch:**
```bash
headroom mcp install --agent opencode
cat ~/.config/opencode/opencode.json | python3 -m json.tool
```

**After-fix evidence — written config:**
```json
{
  "mcp": {
    "headroom": {
      "type": "local",
      "command": ["headroom", "mcp", "serve"],
      "enabled": true
    }
  }
}
```
Before fix: `type: "remote"`, `url: "http://127.0.0.1:8787/mcp"` (404 on
proxy), `env` key (wrong field name). OpenCode failed to start headroom
MCP server.

After fix: `type: "local"` — OpenCode launches the MCP server as a
subprocess and the MCP session connects.

**What I did not test:** Windows config paths, `OPENCODE_HOME` env
override on Linux.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
This commit is contained in:
Peter Lodri 2026-07-15 22:36:14 +02:00 committed by GitHub
parent d50c73f2a1
commit a51bbfb6a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -419,6 +419,42 @@ def test_spec_to_entry_roundtrip() -> None:
assert restored.env == original.env
def test_spec_to_entry_no_env_has_no_environment_key() -> None:
"""_spec_to_entry omits 'environment' key when spec has no env vars."""
from headroom.mcp_registry.base import ServerSpec
spec = ServerSpec(name="headroom", command="headroom", args=("mcp", "serve"))
entry = _spec_to_entry(spec)
assert entry["type"] == "local"
assert "url" not in entry
assert "environment" not in entry
assert "env" not in entry
def test_entry_to_spec_reads_environment_field() -> None:
"""_entry_to_spec reads the 'environment' field (not legacy 'env')."""
entry = {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"environment": {"HEADROOM_PROXY_URL": "http://127.0.0.1:8787"},
"enabled": True,
}
spec = _entry_to_spec("headroom", entry)
assert spec.env == {"HEADROOM_PROXY_URL": "http://127.0.0.1:8787"}
def test_entry_to_spec_falls_back_to_legacy_env_field() -> None:
"""_entry_to_spec falls back to 'env' when 'environment' is absent."""
entry = {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"env": {"LEGACY_KEY": "value"},
"enabled": True,
}
spec = _entry_to_spec("headroom", entry)
assert spec.env == {"LEGACY_KEY": "value"}
def test_diff_specs_all_fields() -> None:
"""_diff_specs reports differences in all fields."""
from headroom.mcp_registry.base import ServerSpec