fix(opencode): write local MCP config (#1381)

## Description

Fixes the OpenCode config corruption reported in #1380 for wrap, MCP
registration, and provider-scope install paths.

OpenCode MCP entries are local stdio servers, not remote HTTP endpoints.
This changes Headroom's OpenCode MCP serialization to write `type:
"local"` with `command: ["headroom", "mcp", "serve"]`, uses OpenCode's
`environment` field for MCP env vars, and still reads the older `env`
key for compatibility.

This also stops provider-only OpenCode config injection from creating a
fake `http://127.0.0.1:<port>/mcp` entry, so `headroom wrap opencode
--no-mcp` no longer leaves `mcp.headroom` behind. Finally, the install
CLI/docs now accept and document `--target opencode` with provider
scope.

This does not change the broader `headroom mcp status/uninstall`
behavior from #1380; that looks like a separate follow-up.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Write OpenCode MCP entries as local stdio config instead of remote
`/mcp` config.
- Use `environment` for OpenCode MCP env vars while continuing to read
legacy `env` entries.
- Stop OpenCode provider injection/persistent provider install from
adding MCP config.
- Keep `--no-mcp` from writing `mcp.headroom` while preserving other MCP
entries such as Serena.
- Allow `headroom install apply --target opencode` at the CLI layer.
- Update OpenCode docs and changelog.

## Testing

- [x] Focused unit tests pass
- [x] Linting passes (`ruff check .`)
- [x] Formatting passes (`ruff format --check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for the fixed behavior
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_mcp_registry_opencode.py tests/test_cli/test_wrap_opencode.py tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py tests/test_cli/test_install_cli.py tests/test_install/test_providers.py
Pytest: 164 passed

$ uvx ruff check .
All checks passed!

$ uvx ruff format --check .
986 files already formatted

$ uvx mypy --config-file pyproject.toml headroom
Success: no issues found in 398 source files
```

## Real Behavior Proof

- Environment: macOS local worktree at
`/Users/vinaygupta/Desktop/git/headroom-fix-opencode-mcp-config`; branch
`fix-opencode-mcp-config`; commit `aea96208`.
- Exact command / steps: ran the focused OpenCode/installer regression
suite plus Ruff lint/format checks and mypy commands shown above.
- Observed result: the focused tests pass and cover OpenCode MCP
serialization as `type: "local"`, `command: ["headroom", "mcp",
"serve"]`, `environment` env vars, `--no-mcp` not writing
`mcp.headroom`, provider-scope install not adding MCP config, and
`install apply --target opencode` being accepted.
- Not tested: full `pytest` locally, because collection requires the
native `headroom._core` extension in this worktree. Attempting the
project runner hit a local native build failure first: `esaxx-rs` failed
compiling `src/esaxx.cpp` with `fatal error: 'cstdint' file not found`.
The broader generic `headroom mcp status/uninstall` behavior from #1380
is intentionally left for a follow-up.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

Scope note: generic `mcp status/uninstall` support from #1380 is
intentionally left as a separate follow-up PR.
This commit is contained in:
Vinay Gupta 2026-06-26 12:23:54 -05:00 committed by GitHub
parent b09f027062
commit 6c83790680
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 156 additions and 56 deletions

View file

@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* **proxy:** add `--protect-tool-results` / `HEADROOM_PROTECT_TOOL_RESULTS` to prevent lossy compression of exact-output tool results (e.g. `Bash cat`/`grep` results) — closes [#1307](https://github.com/headroomlabs-ai/headroom/issues/1307).
* **cli:** add `--rpm`/`--tpm` and `HEADROOM_RPM`/`HEADROOM_TPM` to the Click proxy command for rate-limit parity with the legacy CLI -- closes [#1350](https://github.com/headroomlabs-ai/headroom/issues/1350) (Problem 1).
* **proxy:** register `ToolResultInterceptorTransform` in explicit transforms list when `HEADROOM_INTERCEPT_ENABLED` is set — closes [#829](https://github.com/headroomlabs-ai/headroom/issues/829).
* **opencode:** write Headroom MCP config as a local stdio server instead of a remote `/mcp` URL, keep provider-only installs from adding MCP config, and allow `install apply --target opencode` ([#1380](https://github.com/headroomlabs-ai/headroom/issues/1380)).
* **code:** keep Python `from __future__` imports before executable code during AST compression and validate compressed Python with `compile(..., "exec")` so compile-time syntax rules are enforced ([#1233](https://github.com/chopratejas/headroom/issues/1233)).
* **proxy:** report real input tokens on the streaming `message_start` event for LiteLLM/Bedrock-backed requests. LiteLLM streaming never surfaces prompt tokens mid-stream, so `message_start.usage.input_tokens` was always `0`; Anthropic clients (e.g. Claude Code) read input-token metrics from that event, underreporting token usage by ~99% in OTel/CloudWatch dashboards. The Bedrock streamer now backfills `input_tokens` with the count Headroom actually sent upstream when the backend leaves it unset, preserving any non-zero value the backend genuinely reports ([#1132](https://github.com/chopratejas/headroom/issues/1132)).
* **proxy:** give buffered Anthropic request paths their own longer read timeout, so long `/v1/messages` turns and Anthropic batch or passthrough reads no longer trip the generic proxy cap while unrelated request timeouts stay unchanged.

View file

@ -24,11 +24,11 @@ headroom unwrap opencode
| 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` |
| Runtime env | Sets `OPENCODE_CONFIG_CONTENT` so OpenCode reads provider, model, and MCP config at launch |
| Provider compatibility | Leaves `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` untouched so `/connect` providers keep their own routing |
| Context tool | Injects RTK or `lean-ctx` instructions into OpenCode AGENTS files |
| MCP setup | Registers Headroom MCP tools: `headroom_compress`, `headroom_retrieve`, and `headroom_stats` |
| 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 compatibility | Leaves `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` untouched so OpenCode `/connect` providers keep their own routing |
| Context tool | Injects RTK or `lean-ctx` instructions into `~/.config/opencode/AGENTS.md` and project `AGENTS.md` |
| MCP setup | Registers Headroom MCP tools through a local stdio server: `headroom_compress`, `headroom_retrieve`, and `headroom_stats` |
| Serena MCP | Optionally registers Serena code graph tools. Use `--no-serena` to skip it |
| Backup | Snapshots `opencode.json` to `opencode.json.headroom-backup` before changing it |
| Launch | Starts the `opencode` binary with the generated config |
@ -36,7 +36,8 @@ headroom unwrap opencode
## Options
```bash
headroom wrap opencode --port 8787 \ # Proxy port. Defaults to a random available port
headroom wrap opencode \
--port 8787 \ # Proxy port. Defaults to a random available port
--no-rtk \ # Skip RTK context tool injection
--no-mcp \ # Skip Headroom MCP registration
--no-serena \ # Skip Serena code graph MCP
@ -68,7 +69,7 @@ The default model is `headroom/claude-sonnet-4-6`. Change it in `opencode.json`
| Variable | Description |
|---|---|
| `OPENCODE_CONFIG_CONTENT` | JSON payload with provider, model, and MCP config injected by `wrap` |
| `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 |
| `HEADROOM_CONTEXT_TOOL` | Set to `lean-ctx` to use lean-ctx instead of RTK |
@ -87,16 +88,12 @@ See [Failure Learning](/docs/failure-learning) for details on the learn system.
`headroom install` supports OpenCode as a target for persistent provider wiring:
```bash
headroom install apply --preset persistent-service --providers manual --target opencode
headroom install apply --preset persistent-service --scope provider --providers manual --target opencode
```
This writes the Headroom provider into `~/.config/opencode/opencode.json` and keeps the proxy running on port 8787.
Provider scope is also supported:
```bash
headroom install apply --preset persistent-service --scope provider --providers manual --target opencode
```
Provider-only installs do not write Headroom MCP config. MCP tools are added by `headroom wrap opencode` unless `--no-mcp` is set.
## Native OpenCode Plugin
@ -114,7 +111,7 @@ export default async function plugin(input) {
}
```
Use this plugin when OpenCode should intercept provider traffic in-process. Use `headroom wrap opencode` when you want the CLI to manage the proxy, config injection, MCP registration, backups, and unwrap behavior.
Use this plugin when OpenCode should intercept provider traffic in-process. Use `headroom wrap opencode` when you want the CLI to manage the proxy, config injection, local MCP registration, backups, and unwrap behavior.
## Programmatic Config Helpers
@ -140,8 +137,8 @@ const retrieve = createHeadroomRetrieveTool({
## How It Works Under The Hood
1. **Config injection**. The wrapper writes a `provider.headroom` block into `opencode.json`. The provider uses `@ai-sdk/openai-compatible`, which OpenCode supports natively. Model mappings route requests through `http://127.0.0.1:<port>/v1`.
2. **Runtime config**. `OPENCODE_CONFIG_CONTENT` is set as an env var containing the full provider, model, and MCP JSON. OpenCode reads it at startup and merges it with on-disk config.
3. **MCP tools**. Headroom registers `headroom_compress`, `headroom_retrieve`, and `headroom_stats` unless `--no-mcp` is set.
2. **Runtime config**. `OPENCODE_CONFIG_CONTENT` is set as an env var containing the full provider, model, plugin, and local MCP JSON. OpenCode reads it at startup and merges it with on-disk config.
3. **MCP tools**. Headroom registers `headroom_compress`, `headroom_retrieve`, and `headroom_stats` through a local stdio MCP server unless `--no-mcp` is set.
4. **Native plugin path**. `HeadroomPlugin` installs Headroom transport interception and uses `HEADROOM_PROXY_URL` or `http://127.0.0.1:8787` to reach the proxy.
5. **Unwrap**. `headroom unwrap opencode` restores `opencode.json` from the pre-wrap backup when present, strips Headroom marker blocks when no backup exists, and unregisters Headroom MCP servers.

View file

@ -135,7 +135,7 @@ def _reject_task_lifecycle(manifest: DeploymentManifest, action: str) -> None:
"--target",
"targets",
multiple=True,
type=click.Choice(["claude", "copilot", "codex", "aider", "cursor", "openclaw"]),
type=click.Choice(["claude", "copilot", "codex", "aider", "cursor", "openclaw", "opencode"]),
help="Tool target to configure when --providers manual is used.",
)
@click.option("--profile", default="default", show_default=True, help="Deployment profile name.")

