Manage MCPs panel hangs on “Loading MCP servers…” — silent .catch() in workbench bundle swallows polling errors
Category: Bug — Antigravity IDE (UI / MCP)
Affects: Antigravity 1.107.0 (commit 62335c71d47037adf0a8de54e250bb8ea6016b15, 2026-04-02), Windows 11
Severity: High — makes the Manage MCPs surface permanently unusable; the failure is invisible (no user-facing error, no log line)
Reproducible: Yes, 100% once triggered; persists across relaunches because the broken editor pane is auto-restored
Symptom
Clicking Manage MCPs (top-right title-bar button) opens a webview tab titled “Manage MCP servers” showing:
Loading MCP servers… ⟳ Refreshing…
The spinner never completes. The list never populates. There is no error banner, no toast, and nothing written to Antigravity.log, exthost.log, or renderer.log beyond the normal startup traces. The chat panel itself works fine and the configured MCP servers function normally — only the management UI is affected.
Because Antigravity’s editor-pane serializer persists this panel to state.vscdb whenever it’s open at shutdown, it is auto-restored on every launch after the first hang — so the spinner appears immediately on every subsequent startup with no user action required.
Reproduction
- Have at least one enabled MCP server in
~/.gemini/antigravity/mcp_config.json(verified to reproduce with 10 enabled, and also with zero enabled — see “Isolation” below). - Click the Manage MCPs button in the title bar.
- Observe the “Loading MCP servers…” spinner.
- Wait indefinitely.
- Close Antigravity. Relaunch. The broken panel is auto-restored immediately.
Isolation — what this is NOT
We bisected the config down to zero enabled servers (set "disabled": true on all 24 entries) and the panel still hangs identically. So:
Not caused by any individual MCP server hanging handshake
Not caused by remote-proxied servers (context7,developer-knowledgeviamcp-remote)
Not caused by the local Qdrant backend being down
Not caused by OAuth state corruption (verified: loadCodeAssist+fetchAvailableModelssucceed in every session log)
Not caused by the uss-artifactReviewuncaught exceptions inmain.log(those are present in working environments too; confirmed noise)
Not caused by extensions (reproduces with a clean profile and no MCP-related extensions)
Root cause
Reading the workbench bundle (resources/app/out/vs/workbench/workbench.desktop.main.js), the MCP state service — minified class FYn — drives the panel via this polling loop:
// class FYn constructor:
constructor(...) {
this.a = []; // server states
this.b = false; // mcpServersInitialized flag — the gate
this.c = true; // isLoading flag
this.refreshMcpServers(); // fire-and-forget on construction
this.D(this.w.onDidChangeLsClient(h => h && !this.b && this.refreshMcpServers()));
}
async refreshMcpServers(shallow = false, serverName) {
if (this.y.getOAuthTokenInfo() === null) return; // (A) silent early exit
await this.w.isInitialized();
const s = this.w.lsClient;
if (!s) return; // (B) silent early exit
this.c = true;
s.refreshMcpServers({ shallow, serverName });
return new Promise(resolve => {
const r = setInterval(() => {
s.getMcpServerStates({}).then(a => {
if (a.isLoading) return; // keep polling
clearInterval(r);
this.c = false;
this.updateMcpServers(a.states); // ONLY path that sets this.b = true
resolve();
}).catch(() => {}); // (C) swallows ALL errors — no log, no retry backoff, poll continues forever
}, 300);
});
}
The render function (class Oui, method vb) has this gate:
vb(e) {
if (!this.mb.mcpServersInitialized) return; // (D) gate never opens if above paths fail
this.kb.style.display = "none"; // would hide "Loading MCP servers..." — never reached
// ...render list
}
Four failure modes, all producing the identical symptom
-
Early exit A —
getOAuthTokenInfo()returns null at construction. The fallbackonDidChangeLsClientlistener only retries if!this.b, butthis.bnever becomestruein any failure path, so the listener fires uselessly (or not at all, if the LS client was already steady-state before the service constructed). -
Early exit B —
lsClientis null when the service initialises. Same fallback issue. -
Poll returns
isLoading: trueforever — gRPC round-trips succeed but the LS side never flips its own loading flag. The promise never resolves. -
Poll rejects silently —
.catch(() => {})on thegetMcpServerStates()call swallows everything. No error surfaces, the interval continues at 300 ms forever firing rejected requests that go nowhere. No log line is ever produced.
The rendering code has no branch for “couldn’t initialize” — the loading label is owned exclusively by the success path. So the user sees the initial spinner and literally nothing else ever happens.
Why this persists across restarts
Antigravity’s editor-pane serializer for this input is:
var NLd = class {
canSerialize(t) { return true; }
serialize(t) { return ""; }
deserialize(t,e){ return t.createInstance(Pui); }
};
// ...
getEditorFactory().registerEditorSerializer(Pui.ID, NLd);
Pui.ID === "workbench.input.antigravityConfigurePluginsPageInput". Because canSerialize always returns true and the editor is persisted to each workspace’s state.vscdb → memento/workbench.parts.editor, any workspace that had the panel open during a failed attempt will silently re-open it on every subsequent launch. Users see “spinning immediately on every startup, forever” with no obvious cause.
Suggested fixes (any one of these is sufficient)
- Surface the error in
.catch. At minimumconsole.errorit, ideally propagate through a UI error banner..catch(() => {})of a polling RPC is an anti-pattern. - Let the render path handle “failed to initialise” as a terminal state. Set
this.b = true(withthis.a = []) after N consecutive poll failures sovb()can render the “No servers found.” empty-list branch the bundle already has, instead of the gate-closed spinner. - Retry with exponential backoff instead of a 300 ms fixed interval that silently burns CPU forever.
- Make
onDidChangeLsClientidempotent. If the LS client was already present whenFYnconstructed (common race), the fallback never fires. Either (a) checklsClientnon-null at construction and re-enterrefreshMcpServers, or (b) fireonDidChangeLsClientretroactively on subscribe. - Don’t auto-restore a known-failed editor pane. The serializer’s
canSerializecould returnfalsewhenmcpServersInitialized === falseat shutdown time, preventing the panel from auto-reopening in a broken state.
Workaround we applied (for the record)
With Antigravity closed, we surgically removed the persisted editor entry from every workspace’s state:
# For each C:\Users\<user>\AppData\Roaming\Antigravity\User\workspaceStorage\*\state.vscdb (and .backup):
# Load key "memento/workbench.parts.editor" as JSON
# Walk the grid, remove any editor with id == "workbench.input.antigravityConfigurePluginsPageInput"
# Fix up the "mru" array indices
# Write back
After this, the broken panel stops auto-reopening on startup. The chat panel continues to function normally; users can manage servers by editing mcp_config.json directly with any text editor. The underlying FYn bug is still present — clicking the Manage MCPs title-bar button still hangs — but at least the IDE is usable again.
Evidence files
Antigravity.logduring a hang session: only showsLanguage server started,loadCodeAssist,fetchAvailableModels. No MCP-related entries at all while the panel is spinning — confirming the silent-catch is swallowing RPC errors with no logging.renderer.log: no errors associated with the MCP panel timeframe.workspaceStorage/<hash>/state.vscdb → memento/workbench.parts.editorcontains the persistedantigravityConfigurePluginsPageInputentry across 6 distinct workspaces on our machine.