Skip to content

feat: MCP install target for Claude Code (project + user scope)#655

Open
dmartinol wants to merge 4 commits intomicrosoft:mainfrom
dmartinol:feature/claude-code-mcp
Open

feat: MCP install target for Claude Code (project + user scope)#655
dmartinol wants to merge 4 commits intomicrosoft:mainfrom
dmartinol:feature/claude-code-mcp

Conversation

@dmartinol
Copy link
Copy Markdown

Closes #643

Summary

Adds Claude Code as a first-class MCP client for apm install / lockfile-driven MCP lifecycle: registry-backed server entries are merged into Claude’s documented config locations, with project vs user scope aligned to InstallScope (apm install vs apm install --global).

Behavior

  • Project: .mcp.json at the repo root (mcpServers), only when .claude/ already exists (opt-in, same idea as Cursor + .cursor/).
  • User: ~/.claude.json top-level mcpServers when scope is user / global install path.
  • Reuses Copilot-style registry → config formatting from CopilotClientAdapter, then normalizes entries for Claude Code (e.g. drop Copilot-only stdio fields such as type: local, default tools, empty id; keep remotes / HTTP shape per docs).
  • Install: MCP installation is no longer blanket-disabled for --global when MCP deps exist, so user-scoped Claude (and other home-dir runtimes) can be configured from the manifest.
  • Integrator: MCPIntegrator passes install_scope (and related wiring) into install/uninstall paths; stale cleanup removes Claude entries from project .mcp.json and user ~/.claude.json where applicable.
  • Conflict detection: MCPConflictDetector treats Claude like other mcpServers-shaped clients.
  • Factory: ClientFactory registers runtime claude / Claude.
  • Docs: IDE integration doc updated to include Claude.
  • Tests: tests/unit/test_claude_mcp.py (adapter, merge/normalize, remove_stale); runtime script detection for claude in test_runtime_detection.py.

Notes

  • Repo-local adapter paths follow the same cwd convention as existing repo-local clients (e.g. Cursor): run apm from the project root for project MCP files.
  • safe_installer accepts optional workspace_root / install_scope for API compatibility with the integrator; mcp_install_scope is set on the adapter for Claude user vs project behavior.
  • audit_report.py: extract pipe-escaping for Markdown tables into a variable to satisfy Python 3.11+ f-string rules (no behavior change).

Minor style-only diffs in Python may appear from Black/isort on edited files; repo-wide formatting and CI alignment are out of scope here — see #645.

Testing

  • uv run pytest tests/unit/test_claude_mcp.py tests/unit/test_runtime_detection.py -x
  • (CI) full unit suite as in CONTRIBUTING.md

Signed-off-by: Daniele Martinoli <dmartino@redhat.com>
Copilot AI review requested due to automatic review settings April 9, 2026 20:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Claude Code as a supported MCP client for apm install, including project vs user scope configuration, and wires scope through install/uninstall + conflict detection.

Changes:

  • Introduces ClaudeClientAdapter and registers it in ClientFactory; adds runtime detection for claude.
  • Threads install_scope/workspace_root through MCP install/check/cleanup paths and adds Claude-specific stale cleanup.
  • Updates docs and adds unit tests for Claude MCP config merge/normalize and stale cleanup.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/test_runtime_detection.py Adds unit test to detect claude in scripts runtime detection.
tests/unit/test_claude_mcp.py New unit tests for Claude adapter config pathing, merge/normalize behavior, and stale cleanup.
src/apm_cli/security/audit_report.py Refactors markdown escaping to avoid f-string backslash expression issues.
src/apm_cli/registry/operations.py Threads install_scope/workspace_root into installed-server checks; adds claude/opencode parsing branches.
src/apm_cli/integration/mcp_integrator.py Adds Claude runtime detection, scope threading, and Claude project/user stale cleanup logic.
src/apm_cli/factory.py Registers claude runtime to ClaudeClientAdapter.
src/apm_cli/core/safe_installer.py Passes install_scope through to adapters for scope-sensitive MCP config.
src/apm_cli/core/operations.py Extends install_package API to accept workspace_root/install_scope.
src/apm_cli/core/conflict_detector.py Treats Claude configs as mcpServers schema for conflict detection.
src/apm_cli/commands/uninstall/engine.py Passes workspace_root into MCP stale cleanup after uninstall.
src/apm_cli/commands/uninstall/cli.py Wires deploy root into uninstall MCP cleanup call.
src/apm_cli/commands/install.py Enables MCP install for --global and passes scope/root through to MCP integrator.
src/apm_cli/adapters/client/claude.py New Claude adapter implementing project .mcp.json + user ~/.claude.json behavior and normalization.
docs/src/content/docs/integrations/ide-tool-integration.md Documents Claude MCP config locations and runtime targeting.

Comment on lines 1010 to 1014
if runtime_name == "vscode":
if _is_vscode_available():
ClientFactory.create_client(runtime_name)
installed_runtimes.append(runtime_name)
elif runtime_name == "cursor":
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

