You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
APM supports a growing set of install targets (Copilot CLI, Codex, Claude Code, VS Code, Cursor, OpenCode, with more on the way), but MCP-server lifecycle support is not uniform across them and the integration layer doesn't scale.
Surfaced concretely while reviewing #655 (Claude Code MCP target). The adapter itself (in src/apm_cli/adapters/client/) was a clean ~200-line addition, but wiring it into mcp_integrator.py and friends required threading per-runtime branches through several modules. Adding a hypothetical 7th MCP target today touches ~10 files and ~100–270 lines of glue.
Where the coupling lives
Adapter layer (good — O(1) per new target)
src/apm_cli/adapters/client/{copilot,vscode,cursor,opencode,codex,claude}.py — each adapter owns its config path, format translation, and read/write
src/apm_cli/adapters/client/factory.py — single dict lookup
Adding a target here is one new file plus one factory line
Integration layer (bad — O(n) coupling per target)
src/apm_cli/integration/mcp_integrator.py::remove_stale() — already 5 hand-written if/elif blocks (~136 lines on main, lines 477–612), one per runtime, each duplicating: open file → parse runtime-specific format → find stale names → delete → write back → log. Bypasses the adapter abstraction entirely (does not call adapter.get_current_config() / adapter.update_config()).
src/apm_cli/registry/operations.py::_get_installed_server_ids() — similar per-runtime if/elif chain re-implementing file I/O with hardcoded format knowledge.
src/apm_cli/core/conflict_detector.py::get_existing_server_configs() — per-runtime branches (elif "copilot" in adapter_class_name, etc.) for the same reason.
Multiple per-runtime constants and detection lists in mcp_integrator.py (WORKSPACE_SCOPED_MCP_RUNTIMES, _detect_runtimes, fallback lists) that need updating per target.
The root cause is the same in all four places: integration code re-implements format knowledge instead of delegating to the adapter.
What "not widely supported" means in practice
Some apm install targets have full MCP install + uninstall + stale-cleanup support (Copilot CLI, VS Code).
Others have partial support that silently no-ops when the host config dir is absent (Cursor, OpenCode, now Claude Code project-scope).
Existing pattern in src/apm_cli/integration/mcp_integrator.py:477-612 (5 hand-written runtime blocks).
.github/instructions/integrators.instructions.md — APM already articulates the "one base, many file types" + "pay only for what you touch" principles for the file-level integrators. The same principles should apply to MCP integration.
Problem
APM supports a growing set of install targets (Copilot CLI, Codex, Claude Code, VS Code, Cursor, OpenCode, with more on the way), but MCP-server lifecycle support is not uniform across them and the integration layer doesn't scale.
Surfaced concretely while reviewing #655 (Claude Code MCP target). The adapter itself (in
src/apm_cli/adapters/client/) was a clean ~200-line addition, but wiring it intomcp_integrator.pyand friends required threading per-runtime branches through several modules. Adding a hypothetical 7th MCP target today touches ~10 files and ~100–270 lines of glue.Where the coupling lives
Adapter layer (good — O(1) per new target)
src/apm_cli/adapters/client/{copilot,vscode,cursor,opencode,codex,claude}.py— each adapter owns its config path, format translation, and read/writesrc/apm_cli/adapters/client/factory.py— single dict lookupIntegration layer (bad — O(n) coupling per target)
src/apm_cli/integration/mcp_integrator.py::remove_stale()— already 5 hand-written if/elif blocks (~136 lines onmain, lines 477–612), one per runtime, each duplicating: open file → parse runtime-specific format → find stale names → delete → write back → log. Bypasses the adapter abstraction entirely (does not calladapter.get_current_config()/adapter.update_config()).src/apm_cli/registry/operations.py::_get_installed_server_ids()— similar per-runtime if/elif chain re-implementing file I/O with hardcoded format knowledge.src/apm_cli/core/conflict_detector.py::get_existing_server_configs()— per-runtime branches (elif "copilot" in adapter_class_name, etc.) for the same reason.mcp_integrator.py(WORKSPACE_SCOPED_MCP_RUNTIMES,_detect_runtimes, fallback lists) that need updating per target.The root cause is the same in all four places: integration code re-implements format knowledge instead of delegating to the adapter.
What "not widely supported" means in practice
apm installtargets have full MCP install + uninstall + stale-cleanup support (Copilot CLI, VS Code).mcp_install_scopeas adapter-attached state — no other adapter uses runtime-mutable state).Proposed direction
Lift the duck-typed file I/O out of
mcp_integrator.pyand into the adapter contract. Concretely:MCPClientAdapter(base.py) with two methods:remove_servers(self, names: Set[str]) -> List[str]— adapter reads its own config, removes named servers, writes back, returns removed names.list_server_ids(self) -> Set[str]— replaces the per-runtime branches in_get_installed_server_ids.mcp_integrator.remove_stale()to a polymorphic loop:_get_installed_server_idsandconflict_detectorsimilarly — they calladapter.list_server_ids()instead of branching.mcp_install_scopemonkey-patch becomes a constructor parameter or aClientFactory.create_client(scope=...)kwarg. Most adapters can ignore it.After this refactor, adding MCP target #7 should be: 1 adapter file + 1 factory line. That's it.
Out of scope for this issue
CopilotClientAdapter) — works, low priority to refactor.References
src/apm_cli/integration/mcp_integrator.py:477-612(5 hand-written runtime blocks)..github/instructions/integrators.instructions.md— APM already articulates the "one base, many file types" + "pay only for what you touch" principles for the file-level integrators. The same principles should apply to MCP integration.