fix(adapters): route Gemini config through project_root and user_scope#1306
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes Gemini MCP config path resolution to respect project_root and user_scope, eliminating accidental reads/writes via the process CWD and enabling --global/user-scope installs for Gemini in line with other adapters.
Changes:
- Add scope-aware Gemini directory resolution (
~/.geminifor user scope,<project_root>/.geminifor project scope) and route all config operations through it. - Update unit + integration tests to inject
project_rootdirectly (removingcwd/__new__workarounds) and add regressions for CWD-pollution and user-scope behavior. - Add a changelog entry describing the fix and its impact.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/apm_cli/adapters/client/gemini.py |
Implements scope-aware .gemini directory resolution and removes remaining os.getcwd()-based routing in config reads/writes. |
tests/unit/test_gemini_mcp.py |
Refactors tests to pass project_root, adds regressions for correct routing + user-scope behavior. |
tests/integration/test_gemini_integration.py |
Removes chdir/__new__ shims; constructs GeminiClientAdapter(project_root=...) directly. |
CHANGELOG.md |
Documents the bugfix under Unreleased/Fixed. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 2 | Clean, targeted fix: _get_gemini_dir() centralises scope routing correctly; one nit on redundant double-call in update_config; no blocking issues. |
| CLI Logging Expert | 0 | 2 | 2 | Silent project-scope skip and silent user-scope dir creation are the two output gaps; no blocking regressions introduced by this PR. |
| DevX UX Expert | 0 | 1 | 1 | Correct scope fix; silent skip of workspace-only runtimes at -g needs user-visible feedback. |
| Supply Chain Security Expert | 0 | 2 | 1 | No blocking issues; two recommended fixes: add path-containment guard on project_root, and use atomic write for settings.json. |
| OSS Growth Hacker | 0 | 2 | 1 | Parity fix unlocks Gemini CLI global installs; CHANGELOG is dense but docs table is clear; no blocking growth issues. |
| Doc Writer | 0 | 2 | 2 | Docs are accurate and complete; two prose improvements recommended: table cell readability and unconditional-create caveat placement. |
| Test Coverage Expert | 1 | 1 | 1 | Unit regression tests are well-formed; blocking gap: mcp_integrator.py still uses Path.cwd() for Gemini and never passes project_root, no integration test covers this end-to-end path. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Test Coverage Expert] (blocking-severity) Fix
mcp_integrator.pylines 1128-1129 and 666 to useproject_root_pathinstead ofPath.cwd()for the Gemini block, and addTestGeminiMCPIntegration::test_install_via_mcp_integrator_uses_project_root_not_cwd. -- The orchestrator independently verified the caller is still broken. The adapter fix is inert until this site is patched. Missing integration test on a governed-by-policy surface is a regression trap. - [Supply Chain Security Expert] Make the
settings.jsonwrite atomic (write to tmp,os.replace()) and addensure_path_within(resolved_gemini_dir, self.project_root)in_get_gemini_dir()for the project-scope branch. -- A crash mid-write silently zeros the MCP config;get_current_config()resets to{}onJSONDecodeErrorso all servers are lost on the next write. Path-containment guards against traversal via a maliciousapm.ymlproject_root value. - [Python Architect] Emit a debug-level warning in
MCPClientAdapter.project_rootwhen_project_rootis None anduser_scopeis False. -- The base-class cwd fallback is what makes the mcp_integrator omission silent; a warning would have surfaced the bug earlier and prevents the same class of omission in future adapters. - [CLI Logging Expert] Add
logger.debugfor silent project-scope skip; emit_rich_infowhen~/.gemini/is created for the first time in user scope. -- A silent no-op onapm install -g --mcpis indistinguishable from a failure; first-run directory creation should be visible to the user. - [OSS Growth Hacker] Rewrite the CHANGELOG bullet to lead with the user-facing win and add a 'what this means for you' callout to the docs table for Gemini-first users. -- The current CHANGELOG entry buries the user win inside implementation detail.
Architecture
classDiagram
direction LR
class MCPClientAdapter {
<<Abstract>>
+user_scope bool
+project_root Path
+get_config_path()* str
+update_config(config_updates)* bool
+get_current_config() dict
}
class CopilotClientAdapter {
<<ConcreteBase>>
+supports_user_scope bool
+target_name str
+configure_mcp_server(...) bool
+_format_server_config(...) dict
}
class GeminiClientAdapter {
<<Concrete>>
+target_name str
+mcp_servers_key str
+_get_gemini_dir() Path
+get_config_path() str
+update_config(config_updates)
+get_current_config() dict
+configure_mcp_server(...) bool
+_format_server_config(...) dict
}
class Path {
<<stdlib>>
+home() Path
}
MCPClientAdapter <|-- CopilotClientAdapter : extends
CopilotClientAdapter <|-- GeminiClientAdapter : extends
GeminiClientAdapter ..> Path : uses
MCPClientAdapter ..> Path : project_root
class GeminiClientAdapter:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A([apm install / --global]) --> B{user_scope?}
B -- yes --> C["_get_gemini_dir\nPath.home / .gemini"]
B -- no --> D["_get_gemini_dir\nproject_root / .gemini"]
D --> E{.gemini dir exists?}
E -- no --> F([return / silent skip])
E -- yes --> G["get_config_path\ngemini_dir / settings.json"]
C --> H["gemini_dir.mkdir\nparents=True exist_ok=True"]
H --> G
G --> I["get_current_config\nPath config_path .exists"]
I -- missing --> J[current_config = {}]
I -- present --> K[json.load settings.json]
J --> L[merge mcpServers entries]
K --> L
L --> M[FS: json.dump to settings.json]
M --> N([return success])
style C fill:#fff3b0,stroke:#d47600
style D fill:#fff3b0,stroke:#d47600
style G fill:#fff3b0,stroke:#d47600
style H fill:#fff3b0,stroke:#d47600
Recommendation
The adapter fix is correct and the unit tests are solid, but the PR as submitted cannot resolve issue #1299 end-to-end: the primary CLI call site (mcp_integrator.py lines 1128-1129, 666) still uses Path.cwd() for Gemini, so every live apm install invocation re-introduces the original bug. The maintainer should patch mcp_integrator.py and add the integration test (TestGeminiMCPIntegration::test_install_via_mcp_integrator_uses_project_root_not_cwd) in this PR before merge. The atomic-write and path-containment fixes from supply-chain-security can land as a fast follow in a separate PR if the maintainer prefers to keep scope tight.
Full per-persona findings
Python Architect
- [recommended] project_root fallback to os.getcwd() in MCPClientAdapter base is still present -- latent risk at construction sites that omit project_root= at
src/apm_cli/adapters/client/base.py
The fix correctly stops gemini.py from calling os.getcwd() directly. However, MCPClientAdapter.project_root still falls back to Path(os.getcwd()) when _project_root is None. Any adapter that omits project_root= at construction still gets cwd-relative paths.
Suggested: Consider a follow-up that emits a debug-level warning when _project_root is None and project_root is accessed from a non-user-scope adapter. - [nit] _get_gemini_dir() is called twice in update_config: once directly, once via get_config_path() at
src/apm_cli/adapters/client/gemini.py
Method is pure so no correctness impact, but redundant.
Suggested: Use gemini_dir directly:config_path = gemini_dir / 'settings.json'in update_config(). - [nit] get_config_path() returns str but callers immediately wrap it in Path() at
src/apm_cli/adapters/client/gemini.py
Both update_config() and get_current_config() do Path(self.get_config_path()), revealing a leaky abstraction. Pre-existing shape.
Suggested: Change get_config_path() to return Path directly in GeminiClientAdapter.
CLI Logging Expert
- [recommended] Silent return when project-scope .gemini/ is absent gives no feedback at
src/apm_cli/adapters/client/gemini.py
update_config() and configure_mcp_server() return silently when project scope is active but .gemini/ does not exist.
Suggested: Addlogger.debug("Skipping Gemini CLI: .gemini/ not found in %s", gemini_dir.parent)before the return. - [recommended] Silent ~/.gemini/ directory creation in user scope -- user sees no feedback at
src/apm_cli/adapters/client/gemini.py
When user_scope=True and ~/.gemini/ does not yet exist, update_config() calls mkdir() with no output.
Suggested: Capturewas_created = not gemini_dir.is_dir()before mkdir and emit _rich_info conditionally. - [nit] configure_mcp_server() returns True (not False) when .gemini/ is absent -- asymmetric with other early-exits at
src/apm_cli/adapters/client/gemini.py - [nit] get_current_config() swallows OSError and JSONDecodeError silently with no log at
src/apm_cli/adapters/client/gemini.py
Suggested: Addlogger.warning("Could not read %s: %s", config_path, e)in the except block.
DevX UX Expert
- [recommended] Silent skip of Cursor/OpenCode at -g gives user no feedback; looks like a bug
When a user runsapm install -g --mcp NAME, workspace-only runtimes are silently skipped. A no-op with no output is indistinguishable from a failure or a mis-typed flag.
Suggested: Emit a _rich_info line for each runtime skipped at user scope, naming the runtime and why. - [nit] Auto-creating ~/.gemini/ silently is fine but a one-line notice would help first-run users at
src/apm_cli/adapters/client/gemini.py
Suggested: After mkdir, emit_rich_info(f'Created {gemini_dir}')when the dir did not previously exist.
Supply Chain Security Expert
- [recommended] project_root is not validated with ensure_path_within before constructing the write path at
src/apm_cli/adapters/client/gemini.py
A value like '../../home/victim' could pass the is_dir() guard and write settings.json outside the intended tree. The codebase already has ensure_path_within and validate_path_segments for exactly this.
Suggested: In _get_gemini_dir() (project scope branch), call path_security.ensure_path_within(resolved_gemini_dir, self.project_root) after resolving symlinks. - [recommended] json.dump write to settings.json is not atomic; corrupt file on crash silently wipes MCP config at
src/apm_cli/adapters/client/gemini.py
open(config_path, 'w') truncates immediately; a crash mid-json.dump leaves a zero-byte or partial JSON file. get_current_config() resets to {} on JSONDecodeError, overwriting all previously configured servers.
Suggested: Write to a temp file in the same directory, then os.replace() atomically. - [nit] mkdir(parents=True) is redundant in project-scope path after is_dir() guard at
src/apm_cli/adapters/client/gemini.py
Suggested: Drop parents=True for project scope.
OSS Growth Hacker
- [recommended] CHANGELOG entry is thorough but written for maintainers, not users -- shrink it for release notes at
CHANGELOG.md
Suggested: Lead with 'apm install -g --mcp now routes to ~/.gemini/settings.json for Gemini CLI users (user-scope was previously silently ignored).' - [recommended] Updated docs table and prose are accurate but missing a 'what this means for you' sentence for Gemini-first users at
docs/src/content/docs/consumer/install-mcp-servers.md
Suggested: Add: 'Gemini CLI now supportsapm install -g --mcp-- writes to~/.gemini/settings.json, no project setup required.' - [nit] No release beat or social angle attached to this fix
Suggested: Flag for the next release post: 'Gemini CLI global MCP installs fixed -- apm install -g --mcp now works correctly for all three user-scope runtimes.'
Doc Writer
- [recommended] Table cell for Gemini is overloaded; consider splitting path and trigger condition for readability at
docs/src/content/docs/consumer/install-mcp-servers.md
Suggested: Match the Claude Code row style: '.gemini/settings.json(project, opt-in) or~/.gemini/settings.json(-g)' - [recommended] "creates ~/.gemini/ if needed" caveat buried mid-paragraph; surface as standalone sentence at
docs/src/content/docs/consumer/install-mcp-servers.md
This is the only runtime where APM will unconditionally create a user-home directory.
Suggested: End the opt-in paragraph with: 'Gemini user scope (-g) is unconditional: APM creates~/.gemini/if it does not exist.' - [nit] "routes the write to" is slightly indirect; prefer active verb at
docs/src/content/docs/consumer/install-mcp-servers.md - [nit] CHANGELOG entry is accurate but very long; the parenthetical history lesson is changelog noise at
CHANGELOG.md
Test Coverage Expert
- [blocking] mcp_integrator.py gemini block still uses Path.cwd() and does not pass project_root to ClientFactory.create_client; no test covers this cross-module path at
src/apm_cli/integration/mcp_integrator.py
The PR fixes GeminiClientAdapter to honour project_root, but the primary real-world caller at line 1128 still reads(Path.cwd() / '.gemini').is_dir()and callsClientFactory.create_client(runtime_name)WITHOUTproject_root=project_root_path. Every liveapm installinvocation still exhibits the original [BUG] GeminiClientAdapter ignores project_root/user_scope and writes MCP config under cwd #1299 bug. The integration test suite bypasses this caller entirely.
Proof (missing at integration-with-fixtures):tests/integration/test_gemini_integration.py::TestGeminiMCPIntegration::test_install_via_mcp_integrator_uses_project_root_not_cwd-- proves: When apm install runs in a directory different from the project root, MCP config is written to project_root/.gemini/settings.json, not to cwd/.gemini/settings.json - [recommended] test_falls_back_to_cwd_when_project_root_not_passed codifies the live mcp_integrator bug path as a named backward-compat contract at
tests/unit/test_gemini_mcp.py
mcp_integrator.py relies on exactly this fallback (it never passes project_root for gemini). The test should note the fallback exists for callers that have not yet migrated.
Proof (passed at unit):tests/unit/test_gemini_mcp.py::TestGeminiProjectRootRouting::test_falls_back_to_cwd_when_project_root_not_passed - [nit] test_user_scope_configure_mcp_server patches copilot module, not gemini module; correct but fragile if import chain changes at
tests/unit/test_gemini_mcp.py
Proof (passed at unit):tests/unit/test_gemini_mcp.py::TestGeminiUserScope::test_user_scope_configure_mcp_server_does_not_short_circuit
Auth Expert -- inactive
No auth-relevant files were changed; the PR only routes file-system paths (project_root vs os.getcwd()) in gemini.py and its tests/docs.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Note
🔒 Integrity filter blocked 2 items
The following items were blocked because they don't meet the GitHub integrity level.
- #1306
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". - fix(adapters): route Gemini config through project_root and user_scope #1306
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by PR Review Panel for issue #1306 · ● 3.6M · ◷
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 2 | Sound bug-fix: _get_gemini_dir() aligns Gemini with the MCPClientAdapter project_root contract; one implicit correctness fix and one nit on return-type consistency. |
| CLI Logging Expert | 0 | 1 | 2 | Silent ~/.gemini/ creation on first user-scope write is the main UX gap; opt-in skip in configure_mcp_server returns True with no trace, masking skips in verbose mode. |
| DevX UX Expert | 0 | 2 | 1 | Stale 'supported: copilot, codex' warning text misleads Gemini users at -g; silent True return on skipped project-scope write inflates success counts. |
| Supply Chain Security Expert | 0 | 1 | 2 | No blocking supply-chain issues; project_root path containment gap is pre-existing across all adapters, not a regression introduced here. |
| OSS Growth Hacker | 0 | 2 | 1 | Unblocking apm install -g --mcp for Gemini CLI is a genuine adoption unlock; docs are correct but miss a copyable one-liner that would make it shareable. |
| Doc Writer | 0 | 1 | 2 | Doc changes are accurate and voice-consistent; VS Code omitted from workspace-only list and CHANGELOG entry is one unparseable run-on sentence. |
| Test Coverage Expert | 0 | 1 | 2 | Install path has unit + integration regression traps; remove_stale Gemini cleanup block has no test that enters the deletion branch with stale entries. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [DevX UX Expert] (blocking-severity) Fix the stale fallback warning to include
geminiin the supported-runtimes list inmcp_integrator.py-- A user running the exact scenario this PR enables can still seesupported: copilot, codexand receive a false failure signal. One-line fix, zero risk, should be in this PR. - [Test Coverage Expert] Add integration regression trap
test_remove_stale_gemini_uses_project_root_not_cwdthat exercises the deletion branch withstale_namesnon-empty -- Evidence outcome ismissingon the direct target of this bug fix; without this trap the line 666project_root_pathfix can silently regress toPath.cwd()and no CI check catches it. - [DevX UX Expert] Change
configure_mcp_serverto return aSKIPPEDsentinel (notTrue) when the project opt-in gate fires -- Current behavior inflates the configured server count and gives users a false success signal; this is a correctness issue in the user-visible output contract. - [Doc Writer] Add VS Code to the workspace-only runtimes list in
install-mcp-servers.mdand split the CHANGELOG entry into problem / fix / reference lines -- VS Code is the most widely used harness in the table; omitting it from the scope-skip note misleads the majority of users reading that section. - [OSS Growth Hacker] Add a copyable Gemini global install example to
install-mcp-servers.mdand prepend a two-sentence user-facing summary to the CHANGELOG entry -- The doc page is already a strong SEO surface; the missing example reduces time-to-first-success for Gemini CLI users landing there.
Architecture
classDiagram
direction LR
class MCPClientAdapter {
<<Abstract>>
+project_root Path
+user_scope bool
+supports_user_scope bool
+get_config_path()* str
+update_config(config_updates)* bool
+get_current_config()* dict
}
class CopilotClientAdapter {
<<ConcreteBase>>
+supports_user_scope bool
+configure_mcp_server(...) bool
}
class GeminiClientAdapter {
<<ConcreteStrategy>>
+supports_user_scope bool
+target_name str
+_get_gemini_dir() Path
+get_config_path() str
+update_config(config_updates) bool
+get_current_config() dict
+configure_mcp_server(...) bool
}
class MCPIntegrator {
<<Orchestrator>>
+install_mcp_servers(project_root, ...) void
+uninstall_mcp_servers(project_root, ...) void
}
MCPClientAdapter <|-- CopilotClientAdapter : extends
CopilotClientAdapter <|-- GeminiClientAdapter : extends
MCPIntegrator ..> GeminiClientAdapter : instantiates
class GeminiClientAdapter:::touched
class MCPIntegrator:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["CLI: apm install SERVER --runtime gemini"] --> B["MCPIntegrator.install_mcp_servers()"]
B --> C["GeminiClientAdapter(project_root=project_root_path)"]
C --> D["configure_mcp_server()"]
D --> E{"user_scope?"}
E -- yes --> F["_get_gemini_dir() -> Path.home() / .gemini"]
E -- no --> G["_get_gemini_dir() -> project_root / .gemini"]
G --> H{"gemini dir exists?"}
H -- no --> I["return True [opt-in skip]"]
H -- yes --> J["update_config(server_config)"]
F --> J
J --> K["get_current_config() reads settings.json"]
K --> L["config_path.parent.mkdir(parents=True)"]
L --> M["json.dump -> settings.json"]
B --> N["remove_stale: stale-server cleanup"]
N --> O["project_root_path / .gemini / settings.json [was Path.cwd()]"]
O --> P["read/write settings.json"]
Recommendation
Ship after the stale warning text is corrected to include gemini -- that one-liner is the only change that is both trivially safe and directly undermines the adoption unlock headline if left broken. The remaining followups (remove_stale regression trap, SKIPPED sentinel return, doc gaps, Gemini hero command) are all real and should be filed as issues against the next patch, but none of them block the core fix from reaching users. The unit test evidence is positive, the adapter contract is now consistent, and Gemini CLI global MCP install works for the first time.
Full per-persona findings
Python Architect
-
[recommended] Implicit correctness fix: old
get_current_config()returnedTrue(not{}) on missing dir; this PR silently resolves it attests/unit/test_gemini_mcp.py
The removed branchif not gemini_dir.is_dir(): return Truewas a latent bug -- callers treating the return as a config dict would get a bool. The rewrite to checkconfig_path.exists()first and return{}is correct. Not called out in the PR description or tests, so a future reader may not know the old behavior was wrong.
Suggested: Add a test: with no.geminidir and nosettings.json, assertget_current_config()returns{}(notTrue). Name ittest_get_current_config_returns_empty_dict_when_no_dir. -
[nit]
get_config_path()returnsstrbut every caller immediately re-wraps withPath()atsrc/apm_cli/adapters/client/gemini.py:69
ReturningPathdirectly removes the double-wrap and makes the contract consistent.
Suggested:def get_config_path(self) -> Path: return self._get_gemini_dir() / 'settings.json' -
[nit]
configure_mcp_server()calls_get_gemini_dir()twice (guard + viaupdate_config) atsrc/apm_cli/adapters/client/gemini.py:247
Both calls are cheap -- not a correctness issue, but a shared local variable would make the intent explicit.
CLI Logging Expert
-
[recommended]
update_configcreates~/.gemini/silently on first user-scope write -- no user-visible message at any verbosity level atsrc/apm_cli/adapters/client/gemini.py
config_path.parent.mkdir(parents=True, exist_ok=True)runs unconditionally whenuser_scope=Trueand~/.gemini/does not yet exist. In--verbosemode there is equally nothing.
Suggested: Beforemkdir, add:if not gemini_dir.is_dir(): logger.debug('Creating %s for Gemini CLI user configuration', gemini_dir) -
[nit]
update_configsilent return on project opt-in gate has no debug trace -- verbose mode cannot distinguish skip from write atsrc/apm_cli/adapters/client/gemini.py
Suggested:logger.debug('Skipping Gemini project-scope write -- %s does not exist (opt-in)', gemini_dir)before thereturn. -
[nit]
configure_mcp_serverreturnsTruesilently when project opt-in gate fires -- callers receive a success signal for a no-op atsrc/apm_cli/adapters/client/gemini.py
Suggested:logger.debug('Gemini opt-in gate: %s absent, skipping configure_mcp_server', self._get_gemini_dir())beforereturn True.
DevX UX Expert
-
[recommended] Stale fallback warning names only
copilot, codexas user-scope runtimes, excludinggeminiatsrc/apm_cli/integration/mcp_integrator.py
A user runningapm install -g --mcp NAMEwho has only Gemini CLI detected will seeNo runtimes support user-scope MCP installation (supported: copilot, codex)and return 0 -- the exact scenario this PR was meant to fix.
Suggested:logger.warning('No runtimes support user-scope MCP installation (supported: copilot, codex, gemini)') -
[recommended]
configure_mcp_serverreturnsTrue(success) when project-scope.gemini/dir is absent -- a silent no-op that inflates the configured count atsrc/apm_cli/adapters/client/gemini.py
The caller incrementsconfigured_countonTrue, soapm install --mcp NAMEreports N servers configured even when Gemini wrote nothing.
Suggested: ReturnNoneor aSKIPPEDsentinel instead ofTruewhen the opt-in gate fires. -
[nit]
get_current_configsilently returns{}onJSONDecodeError, so a corruptsettings.jsonis overwritten without warning atsrc/apm_cli/adapters/client/gemini.py
Suggested: Log a warning via_rich_warningwhenJSONDecodeErroris caught.
Supply Chain Security Expert
-
[recommended]
project_rootis used to construct write paths withoutensure_path_withinvalidation atsrc/apm_cli/adapters/client/gemini.py
self.project_root / '.gemini'is constructed with no containment check. Pre-existing gap shared by all adapters, but the new user-scope unconditional write path widens the impact.
Suggested: Callensure_path_within(gemini_dir, self.project_root)for the project-scope branch. File a companion issue to retrofit across all adapters. -
[nit]
get_config_pathreturnsstr, thenupdate_configimmediately re-wraps inPath-- obscures where path construction happens, making audits harder atsrc/apm_cli/adapters/client/gemini.py -
[nit]
config_path.parent.mkdiris reachable for project-scope despite theis_dir()guard earlier -- effectively dead code in that branch, could confuse future readers who relax the guard atsrc/apm_cli/adapters/client/gemini.py
Suggested: Wrap inif self.user_scope:to make the intent explicit.
OSS Growth Hacker
-
[recommended] Docs update correct but lacks a copyable hero command for the new Gemini global flow
Gemini CLI users landing on install-mcp-servers.md get no quick-copy anchor for the exact command that now works.
Suggested: Add a short callout:apm install -g --mcp io.github.github/github-mcp-serverimmediately after the Gemini row. -
[recommended] CHANGELOG entry is correct but written for maintainers, not users -- 200+ words of implementation detail, no user-facing lead
Suggested: Prepend: 'Gemini CLI users can now runapm install -g --mcp NAMEto register MCP servers globally -- this did not work before.' -
[nit] The opt-in note for Gemini user scope buries the lede -- lead with the unconditional create behavior, then explain the project-scope gate.
Auth Expert -- inactive
No auth-critical files changed (auth.py, token_manager.py, github_downloader.py, etc. all untouched); PR strictly reroutes Gemini config file paths from os.getcwd() to project_root with no effect on credential resolution or token handling.
Doc Writer
-
[recommended] VS Code omitted from workspace-only runtimes list, misleading for the most common harness at
docs/src/content/docs/consumer/install-mcp-servers.md
The prose saysWorkspace-only runtimes (e.g. Cursor, OpenCode) are skipped at user scopebut omits VS Code, the most widely used harness in the table.
Suggested:Workspace-only runtimes (VS Code, Cursor, OpenCode) are skipped at user scope. -
[nit] CHANGELOG entry is one unparseable 200-word sentence at
CHANGELOG.md
Suggested: Split into: (1) one-line problem statement, (2) one-line fix statement, (3) reference line for test and docs. -
[nit] Gemini table cell is significantly longer than peer rows, reducing scanability at
docs/src/content/docs/consumer/install-mcp-servers.md
Suggested: Shorten to.gemini/settings.json (project) or ~/.gemini/settings.json (-g)and rely on the paragraph below for the opt-in gate explanation.
Test Coverage Expert
-
[recommended]
remove_staleGemini cleanup path has no test that exercises actual entry removal whenproject_rootdiffers fromcwdatsrc/apm_cli/integration/mcp_integrator.py
All tests passstale_names=set(), so the Gemini deletion branch is never entered. If line 666 silently regressed toPath.cwd()the test suite would not catch it.
Proof (missing at integration-with-fixtures):tests/integration/test_gemini_integration.py::test_remove_stale_gemini_uses_project_root_not_cwd-- proves: apm uninstall removes the Gemini MCP entry fromproject_root/.gemini/settings.json, notcwd/.gemini/settings.json, when cwd differs from project_root
assert 'stale-server' not in json.loads((project_root / '.gemini' / 'settings.json').read_text()).get('mcpServers', {}) -
[nit] Integration regression test outcome cannot be certified by run (pytest absent in review env); outcome downgraded to
unknownper S7 PROBE RULE attests/integration/test_gemini_integration.py
test_install_via_mcp_integrator_uses_project_root_not_cwdexists and reads correctly as a valid regression trap. Code review confirms it exercises the exact bug site.
Proof (unknown at integration-with-fixtures):tests/integration/test_gemini_integration.py::test_install_via_mcp_integrator_uses_project_root_not_cwd-- proves:MCPIntegrator.installwrites toproject_root/.gemini/settings.jsonand notcwdwhen they differ
assert settings.exists() and 'regression-1299-srv' in data.get('mcpServers', {}) -
[nit] Unit tests for
TestGeminiProjectRootRoutingandTestGeminiUserScopeare well-formed and cover the adapter fix attests/unit/test_gemini_mcp.py
All three routing scenarios covered: project_root used when cwd lacks.gemini/, cwd not polluted when both dirs have.gemini/, fallback to cwd when no project_root passed. No gaps at the unit tier.
Proof (passed at unit):tests/unit/test_gemini_mcp.py::TestGeminiProjectRootRouting.test_does_not_pollute_cwd_when_cwd_also_has_gemini-- proves:GeminiClientAdapterwrites only toproject_root/.gemini/and never tocwd/.gemini/when both directories exist
self.assertTrue((self.project_root / '.gemini' / 'settings.json').exists()); self.assertFalse((self.cwd_root / '.gemini' / 'settings.json').exists())
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Note
🔒 Integrity filter blocked 2 items
The following items were blocked because they don't meet the GitHub integrity level.
- #1306
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". - fix(adapters): route Gemini config through project_root and user_scope #1306
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by PR Review Panel for issue #1306 · ● 3.1M · ◷
…and hedge -g enumeration (microsoft#1299)
- Add 'gemini' to user-scope warning in mcp_integrator_install.py
- Add logger.debug/warning calls for silent skips in gemini.py
- Add VS Code to runtime docs prose
- Add test: get_current_config returns {} when .gemini/ absent
- Add test: remove_stale uses project_root not cwd for Gemini
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d0754fc to
58f257b
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
GeminiClientAdapterresolved.gemini/settings.jsonfromos.getcwd()in every method (get_config_path,update_config,configure_mcp_server), so theproject_rootconstructor argument was silently ignored anduser_scope=Truehad no effect at all even thoughsupports_user_scope = Trueis declared. Callers that instantiatedGeminiClientAdapter(project_root=...)from outside the target project either had their MCP writes silently dropped (when the current cwd had no.gemini/) or, worse, written into an unrelated cwd that happened to have one.apm install --globalsimilarly had no effect on Gemini.This change brings the adapter in line with the pattern every other client adapter (Codex, Claude, Cursor, OpenCode, VS Code) already follows:
_get_gemini_dir()helper routes onself.user_scope:~/.gemini/in user scope,self.project_root / ".gemini"in project scope.get_config_path,update_config,configure_mcp_server, andget_current_configall read through the helper -- no remainingos.getcwd()calls in this adapter..gemini/is missing atproject_root). User scope is unconditional andmkdirs the directory, matching the contract used by Claude / Codex / Copilot in user scope.patch("os.getcwd", ...)setup and switched to injectingproject_root=directly; the integration tests dropped theirGeminiClientAdapter.__new__+monkeypatch.chdirworkaround for the same reason. Added regression coverage for cwd-pollution, cwd-fallback, the user-scope path, and thatconfigure_mcp_serverdoes not early-return in user scope when~/.gemini/is missing.Fixes #1299
Type of change
Testing
Local verification:
tests/unit/test_gemini_mcp.py-- 27 tests pass (20 existing kept green after the fixture rewrite, 7 new: 3 regression forproject_rootrouting, 4 for user scope including theconfigure_mcp_serverno-short-circuit case).tests/integration/test_gemini_integration.py-- 17 tests pass after replacing the__new__+monkeypatch.chdirshim withGeminiClientAdapter(project_root=...).tests/unit/compilation/test_gemini_formatter.py-- 8 tests pass.tests/unit, ~1148 tests) shows no new failures introduced by this change.#1299reproducer from the issue body now writes intoproject_root/.gemini/settings.jsoninstead of cwd. A second variant where cwd also contains a.gemini/directory confirms cwd is no longer polluted.apm install --target gemini --only mcpend-to-end (real CLI binary, self-defined stdio MCP server inapm.yml) configures<project>/.gemini/settings.jsoncleanly and leaves$HOME/.gemini/untouched -- no regression in the project-scope install path.