Skip to content

fix(opencode): use local MCP config - #1383

Merged
chopratejas merged 6 commits into
headroomlabs-ai:mainfrom
rudironsoni:fix/opencode-mcp-local
Jul 6, 2026
Merged

chopratejas merged 6 commits into
headroomlabs-ai:mainfrom
rudironsoni:fix/opencode-mcp-local

Conversation

@rudironsoni

@rudironsoni rudironsoni commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes OpenCode Headroom MCP configuration across wrap, MCP install/status/uninstall, and persistent install docs/CLI.

OpenCode was being configured to use a remote HTTP MCP endpoint at /mcp, but the Headroom proxy does not expose MCP there. The correct OpenCode configuration is a local stdio MCP server that runs headroom mcp serve.

Closes #1380

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Tests

Changes Made

  • Changed OpenCode MCP registration to emit type: "local" with command: ["headroom", "mcp", "serve"].
  • Changed OpenCode MCP environment serialization from env to OpenCode's environment key, while still reading legacy env entries.
  • Removed generated remote /mcp entries from OpenCode wrap/runtime config.
  • Made wrap opencode --no-mcp skip persistent mcp.headroom injection.
  • Kept provider-only OpenCode config injection from writing MCP; MCP persistence is owned by the registrar path.
  • Made headroom mcp status and headroom mcp uninstall use the registrar lifecycle so OpenCode is covered.
  • Added opencode to persistent install --target choices.
  • Clarified OpenCode persistent install docs to use --scope provider for direct opencode.json edits.
  • Added regression coverage for registrar serialization, wrap behavior, runtime config, provider-scope install, MCP CLI lifecycle, and install target parsing.

Testing

  • rtk .venv/bin/python -m pytest tests/test_mcp_registry tests/test_cli/test_mcp.py tests/test_cli/test_wrap_opencode.py tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py tests/test_install -q
  • Result after absorbing fix(opencode): write local MCP config #1381 overlap: 263 passed, 1 skipped
  • Targeted Ruff check passed for the changed Python/test files.
  • Targeted Ruff format check passed for the changed Python/test files.
  • Isolated HOME smoke tests with real opencode mcp list --pure.

Real Behavior Proof

  • headroom mcp install --agent opencode --proxy-url http://127.0.0.1:9000 --force against an isolated HOME wrote a valid local OpenCode MCP entry with environment.HEADROOM_PROXY_URL.
  • opencode mcp list --pure against that isolated HOME connected to headroom mcp serve.
  • headroom wrap opencode --prepare-only --no-rtk --no-serena --port 9001 wrote local MCP plus provider config.
  • headroom wrap opencode --prepare-only --no-rtk --no-serena --no-mcp --port 9002 wrote provider config without mcp.headroom.
  • Generated runtime OPENCODE_CONFIG_CONTENT was accepted by opencode mcp list --pure; include_mcp=False reported no MCP servers.
  • headroom mcp status detected the isolated OpenCode config and read the custom proxy URL.
  • headroom mcp uninstall removed mcp.headroom from the isolated OpenCode config while leaving provider config intact.

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the OpenCode local MCP and registrar-based mcp status/uninstall changes. I ran uv run --with pytest --with pytest-asyncio python -m pytest tests/test_mcp_registry_opencode.py tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py tests/test_cli/test_mcp.py tests/test_cli/test_install_cli.py -q: 127 passed, 3 skipped; and ruff check on the changed Python/test files passed. No code-review blockers found.

Coordination note: this overlaps heavily with #1381, which I also reviewed. Please do not merge both independently without resolving the overlap; #1383 is the broader version because it includes the mcp status/uninstall registrar lifecycle work.

@rudironsoni

Copy link
Copy Markdown
Contributor Author

Reviewed the OpenCode local MCP and registrar-based mcp status/uninstall changes. I ran uv run --with pytest --with pytest-asyncio python -m pytest tests/test_mcp_registry_opencode.py tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py tests/test_cli/test_mcp.py tests/test_cli/test_install_cli.py -q: 127 passed, 3 skipped; and ruff check on the changed Python/test files passed. No code-review blockers found.

Coordination note: this overlaps heavily with #1381, which I also reviewed. Please do not merge both independently without resolving the overlap; #1383 is the broader version because it includes the mcp status/uninstall registrar lifecycle work.

Which approach do you suggest us to follow?

@github-actions

github-actions Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Paste real command output or artifact links in Testing → Test Output.
  • Fill in Real Behavior Proof → Environment.
  • Fill in Real Behavior Proof → Exact command / steps.
  • Fill in Real Behavior Proof → Observed result.
  • Fill in Real Behavior Proof → Not tested.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added status: needs author action Pull request body or readiness checklist still needs author updates status: ci failing Required or reported CI checks are failing and removed status: ci failing Required or reported CI checks are failing labels Jun 24, 2026