View file

@ -63,7 +63,7 @@ def _entry_to_spec(name: str, entry: dict[str, Any]) -> ServerSpec:
else:
command = str(command_value) if command_value else ""
args = ()
env_value = entry.get("env", {})
env_value = entry.get("environment", entry.get("env", {}))
env: dict[str, str] = {}
if isinstance(env_value, dict):
env = {str(k): str(v) for k, v in env_value.items()}
@ -72,16 +72,12 @@ def _entry_to_spec(name: str, entry: dict[str, Any]) -> ServerSpec:
def _spec_to_entry(spec: ServerSpec) -> dict[str, Any]:
entry: dict[str, Any] = {
"type": "remote",
"url": "",
"type": "local",
"command": [spec.command, *spec.args],
"enabled": True,
}
if spec.args:
entry["command"] = [spec.command, *spec.args]
else:
entry["command"] = spec.command
if spec.env:
entry["env"] = dict(spec.env)
entry["environment"] = dict(spec.env)
return entry

View file

@ -12,6 +12,7 @@ from typing import Any
import click
from headroom.install.paths import opencode_config_path
from headroom.mcp_registry.install import DEFAULT_PROXY_URL
# Headroom-managed JSON marker comments for idempotent block injection.
_PROVIDER_MARKER_START = "// --- Headroom proxy provider ---"
@ -98,12 +99,16 @@ def _render_provider_block(port: int) -> str:
def _render_mcp_block(port: int) -> str:
"""Render a Headroom MCP block as a JSON comment-wrapped snippet."""
proxy_url = f"http://127.0.0.1:{port}"
mcp_entry: dict[str, Any] = {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
}
if proxy_url != DEFAULT_PROXY_URL:
mcp_entry["environment"] = {"HEADROOM_PROXY_URL": proxy_url}
mcp = {
"headroom": {
"type": "remote",
"url": f"http://127.0.0.1:{port}/mcp",
"enabled": True,
}
"headroom": mcp_entry,
}
lines = [
_MCP_MARKER_START,
@ -192,7 +197,7 @@ def inject_opencode_provider_config(port: int) -> None:
data = {}
# Strip any prior Headroom-managed blocks before re-injecting.
if _PROVIDER_MARKER_START in content:
if _PROVIDER_MARKER_START in content or _MCP_MARKER_START in content:
content = strip_opencode_headroom_blocks(content)
data = _parse_json_loose(content)
@ -206,16 +211,6 @@ def inject_opencode_provider_config(port: int) -> None:
}
data = _inject_key_into_json(data, "provider", provider)
# Inject MCP if not already present.
mcp = {
"headroom": {
"type": "remote",
"url": f"http://127.0.0.1:{port}/mcp",
"enabled": True,
}
}
data = _inject_key_into_json(data, "mcp", mcp)
# Write back as formatted JSON (opencode uses standard JSON with comments).
output = json.dumps(data, indent=2) + "\n"
config_file.write_text(output, encoding="utf-8")

