Repository navigation
fix(cli): distinguish an unreadable MCP enablement config from a missing one - #29445
lets-order-some-fries wants to merge 2 commits into
Conversation
|
📊 PR Size: size/L
|
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
55863d0 to
d1822ff
Compare
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses a critical issue where a corrupt MCP server enablement configuration file would cause the system to fail open, effectively enabling all servers. By introducing explicit handling for unreadable configuration states, the system now defaults to a safer, closed state and prevents destructive write operations that would otherwise overwrite existing data when the configuration is malformed. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request enhances the robustness of the MCP server enablement configuration by introducing strict validation and graceful error handling for malformed or unreadable configuration files. It defines a new McpServerEnablementConfigError to prevent overwriting corrupted configurations, implements a fail-closed mechanism when the configuration cannot be parsed, and adds try-catch blocks in the CLI commands to cleanly report errors to the user. Additionally, comprehensive unit tests have been added to verify these error-handling behaviors. I have no feedback to provide as the changes are well-implemented and thoroughly tested.
|
@googlebot I signed it! |
…ing one
readConfig() returned {} for a SyntaxError exactly as it did for ENOENT.
Downstream isFileEnabled() reads `state?.enabled ?? true`, so an empty
config means everything is enabled - a trust boundary failing in the
permissive direction. Every server the user disabled gets connected and
its tools exposed. disable() then round-trips that same {} back to disk
with one key added, erasing the rest of the file.
readConfig() now returns a result discriminated on ok/missing/unreadable.
Only missing yields an empty config. isFileEnabled() fails closed on
unreadable; enable() and disable() refuse to write and throw
McpServerEnablementConfigError, preserving the file for the user to repair.
Valid JSON of the wrong shape fails open by the same route - {"x": "off"}
parses fine and leaves state.enabled undefined - so it is treated as
unreadable too, while entries with unknown extra fields are still accepted.
The error is reported once per stretch of failures rather than on every
read, and names the file path and the remedy.
Fixes google-gemini#28786
d1822ff to
6e7f587
Compare
…t-once arms The new suite keyed its in-memory fs on a POSIX literal while the manager builds the same path with path.join, so on the test_windows shard the key never matched: readFile threw the mock's ENOENT, readConfig returned `missing` instead of `unreadable`, and most of the new cases failed. That shard is a hard dependency of the final `ci` gate, so the PR could not have merged. Derive the constant the same way the manager does. Also covers two arms the suite left unobserved: a read that fails with something other than ENOENT (every other case supplies readable bytes and fails later at JSON.parse or the shape check), and the report-once-per- stretch behaviour, which no test observed at all - deleting the guard or either reset left the suite green. Documents the behaviour in docs/tools/mcp-server.md, which is where mcp-server-enablement.json is described. Worded around the slash commands: the `gemini mcp enable|disable` path is separately broken until google-gemini#29444.
|
For whoever reviews this: the sibling file has the identical defect, and I have opened #29481 for it.
Reproduced on #29481 uses the same shape as this PR deliberately, so the two read together. Happy to fold it into this one if you would rather review a single change. |
|
Hi there! Thank you for your interest in contributing to Gemini CLI. To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'. This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding. |
|
This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding. |
Summary
A corrupt
mcp-server-enablement.jsoncurrently fails open: every MCP server theuser deliberately disabled is reported as enabled, connected, and its tools are
exposed to the model. The next
disable()then writes a fresh one-entry fileover the corrupt one, erasing every other entry.
Both follow from
readConfig()collapsing "the file does not exist" and "thefile exists but cannot be read" into the same
{}.Details
readConfig()returned{}for aSyntaxErrorexactly as it did forENOENT.Downstream,
isFileEnabled()readsstate?.enabled ?? true, so an empty configmeans everything is enabled — a trust boundary that fails in the permissive
direction.
disable()then round-trips that same{}back to disk with one keyadded, and the unreadable contents are gone.
This PR keeps the two cases apart:
readConfig()returns aReadConfigResultdiscriminated onok/missing/unreadable. Onlymissingyields an empty config.isFileEnabled()fails closed onunreadable. A file that cannot be readmay disable this server, and we cannot tell which.
enable()anddisable()refuse to write onunreadable, throwingMcpServerEnablementConfigError, so the file on disk is preserved for theuser to repair.
{"playwright": "off"}parses fine, leavesstate.enabledundefined, and fails open by thesame
?? true. Entries carrying unknown extra fields are still accepted soolder clients tolerate configs written by newer ones.
isFileEnabled()runs for every server on every connection attempt — and nownames the file path and the remedy.
Call sites that write:
gemini mcp enable/disableand/mcp enable|disablenowprint the refusal instead of rejecting.
autoEnableServers()(extension enable)stops and returns what it managed, since nothing can be re-enabled while the
file is unreadable.
Deliberately out of scope:
canLoadServer()still reportsblockType: 'enablement'with "Run 'gemini mcp enable '" when a server is blockedthis way. Distinguishing "disabled" from "config unreadable" there means
widening the
EnablementCallbacksboolean contract thatpackages/coreconsumes. The emitted error explains the real cause in the meantime.
writeConfigis left non-atomic, exactly as onmain— it is byte-identicalbetween the two, and this PR only changes its two call sites. A truncate-then-
write can still be observed mid-tear by a second
geminiprocess, which nowreads as corruption. Note the direction though: on
mainthat same tear madedisable()read{}, add one key and permanently erase every other entry;here it refuses to write. A tmp-file +
renamewrite (the pattern already usedin
trust.ts,projectRegistry.tsandintegrity.ts) is a separate change andbelongs in its own PR.
Related Issues
Fixes #28786
How to Validate
Unit tests (14 existing + 15 added, all green). 10 of the added cases fail on
mainand are direct regression tests for the two defects:End to end, against a scratch home so your real config is untouched:
Setup, then the part that reproduces on this branch alone:
Observed on this branch (macOS):
On
main, the same command reportsplaywrightas enabled and connects it.The write-refusal half needs #29444 applied on top. On current
main,gemini mcp enable|disableexits early with "Server not found" for every server —a separate bug, fixed in #29444 — so the write path here is unreachable from the CLI
until that lands. With both applied:
with the file byte-identical (same md5) after both refused writes. Every path here is
covered by unit tests either way. The two branches touch adjacent lines of
enableDisable.ts; whichever lands second needs a one-line import rebase.Pre-Merge Checklist
docs/tools/mcp-server.md, the one placemcp-server-enablement.jsonis documentedconfig now disables MCP servers rather than silently enabling them, and
enable/disable refuse to write until it is repaired. That is the point of
the fix; a missing config behaves exactly as before.
The change is filesystem-and-JSON only, with no platform-specific paths beyond
Storage.getGlobalGeminiDir(), which it does not touch.🤖 Generated with Claude Code