@rudironsoni

Copy link
Copy Markdown
Contributor Author

Agreed. We should not merge #1381 and #1383 independently.

I kept #1383 as the merge target because it includes the broader registrar lifecycle work for headroom mcp status / headroom mcp uninstall. I also folded in the useful unique pieces from #1381:

  • changelog entry
  • clearer OpenCode docs for runtime config, plugin, and local MCP
  • provider-scope install coverage that confirms provider-only config does not add MCP
  • cleaner provider injection ownership, so inject_opencode_provider_config() no longer writes MCP at all

I also fixed the Ruff formatting failure reported by CI.

Verification after these updates:

  • rtk .venv/bin/python -m pytest tests/test_mcp_registry tests/test_cli/test_mcp.py tests/test_cli/test_wrap_opencode.py tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py tests/test_install -q
  • Result: 263 passed, 1 skipped
  • rtk .venv/bin/ruff check ... on changed Python/test files
  • rtk .venv/bin/ruff format --check ... on changed Python/test files

@rudironsoni rudironsoni mentioned this pull request Jun 24, 2026
10 of 14 tasks

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the provider-injection follow-up. The new commit correctly keeps provider/runtime OpenCode config MCP-free and leaves MCP persistence to the registrar lifecycle, while preserving existing non-Headroom MCP entries.

Local validation run:

  • uv run --extra proxy --with pytest --with pytest-asyncio python -m pytest tests/test_mcp_registry_opencode.py tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py tests/test_cli/test_mcp.py tests/test_cli/test_install_cli.py -q (133 passed)
  • uv run ruff check headroom/providers/opencode/config.py headroom/cli/wrap.py headroom/cli/install.py tests/test_mcp_registry_opencode.py tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py tests/test_cli/test_mcp.py tests/test_cli/test_install_cli.py

CI is green. Approval stands.

@rudironsoni

Copy link
Copy Markdown
Contributor Author

Re-reviewed after the provider-injection follow-up. The new commit correctly keeps provider/runtime OpenCode config MCP-free and leaves MCP persistence to the registrar lifecycle, while preserving existing non-Headroom MCP entries.

Local validation run:

  • uv run --extra proxy --with pytest --with pytest-asyncio python -m pytest tests/test_mcp_registry_opencode.py tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py tests/test_cli/test_mcp.py tests/test_cli/test_install_cli.py -q (133 passed)
  • uv run ruff check headroom/providers/opencode/config.py headroom/cli/wrap.py headroom/cli/install.py tests/test_mcp_registry_opencode.py tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py tests/test_cli/test_mcp.py tests/test_cli/test_install_cli.py

CI is green. Approval stands.

@JerrettDavis can you merge?

@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch from ab43484 to 6fa6277 Compare June 25, 2026 09:11
@github-actions github-actions Bot added the status: has conflicts Pull request has merge conflicts with the base branch label Jun 25, 2026
@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch from ca147bc to e1b23f3 Compare June 25, 2026 16:12
@github-actions github-actions Bot removed the status: has conflicts Pull request has merge conflicts with the base branch label Jun 25, 2026
@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch 2 times, most recently from 842d46e to 61a9ccf Compare June 26, 2026 11:07

@JerrettDavis JerrettDavis left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for keeping this updated. The OpenCode MCP behavior itself is moving in the right direction, and the CI is green, but the latest docs edit breaks two Markdown tables in docs/content/docs/opencode.mdx. Both the 'What wrap opencode Does' and 'Environment Variables' tables now have a header row followed immediately by data rows with no separator row, so they will render as plain text instead of tables. Please restore the separator rows (for example, |---|---|) before this is mergeable.

Given that #1381 is the narrower version of the same core OpenCode MCP fix, it would also be good to either coordinate which PR should land or split the broader mcp status/uninstall and Serena context changes into their own follow-up after the focused fix merges.

@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch 5 times, most recently from a0e766d to 3c7b967 Compare June 29, 2026 15:19

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the latest update. This clears the prior concern from my side.

The uninstall/status path now delegates to the registrar abstraction instead of duplicating Claude-specific CLI/file behavior in the CLI command, and ClaudeRegistrar still handles both the CLI and legacy ~/.claude/mcp.json fallback. The OpenCode Serena context change is also covered and matches the local-agent use case.

Local check: python -m pytest tests/test_cli/test_mcp.py tests/test_cli/test_wrap_opencode.py -q passed (48 passed, 3 skipped). CI is green.

@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch from 3c7b967 to b3ac8bf Compare June 30, 2026 20:48
Copilot AI review requested due to automatic review settings July 1, 2026 07:41
@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch from b3ac8bf to 3f5ab5e Compare July 1, 2026 07:41
@rudironsoni