View file

@ -6,6 +6,8 @@ import json
import os
from collections.abc import Mapping
from headroom.mcp_registry.install import DEFAULT_PROXY_URL
from .config import HEADROOM_OPENCODE_PLUGIN
@ -37,12 +39,16 @@ def build_opencode_config_content(
}
}
if include_mcp:
proxy_url = f"http://127.0.0.1:{port}"
mcp_entry: dict[str, object] = {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
}
if proxy_url != DEFAULT_PROXY_URL:
mcp_entry["environment"] = {"HEADROOM_PROXY_URL": proxy_url}
config["mcp"] = {
"headroom": {
"type": "remote",
"url": f"http://127.0.0.1:{port}/mcp",
"enabled": True,
}
"headroom": mcp_entry,
}
if include_plugin:
config["plugin"] = [[HEADROOM_OPENCODE_PLUGIN, {"proxyUrl": base_url}]]

View file

@ -135,6 +135,57 @@ def test_install_apply_rejects_provider_scope_targets_without_support() -> None:
assert "Provider scope supports only claude, codex, openclaw, and opencode" in result.output
def test_install_apply_accepts_opencode_target(monkeypatch) -> None:
runner = CliRunner()
captured: dict[str, object] = {}
class Manifest:
profile = "default"
preset = "persistent-service"
runtime_kind = "python"
supervisor_kind = "service"
scope = "provider"
health_url = "http://127.0.0.1:8787/readyz"
targets = ["opencode"]
mutations = []
artifacts = []
manifest = Manifest()
def fake_build_manifest(**kwargs):
captured.update(kwargs)
return manifest
monkeypatch.setattr("headroom.cli.install.build_manifest", fake_build_manifest)
monkeypatch.setattr("headroom.cli.install.load_manifest", lambda profile: None)
monkeypatch.setattr("headroom.cli.install.apply_mutations", lambda deployment: [])
monkeypatch.setattr("headroom.cli.install.install_supervisor", lambda deployment: [])
monkeypatch.setattr("headroom.cli.install.save_manifest", lambda deployment: None)
monkeypatch.setattr("headroom.cli.install.start_supervisor", lambda deployment: None)
monkeypatch.setattr("headroom.cli.install.start_detached_agent", lambda profile: None)
monkeypatch.setattr(
"headroom.cli.install.wait_ready", lambda deployment, timeout_seconds=45: True
)
result = runner.invoke(
main,
[
"install",
"apply",
"--scope",
"provider",
"--providers",
"manual",
"--target",
"opencode",
],
)
assert result.exit_code == 0, result.output
assert captured["targets"] == ["opencode"]
assert "Targets: opencode" in result.output
def test_install_apply_restores_previous_deployment_after_failed_update(monkeypatch) -> None:
runner = CliRunner()
calls: list[str] = []

