feat(codex): add project-scoped MCP and user target support#803
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates APM's Codex integration to support project-scoped MCP configuration (writing to .codex/config.toml for project installs) and adds Codex participation in the existing user-scope target/scope model.
Changes:
- Make Codex MCP config scope-aware (project
.codex/config.tomlvs user~/.codex/config.toml) and avoid configuring Codex MCP unless Codex is an active project target. - Add scope-aware
project_root/user_scopeplumbing through MCP client creation and MCP install/uninstall flows. - Expand unit/integration tests and update Starlight docs to reflect the new Codex MCP scoping behavior.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/integration/mcp_integrator.py |
Adds scope parameters and filters Codex MCP configuration to active project targets. |
src/apm_cli/adapters/client/base.py |
Introduces project_root/user_scope context and placeholder normalization helper. |
src/apm_cli/adapters/client/codex.py |
Resolves Codex config path by scope and normalizes project placeholders in args. |
src/apm_cli/factory.py |
Passes project_root/user_scope into client adapter construction. |
src/apm_cli/core/safe_installer.py |
Creates adapters with scope context for conflict-aware installs. |
src/apm_cli/core/operations.py |
Threads scope context through configure/install/uninstall helpers. |
src/apm_cli/registry/operations.py |
Checks installed server IDs using scope-aware adapter config resolution. |
src/apm_cli/integration/targets.py |
Marks Codex as partially user-scope supported. |
src/apm_cli/commands/install.py |
Passes scope context into MCP install + stale cleanup. |
src/apm_cli/commands/uninstall/cli.py / engine.py |
Passes scope context into MCP stale cleanup on uninstall. |
src/apm_cli/adapters/client/{vscode,cursor,opencode,copilot}.py |
Switches workspace-root resolution to self.project_root. |
tests/unit/test_transitive_mcp.py |
Adds Codex project-scoped MCP behavior tests (install + stale cleanup). |
tests/unit/test_mcp_client_factory.py |
Validates Codex config-path scoping + placeholder normalization. |
tests/unit/integration/test_scope_*.py |
Updates/extends target resolution and install/uninstall scope tests for Codex. |
tests/unit/integration/test_hook_integrator.py |
Verifies Codex hooks merge target respects scope-resolved root dir. |
docs/src/content/docs/integrations/ide-tool-integration.md |
Documents Codex MCP config paths and project-target gating. |
docs/src/content/docs/reference/cli-commands.md |
Clarifies global vs project Codex configuration behavior. |
Comments suppressed due to low confidence (4)
src/apm_cli/registry/operations.py:36
- The method signature now accepts project_root and user_scope, but the docstring's Args section doesn't document them. Please update the docstring to describe how scope/root affect which config files are inspected when checking installation status.
def check_servers_needing_installation(self, target_runtimes: List[str], server_references: List[str], project_root=None, user_scope: bool = False) -> List[str]:
"""Check which MCP servers actually need installation across target runtimes.
This method checks the actual MCP configuration files to see which servers
are already installed by comparing server IDs (UUIDs), not names.
Args:
target_runtimes: List of target runtimes to check
server_references: List of MCP server references (names or IDs)
src/apm_cli/core/operations.py:13
- The function signature now includes project_root and user_scope, but the docstring doesn't document these parameters. Please update the Args section so callers understand which config scope/path will be written.
def configure_client(client_type, config_updates, project_root=None, user_scope=False):
"""Configure an MCP client.
Args:
client_type (str): Type of client to configure.
config_updates (dict): Configuration updates to apply.
src/apm_cli/core/operations.py:40
- The function signature now includes project_root and user_scope, but these parameters are missing from the docstring Args section. Document them so it's clear which project/user config is being targeted during installation.
def install_package(client_type, package_name, version=None, shared_env_vars=None, server_info_cache=None, shared_runtime_vars=None, project_root=None, user_scope=False):
"""Install an MCP package for a specific client type.
Args:
client_type (str): Type of client to configure.
package_name (str): Name of the package to install.
version (str, optional): Version of the package to install.
shared_env_vars (dict, optional): Pre-collected environment variables to use.
server_info_cache (dict, optional): Pre-fetched server info to avoid duplicate registry calls.
shared_runtime_vars (dict, optional): Pre-collected runtime variables to use.
src/apm_cli/core/operations.py:86
- The function signature now includes project_root and user_scope, but the docstring Args section doesn't mention them. Please document how they affect which config file is edited during uninstall.
def uninstall_package(client_type, package_name, project_root=None, user_scope=False):
"""Uninstall an MCP package.
Args:
client_type (str): Type of client to configure.
package_name (str): Name of the package to uninstall.
1070d52 to
0d0328b
Compare
0d0328b to
8df3621
Compare
8df3621 to
b31b2af
Compare
b31b2af to
84b5115
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/apm_cli/registry/operations.py:116
- _get_installed_server_ids() currently extracts IDs only for runtimes 'copilot', 'codex', and 'vscode'. MCPIntegrator.install() can pass 'cursor' (and potentially other runtimes) into check_servers_needing_installation(), and Cursor uses the same
mcpServersschema as Copilot (includingid). Without a 'cursor' branch here, Cursor will always appear to have zero installed servers, causing unnecessary reinstalls. Consider handling 'cursor' by reading IDs fromconfig.get("mcpServers", {})like the Copilot branch (and similarly address any other MCP-compatible runtimes you intend to support).
)
config = client.get_current_config()
if isinstance(config, dict):
if runtime == 'copilot':
84b5115 to
4863999
Compare
4863999 to
729180a
Compare
828cb98 to
aaf4fcd
Compare
798a0d0 to
157c85c
Compare
157c85c to
ced7b7b
Compare
APM Review Panel VerdictDisposition: APPROVE (one required pre-merge fix -- CHANGELOG format; four optional follow-ups) Per-persona findingsPython Architect: This PR threads OO / class diagram classDiagram
direction LR
class MCPClientAdapter {
<<AbstractBase>>
+project_root Path
+user_scope bool
+supports_user_scope bool
+get_config_path()* str
+update_config(config_updates)*
+get_current_config()*
+configure_mcp_server(...)* bool
+normalize_project_arg(value) str
}
class CodexClientAdapter {
+_get_codex_dir() Path
+get_config_path() str
+update_config(config_updates) bool
+get_current_config() dict|None
+configure_mcp_server(...) bool
}
class CopilotClientAdapter {
+supports_user_scope bool
+get_config_path() str
+update_config(config_updates)
}
class VSCodeClientAdapter {
+get_config_path() str
}
class CursorClientAdapter {
+get_config_path() str
}
class OpenCodeClientAdapter {
+get_config_path() str
}
class ClientFactory {
<<Factory>>
+create_client(client_type, project_root, user_scope) MCPClientAdapter
}
class SafeMCPInstaller {
+runtime str
+adapter MCPClientAdapter
+install_package(...) dict
}
class MCPIntegrator {
<<Service>>
+install(..., project_root, user_scope) int
+remove_stale(..., project_root, user_scope)
+_install_mcp_for_runtime(..., project_root, user_scope) bool
+_check_self_defined_servers_needing_installation(..., project_root, user_scope) list
}
class MCPServerOperations {
+check_servers_needing_installation(..., project_root, user_scope) list
+_get_installed_server_ids(..., project_root, user_scope) set
}
MCPClientAdapter <|-- CodexClientAdapter
MCPClientAdapter <|-- CopilotClientAdapter
CopilotClientAdapter <|-- VSCodeClientAdapter
CopilotClientAdapter <|-- CursorClientAdapter
CopilotClientAdapter <|-- OpenCodeClientAdapter
ClientFactory ..> MCPClientAdapter : creates
SafeMCPInstaller o-- MCPClientAdapter : adapter
MCPIntegrator ..> ClientFactory : delegates
MCPIntegrator ..> SafeMCPInstaller : creates
MCPIntegrator ..> MCPServerOperations : delegates
class MCPClientAdapter:::touched
class CodexClientAdapter:::touched
class CopilotClientAdapter:::touched
class VSCodeClientAdapter:::touched
class CursorClientAdapter:::touched
class OpenCodeClientAdapter:::touched
class ClientFactory:::touched
class SafeMCPInstaller:::touched
class MCPIntegrator:::touched
class MCPServerOperations:::touched
classDef touched fill:#fff3b0,stroke:#d47600
Execution flow diagram flowchart TD
A["apm install\ncommands/install.py"] --> B["_install_apm_packages(ctx)"]
B --> C["MCPIntegrator.install\nmcp_deps, project_root=ctx.project_root\nuser_scope=scope==USER"]
C --> D{"scope override\nUSER -> user_scope=True\nPROJECT -> user_scope=False"}
D --> E{"runtime flag\nspecified?"}
E -->|"yes"| F["target_runtimes = [runtime]"]
E -->|"no"| G["[FS] auto-discover:\nproject_root/.vscode .cursor .codex .opencode .gemini"]
G --> H{"not user_scope AND\ncodex in target_runtimes?"}
H -->|"yes"| I["[FS] active_targets(project_root, config_target)\nintegration/targets.py"]
I -->|"codex not active"| J["[LOG] logger.progress: skipping Codex\nremove codex from target_runtimes"]
I -->|"codex active"| K["scope filter: USER -> keep supports_user_scope only"]
H -->|"no"| K
F --> K
J --> K
K --> L["MCPServerOperations.check_servers_needing_installation\ntarget_runtimes, servers, project_root, user_scope"]
L --> M["[I/O] ClientFactory.create_client(runtime, project_root, user_scope)\n.get_current_config()"]
M --> N["_install_mcp_for_runtime(runtime, deps, project_root, user_scope)"]
N --> O["SafeMCPInstaller(runtime, project_root, user_scope)\n-> ClientFactory.create_client(runtime, project_root, user_scope)"]
O --> P{"user_scope?"}
P -->|"True"| Q["[FS] ~/.codex/config.toml"]
P -->|"False"| R["[FS] project_root/.codex/config.toml"]
Q --> S["[I/O] get_current_config() -> dict or None"]
R --> S
S -->|"None (TOML parse error)"| T["[LOG] _rich_warning: could not parse -- skipping\nreturn False"]
S -->|"dict"| U["[FS] update_config: write config.toml\nreturn True"]
Design patterns
Structural concern: Minor inefficiency: In CLI Logging Expert: Output paths in new code are clean. DevX UX Expert: The project-local opt-in model ( Supply Chain Security Expert: This PR improves the security posture in two concrete ways:
Follow-up concern (not blocking): Auth Expert: Not activated -- the diff touches OSS Growth Hacker: Strong story: "APM now configures Codex MCP project-locally, the same way it handles Cursor -- CEO arbitrationAll specialists are aligned: the architecture is correct, the security posture improves, and the UX follows established Cursor/OpenCode precedent. No disagreements to arbitrate. This is a solid external contribution (Christian Hessel, Required actions before merge
Optional follow-ups
|
danielmeppiel
left a comment
There was a problem hiding this comment.
Required actions before merge (all required + optional)
CHANGELOG.md -- rewrite the ## [Unreleased] entry from conventional-commit format to user prose per the project's Keep-a-Changelog convention. The current entry opens with a backtick-wrapped feat(codex): prefix and runs to three clauses; replace with a single concise sentence ending with (#803). Example: - Codex CLI MCP config is now project-local (.codex/config.toml) during project installs and gated to active project targets, matching Cursor and OpenCode behavior; user-scope primitive deployment is also supported. (#803) -- or shorter.
ScopeContext dataclass: introduce a small ScopeContext(project_root: Path, user_scope: bool) value object to collapse the two-parallel-parameter threading visible in 10+ function signatures across mcp_integrator.py, operations.py, safe_installer.py, registry/operations.py. Eliminates the class of "forgot to pass user_scope" bugs at new call sites.
MCPClientAdapter.update_config() return type: add -> bool | None annotation to the ABC and a docstring note that False/None means the write was skipped; prevents future implementers from silently breaking callers that check the return value.
Path security guard: add ensure_path_within(resolved_config_path, self.project_root) in adapter get_config_path() implementations (or in the project_root property) as defense-in-depth, consistent with path_security.py convention.
Pre-existing print() in codex.py:configure_mcp_server (~line 174): convert to logger.verbose_detail() / CommandLogger call in a follow-up cleanup PR.
ced7b7b to
6e98646
Compare
Head branch was pushed to by a user without write access
ff0ec78 to
572adb5
Compare
|
@danielmeppiel fixed failing failing ci build and rebased with main |
danielmeppiel
left a comment
There was a problem hiding this comment.
@Nickolaus good one! no need to update against main next time, we have a merge queue (and updating manually makes reviews stale) thanks!
Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps pyproject.toml + uv.lock to 0.11.0. Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because this release ships one BREAKING removal (`apm marketplace build` -> exits 2, use `apm pack`) plus several net-new features (Dev Container Feature, Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack` unification, multi-org `apps[]`). Strict semver in 0.x: minor for features-with-break, patch only for bugfixes. Milestone admin (done out-of-band): - Renamed milestone #8 `0.10.1` -> `0.11.0` - Created milestone #9 `0.12.0` as next-up bucket - Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0` - 6 closed items stay in `0.11.0` PRs shipping in 0.11.0 (22 commits since v0.10.0): User-facing features: - #1042/#722 `apm pack` unifies bundle + marketplace.json (BREAKING: `apm marketplace build` removed) - #1038 `marketplace:` block in apm.yml + `apm marketplace migrate` - #803 /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives - #861 Dev Container Feature `ghcr.io/microsoft/apm/apm-cli` - #982/#984 shared/apm.md `apps:` array for cross-org private packages - #820 `target:` in apm.yml validates at parse time - #1032 `apm marketplace add` honors manifest.name (Claude Code parity) - #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms User-facing fixes: - #1015 ADO Entra ID auth + `apm install --update` pre-flight abort - #1019/#1020 GEMINI.md only created when target requested - #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms - #1018 POSIX paths in auto-discovery output (Windows compat) - #996 drop stray 'specify' from generated file footer Maintainer tooling: - #1043 NOTICE.md per CELA template - #1045/#1044 NOTICE drift gate + license-policy gate in CI - #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut) - #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1 - #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file - #1022 review-panel: true fan-out + binary verdict + label automation - #918 complexity audit + benchmarks suite - #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder) Files changed: - pyproject.toml: 0.10.0 -> 0.11.0 - uv.lock: regenerated (version field only) - CHANGELOG.md: [Unreleased] promoted to [0.11.0] - 2026-04-29 NOTICE drift check passes against the bumped lockfile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Make Codex MCP config project-scoped for repo installs by writing to
.codex/config.tomlinstead of~/.codex/config.toml, and only configure Codex MCP when Codex is an active project target.Also add Codex user-scope support for installed primitives so agents, hooks, and skills follow the existing target and scope model used by the other supported tools.
Fixes #502
Type of change
Testing