Copy link
Copy Markdown
Contributor Author

@JerrettDavis can we merge?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the OpenCode integration’s MCP configuration by switching from an invalid remote /mcp HTTP endpoint to a local stdio MCP server (headroom mcp serve), and aligns MCP install/status/uninstall behavior around the registrar lifecycle so OpenCode is covered consistently.

Changes:

  • Update MCP uninstall/status commands to delegate to the MCP registrar set (instead of Claude-only config/CLI handling).
  • Remove OpenCode provider-config injection that wrote a generated Headroom MCP block into opencode.json (MCP persistence is handled via registrars / wrap behavior instead).
  • Add/adjust regression tests and update docs/changelog for the corrected OpenCode MCP wiring.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_cli/test_wrap_opencode.py Adds a regression test ensuring Serena is registered with --context agent during wrap opencode.
tests/test_cli/test_mcp.py Refactors CLI tests to validate uninstall/status via registrars rather than Claude’s legacy config file behavior.
headroom/providers/opencode/config.py Removes the generated Headroom MCP block writer from OpenCode provider config injection helpers.
headroom/cli/mcp.py Changes mcp uninstall and mcp status to enumerate registrars so OpenCode is included.
docs/content/docs/opencode.mdx Updates OpenCode docs to reflect local MCP behavior and provider-scope install guidance.
CHANGELOG.md Adds a changelog entry documenting the OpenCode MCP config fix and install target support.

Comment on lines 24 to 28
| Step | What happens |
|---|---|
| Proxy | Starts the Headroom proxy unless `--no-proxy` is set |
| Provider config | Writes a `headroom` provider using `@ai-sdk/openai-compatible` into `opencode.json` and `OPENCODE_CONFIG_CONTENT`, pointing at `http://127.0.0.1:<port>/v1` |
| Runtime env | Sets `OPENCODE_CONFIG_CONTENT` so OpenCode reads provider, model, plugin, and local MCP config at launch |
| Provider injection | Writes a `headroom` provider using `@ai-sdk/openai-compatible` into `opencode.json`, pointing at `http://127.0.0.1:<port>/v1` |
| Runtime env | Sets `OPENCODE_CONFIG_CONTENT` with provider, plugin, and local MCP config so OpenCode picks up Headroom at launch |
| Provider compatibility | Leaves `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` untouched so OpenCode `/connect` providers keep their own routing |
Comment on lines 69 to 72
| Variable | Description |
|---|---|
| `OPENCODE_CONFIG_CONTENT` | JSON payload with provider, model, plugin, and optional local MCP config injected by `wrap` |
| `HEADROOM_PROXY_URL` | Proxy URL passed to the native `headroom-opencode` plugin. Defaults to `http://127.0.0.1:8787` inside the plugin |
| `OPENCODE_CONFIG_CONTENT` | JSON payload with provider, plugin, and optional local MCP config injected by `wrap` |
| `HEADROOM_PROXY_URL` | Proxy URL passed to Headroom MCP when a non-default port is used, and to the native plugin when configured |
| `HEADROOM_CONTEXT_TOOL` | Set to `lean-ctx` to use lean-ctx instead of RTK |
@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch from 3f5ab5e to 962394c Compare July 1, 2026 09:19

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the latest OpenCode docs/table follow-up. The previously-approved local MCP behavior is unchanged; the final commit restores the markdown table separators and keeps the runtime config wording accurate as optional MCP config. CI is green on the latest head, and I do not see a remaining code-review blocker.

@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch 3 times, most recently from d3b5d97 to 8a5c7ea Compare July 4, 2026 12:04
@rudironsoni
rudironsoni force-pushed the fix/opencode-mcp-local branch from 8a5c7ea to b0e61c4 Compare July 6, 2026 10:18
@chopratejas
chopratejas merged commit 4bd3ddf into headroomlabs-ai:main Jul 6, 2026
31 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 9, 2026
chopratejas pushed a commit that referenced this pull request Jul 9, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.31.0</summary>

##
[0.31.0](v0.30.0...v0.31.0)
(2026-07-09)


### Features