When running with user/global scope, this runtime detection can still include "vscode" (via _is_vscode_available()). That can cause apm install -g to create/update a workspace-local .vscode/mcp.json in the current working directory (VSCodeClientAdapter auto-creates .vscode/ even on read), which breaks the intended scope separation. Consider filtering out workspace-scoped runtimes (vscode/cursor/opencode/claude-project) when install_scope is InstallScope.USER, unless the user explicitly targets them, or ensure repo-local adapters are rooted at workspace_root rather than CWD.

Copilot uses AI. Check for mistakes.
@dmartinol
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

dmartinol and others added 3 commits April 9, 2026 22:57
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Daniele Martinoli <dmartino@redhat.com>
Signed-off-by: Daniele Martinoli <dmartino@redhat.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.

Comment on lines 1021 to 1025
if runtime_name == "vscode":
if _is_vscode_available():
ClientFactory.create_client(runtime_name)
installed_runtimes.append(runtime_name)
elif runtime_name == "cursor":
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

When running with install_scope=InstallScope.USER (e.g. apm install -g), this runtime detection still includes workspace-scoped clients like VS Code. Since VSCodeClientAdapter uses os.getcwd() and auto-creates .vscode/ in get_config_path(), a global install can end up creating/modifying .vscode/mcp.json in whatever directory the user ran the command from. Consider filtering target runtimes by scope (USER: only home-scoped runtimes like copilot/codex/claude) or implementing workspace_root support so repo-local adapters read/write under that root instead of CWD.

Copilot uses AI. Check for mistakes.
Comment on lines 863 to 867
stored_mcp_configs=old_mcp_configs,
diagnostics=apm_diagnostics,
workspace_root=get_deploy_root(scope),
install_scope=scope,
)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

workspace_root is documented as the root for repo-local MCP configs, but get_deploy_root(InstallScope.USER) is Path.home(). Passing the home directory here makes runtime detection/checks look for .cursor/ / .opencode/ / .claude/ under $HOME, and does not prevent repo-local adapters from writing under the process CWD. It would be safer to pass Path.cwd() for workspace_root (repo-local), and rely on install_scope for Claude user-vs-project behavior, or otherwise ensure only user-scoped runtimes run in USER installs.

Copilot uses AI. Check for mistakes.
Comment on lines +469 to 473
wr = workspace_root if workspace_root is not None else Path.cwd()

# Determine which runtimes to clean, mirroring install-time logic.
all_runtimes = {"vscode", "copilot", "codex", "cursor", "opencode"}
all_runtimes = {"vscode", "copilot", "codex", "cursor", "opencode", "claude"}
if runtime:
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

In USER scope, workspace_root will typically be Path.home(), but remove_stale() still considers workspace-scoped runtimes (vscode/cursor/opencode) in all_runtimes. This can cause stale cleanup during apm install -g / user-scope uninstall to touch unexpected paths (and some adapters create directories on read). Consider deriving all_runtimes from install_scope (USER: home-scoped runtimes only) or requiring an explicit --runtime for workspace-scoped cleanup.

Copilot uses AI. Check for mistakes.
else:
clean_claude_user = clean_claude_project = True

if clean_claude_project:
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Claude project behavior is opt-in on install (only write .mcp.json when .claude/ exists), but stale cleanup removes entries from .mcp.json whenever it exists, even if .claude/ is absent. To keep behavior consistent and avoid modifying .mcp.json in repos that are not opted-in, gate project cleanup on (wr / '.claude').is_dir() (or track whether APM previously managed the file).

Suggested change
if clean_claude_project:
claude_project_enabled = (wr / ".claude").is_dir()
if clean_claude_project and claude_project_enabled:

Copilot uses AI. Check for mistakes.
Comment on lines +625 to +627
_rich_info(
f"+ Removed stale MCP server '{name}' from .mcp.json"
)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

These new CLI messages hardcode a leading "+" instead of using the repo's status-symbol convention (STATUS_SYMBOLS in src/apm_cli/utils/console.py). Consider emitting "Removed stale MCP server ..." and passing symbol="check" (or prefixing with [+]) so output is consistent across commands.

Copilot uses AI. Check for mistakes.
use the process working directory (same as Cursor/VS Code paths).
install_scope: ``InstallScope`` for user vs project MCP paths (Claude).
"""
_ = workspace_root
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

workspace_root is now threaded through the install APIs, but it is explicitly ignored here (_ = workspace_root). This makes it easy for callers to think repo-local adapters will honor the provided root when they actually use os.getcwd(), which is especially risky for apm install -g (may write to the wrong directory). Either implement workspace_root handling (e.g., run repo-local adapter operations under that directory) or remove the parameter to avoid a misleading API.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +110
_ = workspace_root

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

workspace_root is accepted but currently unused (_ = workspace_root), so check_servers_needing_installation() still reads runtime configs relative to the process CWD (and some adapters create directories as a side effect of reads). If this parameter is intended for user-scope/global installs, it should be applied (e.g., temporarily chdir to workspace_root for repo-local runtimes) or removed to prevent incorrect assumptions by callers.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] MCP install target for Claude Code (project + user scope)

2 participants