Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `apm install --target claude` now preserves self-defined stdio MCP `env` values from `apm.yml` and writes non-string values such as `PORT: 3000` and `DEBUG: false` as MCP-compatible strings. (#1222)
- Pin `Path.home()` under unit tests via a session-scoped autouse conftest fixture, fixing 56 Windows runner failures on the new `windows-2025-vs2026` GitHub-hosted image where `USERPROFILE`/`HOMEDRIVE`+`HOMEPATH` are not seeded for pytest workers; also patch the `_check_and_notify_updates` import binding in the disabled-self-update test so it no longer races on the version-check cache. (#1270)
- `apm install` now works on macOS git 2.53.0 (Homebrew): bare-cache commands switch to `--git-dir` to satisfy the `safe.bareRepository=explicit` default; fetched SHAs are pinned as synthetic refs so `git clone --local --shared` no longer silently omits them. (#1268)
- Set the unit-test hermetic HOME at conftest import time so a single xdist worker on the `windows-2025-vs2026` runner can no longer race fixture setup and re-trigger the 53 `Path.home()` failures the session-scoped autouse fixture was supposed to prevent. (#1271)
Expand Down
24 changes: 23 additions & 1 deletion src/apm_cli/adapters/client/copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ def _has_env_placeholder(value):
return bool(_COPILOT_ENV_RE.search(value))


def _stringify_env_literal(value):
"""Return MCP env literal values in the manifest ``map<string, string>`` shape."""
if isinstance(value, bool):
return str(value).lower()
return str(value)


class CopilotClientAdapter(MCPClientAdapter):
"""Copilot CLI implementation of MCP client adapter.

Expand Down Expand Up @@ -745,8 +752,10 @@ def _resolve_environment_variables(self, env_vars, env_overrides=None):
for name, raw_value in env_vars.items():
if not name:
continue
if raw_value is None:
continue
if not isinstance(raw_value, str):
translated[name] = raw_value
translated[name] = _stringify_env_literal(raw_value)
continue
if _has_env_placeholder(raw_value):
self._last_legacy_angle_vars.update(_extract_legacy_angle_vars(raw_value))
Expand Down Expand Up @@ -788,6 +797,19 @@ def _resolve_environment_variables(self, env_vars, env_overrides=None):
self._last_env_placeholder_keys = set(placeholder_keys)
return resolved

if isinstance(env_vars, dict):
resolved = {}
for name, value in env_vars.items():
if not name:
continue
if isinstance(value, str):
resolved[name] = self._resolve_env_variable(
name, value, env_overrides=env_overrides
)
elif value is not None:
resolved[name] = _stringify_env_literal(value)
return resolved
Comment on lines +800 to +811
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed


import os
import sys

Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_claude_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,36 @@ def test_update_config_tolerates_malformed_project_mcp_json(self):
data = json.loads(self.mcp_path.read_text(encoding="utf-8"))
self.assertIn("srv", data["mcpServers"])

def test_configure_self_defined_stdio_preserves_env(self):
with patch.object(self.adapter, "registry_client") as mock_registry:
mock_registry.find_server_by_reference.return_value = {
"name": "env-demo",
"_raw_stdio": {
"command": "npx",
"args": ["-y", "example-mcp"],
"env": {
"DEMO_ENV": "demo-value",
"PORT": 3000,
"DEBUG": False,
"RATE": 0.5,
},
},
}

ok = self.adapter.configure_mcp_server("env-demo")

self.assertTrue(ok)
data = json.loads(self.mcp_path.read_text(encoding="utf-8"))
self.assertEqual(
data["mcpServers"]["env-demo"]["env"],
{
"DEMO_ENV": "demo-value",
"PORT": "3000",
"DEBUG": "false",
"RATE": "0.5",
},
)


class TestClaudeClientAdapterUser(unittest.TestCase):
"""User scope: ``~/.claude.json`` top-level ``mcpServers``."""
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/test_copilot_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,18 @@ def test_dict_shaped_env_block_does_not_silently_drop(self):
)
self.assertEqual(set(result.keys()), {"FOO", "BAR"})

def test_dict_shaped_env_block_stringifies_literals_and_omits_none(self):
adapter = CopilotClientAdapter()
result = adapter._resolve_environment_variables(
{"PORT": 3000, "DEBUG": False, "RATE": 0.5, "OPTIONAL": None},
env_overrides=None,
)

self.assertEqual(
result,
{"PORT": "3000", "DEBUG": "false", "RATE": "0.5"},
)


class TestCopilotInstallRunSummary(unittest.TestCase):
"""Issue #1152: aggregated post-install diagnostics.
Expand Down