* **cache:** provider-agnostic cache-mode delta + cc-agnostic prefix
comparison
([#1868](#1868))
([7c2f0ea](7c2f0ea))
* **ccr:** wire retrieve-tool interception into OpenAI Responses handler
([#1898](#1898))
([62cd307](62cd307))
* **compression:** add audit-safe mode with protected pattern matching
([#1899](#1899))
([bb112dd](bb112dd))
* **content-router:** accept any real compression (remove min-savings
floor)
([#1771](#1771))
([6c31db9](6c31db9))
* **content-router:** lossless-first dispatch, cross-turn dedup, and A7
lossy-after-fold
([#1818](#1818))
([60af15f](60af15f))
* **proxy:** add provider-only HTTP proxy
([#1807](#1807))
([ebe0a3b](ebe0a3b))
* **proxy:** add turn-hook extension point for buffered model turns
([#1891](#1891))
([ec950f7](ec950f7))


### Bug Fixes

* **build:** enable Intel macOS pip installs via ort-load-dynamic
([#1538](#1538))
([32ce99e](32ce99e))
* **cache:** avoid fallback session collisions
([#1827](#1827))
([0f606b6](0f606b6))
* **ccr:** make expired retrieve misses terminal
([#1781](#1781))
([9cbdba4](9cbdba4))
* **ccr:** preserve Anthropic re-stream shape
([#1854](#1854))
([f663894](f663894))
* **ccr:** preserve thinking blocks in buffered stream re-synthesis
([#1897](#1897))
([ede085c](ede085c))
* **cli/proxy:** preserve explicit HEADROOM_MIN_TOKENS=0 / MAX_ITEMS=0
([#1886](#1886))
([3a33af1](3a33af1))
* **code-compressor:** CJK-aware relevance-query symbol matching
([#1747](#1747))
([b38315c](b38315c))
* **codex:** discover updated Codex state stores
([#1889](#1889))
([9d42eba](9d42eba))
* **codex:** OpenCode Zen telemetry attribution
([#1648](#1648))
([f18c6bd](f18c6bd))
* **content-detector:** detect and compress space-separated JSON objects
([#1742](#1742))
([5194bdc](5194bdc))
* **content-router:** token-measure lossless folds at the acceptance
gate ([#1772](#1772))
([c5493ea](c5493ea))
* **copilot:** normalize subscription routing host
([#1836](#1836))
([afd9cbd](afd9cbd))
* **copilot:** route mixed-model requests per model
([#1785](#1785))
([5af5e22](5af5e22))
* **dashboard:** deduplicate repeated savings metrics
([#1804](#1804))
([88f935a](88f935a))
* **dashboard:** distinguish unavailable RTK from zero stats in Docker
([#1900](#1900))
([87f6e93](87f6e93))
* **dashboard:** distinguish unavailable RTK from zero stats in Docker
([#1901](#1901))
([361adcd](361adcd))
* **dashboard:** price proxy savings without litellm
([#1728](#1728))
([188e382](188e382))
* detect and clear stale ANTHROPIC_BASE_URL from crashed wrap sessions
([#1768](#1768))
([#1837](#1837))
([84509a4](84509a4))
* **docker:** persist headroom workspace in compose
([#1839](#1839))
([5e29c06](5e29c06))
* **docker:** report source build version
([#1862](#1862))
([3807488](3807488))
* **evals:** default unparseable judge scores below pass threshold
([#1892](#1892))
([42ebbc6](42ebbc6))
* **install:** pass sc.exe create as raw command line so binPath=
quoting survives
([#1654](#1654))
([#1702](#1702))
([d6e0710](d6e0710))
* **install:** persist --no-http2 override through install apply
([#1676](#1676))
([6fb5f3b](6fb5f3b))
* **mcp:** isolate ClaudeRegistrar CLI config env
([#1888](#1888))
([1c947b1](1c947b1))
* **mcp:** surface dead proxy state
([#1786](#1786))
([931eed8](931eed8))
* **memory:** resolve Trae cwd metadata from user reminders
([#1737](#1737))
([#1887](#1887))
([3e85eb1](3e85eb1))
* **opencode:** use local MCP config
([#1383](#1383))
([4bd3ddf](4bd3ddf))
* **proxy/openai:** thread savings-profile kwargs into chat completions
([#1606](#1606))
([7ff842d](7ff842d))
* **proxy/openai:** translate max_tokens -&gt; max_completion_tokens on
chat path
([#1774](#1774))
([285808b](285808b))
* **proxy:** bound Codex WS compression fallback latency
([#1802](#1802))
([d24a3f8](d24a3f8))
* **proxy:** bound HF tokenizer load and offload token counting off
event loop
([#1738](#1738))
([46d5d68](46d5d68))
* **proxy:** cancel retry backoff on shutdown
([#1834](#1834))
([da2d8dc](da2d8dc))
* **proxy:** compress Anthropic user text blocks when enabled
([#1875](#1875))
([e36439a](e36439a))
* **proxy:** freeze must forward cached (compressed) prefix
byte-identical — stop token-mode cache busting
([#1850](#1850))
([248ae0f](248ae0f))
* **proxy:** fsync savings dir after atomic rename
([#1764](#1764))
([7de2c1e](7de2c1e))
* **proxy:** keep cache_control bounded + stable so the freeze overlay
stops busting
([#1852](#1852))
([4820134](4820134))
* **proxy:** persist lifetime cache-read savings across restarts
([#1665](#1665))
([908997e](908997e))
* **proxy:** preserve streaming passthrough beta headers
([#1783](#1783))
([0f553a8](0f553a8))
* **proxy:** release _active_streams session lock on setup-phase errors
([#1864](#1864))
([2ccd831](2ccd831))
* **proxy:** retry HTTP/2 stream resets instead of 502ing
([#1645](#1645))
([2ce19c2](2ce19c2))
* **proxy:** retry passthrough on transient upstream connection close
([#1513](#1513))
([5d14080](5d14080))
* **proxy:** route Foundry Anthropic messages
([#1878](#1878))
([739f654](739f654))
* **proxy:** serve /favicon.ico locally instead of tunneling upstream
([#1787](#1787))
([#1847](#1847))
([3076e32](3076e32))
* **proxy:** stop rtk stat failures from corrupting session baseline
([#1693](#1693))
([681b9a8](681b9a8))
* **proxy:** strip 1m model suffix before upstream forwarding
([#1840](#1840))
([e22d745](e22d745))
* **proxy:** subtract cache write premiums from net savings
([#1800](#1800))
([53a465b](53a465b))
* **router:** honor MCP aliases in excluded tools
([#1822](#1822))
([#1863](#1863))
([140d6e4](140d6e4))
* **rtk:** link managed rtk onto PATH instead of mutating the hook
([#1698](#1698))
([140cb05](140cb05))
* **streaming:** preserve server_tool_use sse blocks
([#1826](#1826))
([4ac5493](4ac5493))
* **toin:** publish skip compression recommendations
([#1782](#1782))
([be51008](be51008))
* **transforms:** normalize diff compressor context
([#1801](#1801))
([838c523](838c523))
* **transforms:** pass through ragged tables instead of misaligning
columns
([#1713](#1713))
([c7665ca](c7665ca))
* use rtk native Cursor hook instead of injecting .cursorrules
([#756](#756))
([#1846](#1846))
([1573f1f](1573f1f))
* **wrap:** replace stale-proxy detection with Vite-style port fallback
([#1406](#1406))
([b4205c6](b4205c6))


### Performance Improvements

* **proxy:** cap compression workers to CPU count
([#1803](#1803))
([0a3851b](0a3851b))
* **savings:** batch tracker persistence off the request hot path
([#1817](#1817))
([451b9f0](451b9f0))


### Dependencies

* bump the cargo-minor-patch group across 1 directory with 7 updates
([#1909](#1909))
([45601d9](45601d9))
* bump the npm-minor-patch group across 4 directories with 18 updates
([#1907](#1907))
([8872bbc](8872bbc))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
JerrettDavis pushed a commit to peterlodri-sec/headroom that referenced this pull request Jul 14, 2026
## Description

Fixes OpenCode Headroom MCP configuration across wrap, MCP
install/status/uninstall, and persistent install docs/CLI.

OpenCode was being configured to use a remote HTTP MCP endpoint at
`/mcp`, but the Headroom proxy does not expose MCP there. The correct
OpenCode configuration is a local stdio MCP server that runs `headroom
mcp serve`.

Closes headroomlabs-ai#1380

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [x] Documentation update
- [x] Tests

## Changes Made

- Changed OpenCode MCP registration to emit `type: "local"` with
`command: ["headroom", "mcp", "serve"]`.
- Changed OpenCode MCP environment serialization from `env` to
OpenCode's `environment` key, while still reading legacy `env` entries.
- Removed generated remote `/mcp` entries from OpenCode wrap/runtime
config.
- Made `wrap opencode --no-mcp` skip persistent `mcp.headroom`
injection.
- Kept provider-only OpenCode config injection from writing MCP; MCP
persistence is owned by the registrar path.
- Made `headroom mcp status` and `headroom mcp uninstall` use the
registrar lifecycle so OpenCode is covered.
- Added `opencode` to persistent install `--target` choices.
- Clarified OpenCode persistent install docs to use `--scope provider`
for direct `opencode.json` edits.
- Added regression coverage for registrar serialization, wrap behavior,
runtime config, provider-scope install, MCP CLI lifecycle, and install
target parsing.

## Testing

- [x] `rtk .venv/bin/python -m pytest tests/test_mcp_registry
tests/test_cli/test_mcp.py tests/test_cli/test_wrap_opencode.py
tests/test_providers_opencode_config.py
tests/test_providers_opencode_install.py tests/test_install -q`
- [x] Result after absorbing headroomlabs-ai#1381 overlap: `263 passed, 1 skipped`
- [x] Targeted Ruff check passed for the changed Python/test files.
- [x] Targeted Ruff format check passed for the changed Python/test
files.
- [x] Isolated HOME smoke tests with real `opencode mcp list --pure`.

## Real Behavior Proof

- `headroom mcp install --agent opencode --proxy-url
http://127.0.0.1:9000 --force` against an isolated HOME wrote a valid
local OpenCode MCP entry with `environment.HEADROOM_PROXY_URL`.
- `opencode mcp list --pure` against that isolated HOME connected to
`headroom mcp serve`.
- `headroom wrap opencode --prepare-only --no-rtk --no-serena --port
9001` wrote local MCP plus provider config.
- `headroom wrap opencode --prepare-only --no-rtk --no-serena --no-mcp
--port 9002` wrote provider config without `mcp.headroom`.
- Generated runtime `OPENCODE_CONFIG_CONTENT` was accepted by `opencode
mcp list --pure`; `include_mcp=False` reported no MCP servers.
- `headroom mcp status` detected the isolated OpenCode config and read
the custom proxy URL.
- `headroom mcp uninstall` removed `mcp.headroom` from the isolated
OpenCode config while leaving provider config intact.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review
JerrettDavis pushed a commit to peterlodri-sec/headroom that referenced this pull request Jul 14, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.31.0</summary>

##
[0.31.0](headroomlabs-ai/headroom@v0.30.0...v0.31.0)
(2026-07-09)


### Features

* **cache:** provider-agnostic cache-mode delta + cc-agnostic prefix
comparison
([headroomlabs-ai#1868](headroomlabs-ai#1868))
([7c2f0ea](headroomlabs-ai@7c2f0ea))
* **ccr:** wire retrieve-tool interception into OpenAI Responses handler
([headroomlabs-ai#1898](headroomlabs-ai#1898))
([62cd307](headroomlabs-ai@62cd307))
* **compression:** add audit-safe mode with protected pattern matching
([headroomlabs-ai#1899](headroomlabs-ai#1899))
([bb112dd](headroomlabs-ai@bb112dd))
* **content-router:** accept any real compression (remove min-savings
floor)
([headroomlabs-ai#1771](headroomlabs-ai#1771))
([6c31db9](headroomlabs-ai@6c31db9))
* **content-router:** lossless-first dispatch, cross-turn dedup, and A7
lossy-after-fold
([headroomlabs-ai#1818](headroomlabs-ai#1818))
([60af15f](headroomlabs-ai@60af15f))
* **proxy:** add provider-only HTTP proxy
([headroomlabs-ai#1807](headroomlabs-ai#1807))
([ebe0a3b](headroomlabs-ai@ebe0a3b))
* **proxy:** add turn-hook extension point for buffered model turns
([headroomlabs-ai#1891](headroomlabs-ai#1891))
([ec950f7](headroomlabs-ai@ec950f7))


### Bug Fixes

* **build:** enable Intel macOS pip installs via ort-load-dynamic
([headroomlabs-ai#1538](headroomlabs-ai#1538))
([32ce99e](headroomlabs-ai@32ce99e))
* **cache:** avoid fallback session collisions
([headroomlabs-ai#1827](headroomlabs-ai#1827))
([0f606b6](headroomlabs-ai@0f606b6))
* **ccr:** make expired retrieve misses terminal
([headroomlabs-ai#1781](headroomlabs-ai#1781))
([9cbdba4](headroomlabs-ai@9cbdba4))
* **ccr:** preserve Anthropic re-stream shape
([headroomlabs-ai#1854](headroomlabs-ai#1854))
([f663894](headroomlabs-ai@f663894))
* **ccr:** preserve thinking blocks in buffered stream re-synthesis
([headroomlabs-ai#1897](headroomlabs-ai#1897))
([ede085c](headroomlabs-ai@ede085c))
* **cli/proxy:** preserve explicit HEADROOM_MIN_TOKENS=0 / MAX_ITEMS=0
([headroomlabs-ai#1886](headroomlabs-ai#1886))
([3a33af1](headroomlabs-ai@3a33af1))
* **code-compressor:** CJK-aware relevance-query symbol matching
([headroomlabs-ai#1747](headroomlabs-ai#1747))
([b38315c](headroomlabs-ai@b38315c))
* **codex:** discover updated Codex state stores
([headroomlabs-ai#1889](headroomlabs-ai#1889))
([9d42eba](headroomlabs-ai@9d42eba))
* **codex:** OpenCode Zen telemetry attribution
([headroomlabs-ai#1648](headroomlabs-ai#1648))
([f18c6bd](headroomlabs-ai@f18c6bd))
* **content-detector:** detect and compress space-separated JSON objects
([headroomlabs-ai#1742](headroomlabs-ai#1742))
([5194bdc](headroomlabs-ai@5194bdc))
* **content-router:** token-measure lossless folds at the acceptance
gate ([headroomlabs-ai#1772](headroomlabs-ai#1772))
([c5493ea](headroomlabs-ai@c5493ea))
* **copilot:** normalize subscription routing host
([headroomlabs-ai#1836](headroomlabs-ai#1836))
([afd9cbd](headroomlabs-ai@afd9cbd))
* **copilot:** route mixed-model requests per model
([headroomlabs-ai#1785](headroomlabs-ai#1785))
([5af5e22](headroomlabs-ai@5af5e22))
* **dashboard:** deduplicate repeated savings metrics
([headroomlabs-ai#1804](headroomlabs-ai#1804))
([88f935a](headroomlabs-ai@88f935a))
* **dashboard:** distinguish unavailable RTK from zero stats in Docker
([headroomlabs-ai#1900](headroomlabs-ai#1900))
([87f6e93](headroomlabs-ai@87f6e93))
* **dashboard:** distinguish unavailable RTK from zero stats in Docker
([headroomlabs-ai#1901](headroomlabs-ai#1901))
([361adcd](headroomlabs-ai@361adcd))
* **dashboard:** price proxy savings without litellm
([headroomlabs-ai#1728](headroomlabs-ai#1728))
([188e382](headroomlabs-ai@188e382))
* detect and clear stale ANTHROPIC_BASE_URL from crashed wrap sessions
([headroomlabs-ai#1768](headroomlabs-ai#1768))
([headroomlabs-ai#1837](headroomlabs-ai#1837))
([84509a4](headroomlabs-ai@84509a4))
* **docker:** persist headroom workspace in compose
([headroomlabs-ai#1839](headroomlabs-ai#1839))
([5e29c06](headroomlabs-ai@5e29c06))
* **docker:** report source build version
([headroomlabs-ai#1862](headroomlabs-ai#1862))
([3807488](headroomlabs-ai@3807488))
* **evals:** default unparseable judge scores below pass threshold
([headroomlabs-ai#1892](headroomlabs-ai#1892))
([42ebbc6](headroomlabs-ai@42ebbc6))
* **install:** pass sc.exe create as raw command line so binPath=
quoting survives
([headroomlabs-ai#1654](headroomlabs-ai#1654))
([headroomlabs-ai#1702](headroomlabs-ai#1702))
([d6e0710](headroomlabs-ai@d6e0710))
* **install:** persist --no-http2 override through install apply
([headroomlabs-ai#1676](headroomlabs-ai#1676))
([6fb5f3b](headroomlabs-ai@6fb5f3b))
* **mcp:** isolate ClaudeRegistrar CLI config env
([headroomlabs-ai#1888](headroomlabs-ai#1888))
([1c947b1](headroomlabs-ai@1c947b1))
* **mcp:** surface dead proxy state
([headroomlabs-ai#1786](headroomlabs-ai#1786))
([931eed8](headroomlabs-ai@931eed8))
* **memory:** resolve Trae cwd metadata from user reminders
([headroomlabs-ai#1737](headroomlabs-ai#1737))
([headroomlabs-ai#1887](headroomlabs-ai#1887))
([3e85eb1](headroomlabs-ai@3e85eb1))
* **opencode:** use local MCP config
([headroomlabs-ai#1383](headroomlabs-ai#1383))
([4bd3ddf](headroomlabs-ai@4bd3ddf))
* **proxy/openai:** thread savings-profile kwargs into chat completions
([headroomlabs-ai#1606](headroomlabs-ai#1606))
([7ff842d](headroomlabs-ai@7ff842d))
* **proxy/openai:** translate max_tokens -&gt; max_completion_tokens on
chat path
([headroomlabs-ai#1774](headroomlabs-ai#1774))
([285808b](headroomlabs-ai@285808b))
* **proxy:** bound Codex WS compression fallback latency
([headroomlabs-ai#1802](headroomlabs-ai#1802))
([d24a3f8](headroomlabs-ai@d24a3f8))
* **proxy:** bound HF tokenizer load and offload token counting off
event loop
([headroomlabs-ai#1738](headroomlabs-ai#1738))
([46d5d68](headroomlabs-ai@46d5d68))
* **proxy:** cancel retry backoff on shutdown
([headroomlabs-ai#1834](headroomlabs-ai#1834))
([da2d8dc](headroomlabs-ai@da2d8dc))
* **proxy:** compress Anthropic user text blocks when enabled
([headroomlabs-ai#1875](headroomlabs-ai#1875))
([e36439a](headroomlabs-ai@e36439a))
* **proxy:** freeze must forward cached (compressed) prefix
byte-identical — stop token-mode cache busting
([headroomlabs-ai#1850](headroomlabs-ai#1850))
([248ae0f](headroomlabs-ai@248ae0f))
* **proxy:** fsync savings dir after atomic rename
([headroomlabs-ai#1764](headroomlabs-ai#1764))
([7de2c1e](headroomlabs-ai@7de2c1e))
* **proxy:** keep cache_control bounded + stable so the freeze overlay
stops busting
([headroomlabs-ai#1852](headroomlabs-ai#1852))
([4820134](headroomlabs-ai@4820134))
* **proxy:** persist lifetime cache-read savings across restarts
([headroomlabs-ai#1665](headroomlabs-ai#1665))
([908997e](headroomlabs-ai@908997e))
* **proxy:** preserve streaming passthrough beta headers
([headroomlabs-ai#1783](headroomlabs-ai#1783))
([0f553a8](headroomlabs-ai@0f553a8))
* **proxy:** release _active_streams session lock on setup-phase errors
([headroomlabs-ai#1864](headroomlabs-ai#1864))
([2ccd831](headroomlabs-ai@2ccd831))
* **proxy:** retry HTTP/2 stream resets instead of 502ing
([headroomlabs-ai#1645](headroomlabs-ai#1645))
([2ce19c2](headroomlabs-ai@2ce19c2))
* **proxy:** retry passthrough on transient upstream connection close
([headroomlabs-ai#1513](headroomlabs-ai#1513))
([5d14080](headroomlabs-ai@5d14080))
* **proxy:** route Foundry Anthropic messages
([headroomlabs-ai#1878](headroomlabs-ai#1878))
([739f654](headroomlabs-ai@739f654))
* **proxy:** serve /favicon.ico locally instead of tunneling upstream
([headroomlabs-ai#1787](headroomlabs-ai#1787))
([headroomlabs-ai#1847](headroomlabs-ai#1847))
([3076e32](headroomlabs-ai@3076e32))
* **proxy:** stop rtk stat failures from corrupting session baseline
([headroomlabs-ai#1693](headroomlabs-ai#1693))
([681b9a8](headroomlabs-ai@681b9a8))
* **proxy:** strip 1m model suffix before upstream forwarding
([headroomlabs-ai#1840](headroomlabs-ai#1840))
([e22d745](headroomlabs-ai@e22d745))
* **proxy:** subtract cache write premiums from net savings
([headroomlabs-ai#1800](headroomlabs-ai#1800))
([53a465b](headroomlabs-ai@53a465b))
* **router:** honor MCP aliases in excluded tools
([headroomlabs-ai#1822](headroomlabs-ai#1822))
([headroomlabs-ai#1863](headroomlabs-ai#1863))
([140d6e4](headroomlabs-ai@140d6e4))
* **rtk:** link managed rtk onto PATH instead of mutating the hook
([headroomlabs-ai#1698](headroomlabs-ai#1698))
([140cb05](headroomlabs-ai@140cb05))
* **streaming:** preserve server_tool_use sse blocks
([headroomlabs-ai#1826](headroomlabs-ai#1826))
([4ac5493](headroomlabs-ai@4ac5493))
* **toin:** publish skip compression recommendations
([headroomlabs-ai#1782](headroomlabs-ai#1782))
([be51008](headroomlabs-ai@be51008))
* **transforms:** normalize diff compressor context
([headroomlabs-ai#1801](headroomlabs-ai#1801))
([838c523](headroomlabs-ai@838c523))
* **transforms:** pass through ragged tables instead of misaligning
columns
([headroomlabs-ai#1713](headroomlabs-ai#1713))
([c7665ca](headroomlabs-ai@c7665ca))
* use rtk native Cursor hook instead of injecting .cursorrules
([headroomlabs-ai#756](headroomlabs-ai#756))
([headroomlabs-ai#1846](headroomlabs-ai#1846))
([1573f1f](headroomlabs-ai@1573f1f))
* **wrap:** replace stale-proxy detection with Vite-style port fallback
([headroomlabs-ai#1406](headroomlabs-ai#1406))
([b4205c6](headroomlabs-ai@b4205c6))


### Performance Improvements

* **proxy:** cap compression workers to CPU count
([headroomlabs-ai#1803](headroomlabs-ai#1803))
([0a3851b](headroomlabs-ai@0a3851b))
* **savings:** batch tracker persistence off the request hot path
([headroomlabs-ai#1817](headroomlabs-ai#1817))
([451b9f0](headroomlabs-ai@451b9f0))


### Dependencies

* bump the cargo-minor-patch group across 1 directory with 7 updates
([headroomlabs-ai#1909](headroomlabs-ai#1909))
([45601d9](headroomlabs-ai@45601d9))
* bump the npm-minor-patch group across 4 directories with 18 updates
([headroomlabs-ai#1907](headroomlabs-ai#1907))
([8872bbc](headroomlabs-ai@8872bbc))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@rudironsoni
rudironsoni deleted the fix/opencode-mcp-local branch July 15, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenCode integration writes invalid MCP config across wrap, mcp install, and persistent install

4 participants