View file

@ -160,6 +160,9 @@ def test_wrap_opencode_no_mcp_skips_mcp_injection(
env = captured["env"]
config = json.loads(env["OPENCODE_CONFIG_CONTENT"])
assert "mcp" not in config
config_file = tmp_path / ".config" / "opencode" / "opencode.json"
persisted_config = json.loads(config_file.read_text())
assert "headroom" not in persisted_config.get("mcp", {})
def test_wrap_opencode_injects_mcp_by_default(
@ -186,7 +189,12 @@ def test_wrap_opencode_injects_mcp_by_default(
env = captured["env"]
config = json.loads(env["OPENCODE_CONFIG_CONTENT"])
assert "mcp" in config
assert config["mcp"]["headroom"]["type"] == "remote"
assert config["mcp"]["headroom"] == {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
"environment": {"HEADROOM_PROXY_URL": "http://127.0.0.1:9000"},
}
def test_wrap_opencode_injects_rtk_into_agents_md(

View file

@ -63,8 +63,8 @@ def test_get_server_returns_spec_when_present(tmp_path: Path) -> None:
config = {
"mcp": {
"headroom": {
"type": "remote",
"url": "http://127.0.0.1:8787/mcp",
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
}
}
@ -74,6 +74,8 @@ def test_get_server_returns_spec_when_present(tmp_path: Path) -> None:
spec = registrar.get_server("headroom")
assert spec is not None
assert spec.name == "headroom"
assert spec.command == "headroom"
assert spec.args == ("mcp", "serve")
def test_register_server_creates_config_when_missing(tmp_path: Path) -> None:
@ -86,6 +88,12 @@ def test_register_server_creates_config_when_missing(tmp_path: Path) -> None:
assert result.status == RegisterStatus.REGISTERED
config_path = tmp_path / "opencode.json"
assert config_path.exists()
data = json.loads(config_path.read_text())
assert data["mcp"]["headroom"] == {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
}
def test_register_server_idempotent(tmp_path: Path) -> None:
@ -259,7 +267,7 @@ def test_register_server_with_env_vars(tmp_path: Path) -> None:
registrar.register_server(spec)
data = json.loads((tmp_path / "opencode.json").read_text())
assert data["mcp"]["headroom"]["env"] == {"HEADROOM_PROXY_URL": "http://127.0.0.1:9090"}
assert data["mcp"]["headroom"]["environment"] == {"HEADROOM_PROXY_URL": "http://127.0.0.1:9090"}
def test_register_then_re_register_with_different_env_returns_mismatch(tmp_path: Path) -> None:
@ -306,6 +314,30 @@ def test_entry_to_spec_command_as_string() -> None:
assert spec.args == ()
def test_entry_to_spec_reads_opencode_environment() -> None:
"""_entry_to_spec reads OpenCode's environment map."""
entry = {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
"environment": {"HEADROOM_PROXY_URL": "http://127.0.0.1:9090"},
}
spec = _entry_to_spec("headroom", entry)
assert spec.env == {"HEADROOM_PROXY_URL": "http://127.0.0.1:9090"}
def test_entry_to_spec_reads_legacy_env() -> None:
"""_entry_to_spec keeps compatibility with previously written env maps."""
entry = {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
"env": {"HEADROOM_PROXY_URL": "http://127.0.0.1:9090"},
}
spec = _entry_to_spec("headroom", entry)
assert spec.env == {"HEADROOM_PROXY_URL": "http://127.0.0.1:9090"}
def test_entry_to_spec_no_command() -> None:
"""_entry_to_spec handles an entry without a 'command' key."""
entry: dict[str, Any] = {
@ -329,9 +361,9 @@ def test_spec_to_entry_roundtrip() -> None:
env={"KEY": "VAL"},
)
entry = _spec_to_entry(original)
assert entry["type"] == "remote"
assert entry["type"] == "local"
assert entry["command"] == ["python", "-m", "server"]
assert entry["env"] == {"KEY": "VAL"}
assert entry["environment"] == {"KEY": "VAL"}
restored = _entry_to_spec("test", entry)
assert restored.name == original.name

View file

@ -164,6 +164,7 @@ def test_inject_provider_config_creates_file(
assert config_file.exists()
config = _parse_json_loose(config_file.read_text())
assert config["provider"]["headroom"]["npm"] == "@ai-sdk/openai-compatible"
assert "mcp" not in config
assert "model" not in config # headroom provider is a transparent pass-through
@ -293,10 +294,10 @@ def test_strip_blocks_handles_only_mcp_markers() -> None:
# ---------------------------------------------------------------------------
def test_inject_provider_config_merges_with_existing_mcp(
def test_inject_provider_config_preserves_existing_mcp(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""inject_opencode_provider_config merges headroom MCP with existing MCP servers."""
"""inject_opencode_provider_config preserves MCP without adding headroom."""
_set_test_home(monkeypatch, tmp_path)
config_file = tmp_path / ".config" / "opencode" / "opencode.json"
config_file.parent.mkdir(parents=True, exist_ok=True)
@ -308,7 +309,7 @@ def test_inject_provider_config_merges_with_existing_mcp(
config = json.loads(config_file.read_text())
assert "existing-server" in config["mcp"]
assert "headroom" in config["mcp"]
assert "headroom" not in config["mcp"]
def test_inject_provider_config_idempotent_with_complex_config(
@ -335,7 +336,7 @@ def test_inject_provider_config_idempotent_with_complex_config(
assert "openai" in config["provider"]
assert "headroom" in config["provider"]
assert "myserver" in config["mcp"]
assert "headroom" in config["mcp"]
assert "headroom" not in config["mcp"]
def test_inject_provider_config_preserves_unrelated_top_level_keys(
@ -412,6 +413,18 @@ def test_build_opencode_config_content_without_mcp() -> None:
assert config["plugin"] == [["headroom-opencode", {"proxyUrl": "http://127.0.0.1:8787/v1"}]]
def test_build_opencode_config_content_with_mcp_uses_local_stdio() -> None:
from headroom.providers.opencode.runtime import build_opencode_config_content
config = build_opencode_config_content(port=9000, include_mcp=True)
assert config["mcp"]["headroom"] == {
"type": "local",
"command": ["headroom", "mcp", "serve"],
"enabled": True,
"environment": {"HEADROOM_PROXY_URL": "http://127.0.0.1:9000"},
}
def test_build_launch_env_with_project(monkeypatch: pytest.MonkeyPatch) -> None:
from headroom.providers.opencode.runtime import build_launch_env

View file

@ -60,6 +60,7 @@ def test_apply_provider_scope_creates_config(
config = json.loads(config_file.read_text())
assert config["provider"]["headroom"]["options"]["baseURL"] == "http://127.0.0.1:8787/v1"
assert "mcp" not in config
def test_apply_provider_scope_skips_when_scope_is_not_provider(