AutoGPT is the vision of accessible AI for everyone, to use and to build on. Our mission is to provide the tools, so that you can focus on what matters.
fix(platform/copilot): MCP setup card fires on stale creds, OAuth popup close race, registry-search prompt, Connected/Reconnect UI, integrations page polish (#13207)
## Why
Five MCP-related bugs / UX gaps all surfaced as **"the MCP sign-in is
broken for users"**. Reported by John (#breakage 2026-05-22) and
reproduced by Zamil (session `c79e0ad0-bbbc-475f-815f-cb8d73dc4c9d`,
2026-05-25). The headline pattern: autopilot promises a sign-in card
that never appears, or surfaces one whose "Connect" button fails with
**"Sign-in window was closed"**, or shows the user nothing at all when
they're already connected.
The five root causes:
1. **Stale-creds shadow the setup card.** `run_mcp_tool.py:192-195` only
fired `SetupRequirementsResponse` on 401/403 when `not creds`. If the
user already had an MCP credential row whose token was revoked/expired
*server-side* (without our local `access_token_expires_at` knowing),
`auto_lookup_mcp_credential` skipped refresh, fed the dead token to the
server, got 401, and we fell through to a generic HTTP error — no
sign-in card ever shown. Matches John's screenshot: *"the system can't
trigger the sign in until after I delete the expired credential"*.
**Verified fixed E2E** in the test comment below.
2. **OAuth popup close race.** `oauth-popup.ts:184-197` polled
`popup.closed` every 500 ms and rejected with
`OAUTH_ERROR_WINDOW_CLOSED` the *instant* the popup closed. The Sentry
MCP callback page writes BroadcastChannel + localStorage **then** calls
`window.close()`; across origins those events can land a few hundred ms
after close, so a successful OAuth handshake gets reported as "Sign-in
window was closed".
3. **Model never tried MCP for services not in the hardcoded list.**
Prompt told the model `find_block` is mandatory and listed MCP only as
"check if a hosted MCP server is available… only use known URLs". For
services like Sentry that aren't in `mcp_tool_guide.md`'s table, after
`find_block` returned no_results the model jumped straight to
`SendAuthenticatedWebRequestBlock` — never read the guide, never
searched the MCP registry.
4. **No "already connected" UX.** When the user IS already signed in to
an MCP server, the model would say *"a sign-in card has appeared"* but
no card rendered (discovery success was invisible). User reported:
*"there are instances that says we are already logged in and button
should appear and turns out because we already logged in nothing is
rendered making it misleading"*.
5. **Integrations page UI for MCP** — group label rendered as "Mcp"
(title-caser stumble on acronym), no logo, per-row title duplicated
`MCP: ...` prefix that the group label already showed.
## What
### Backend
- `run_mcp_tool.py`:
- On 401/403, **always** fire the setup card (regardless of whether
creds exist), and call the new `invalidate_mcp_credential` helper to
delete the stale row so the next attempt doesn't loop on the same dead
token.
- New `surface_connect_card: bool = False` parameter — when true, skip
the MCPClient call entirely and return a `SetupRequirementsResponse`
whose `user_readiness.has_all_credentials` reflects current stored
state. Lets the model explicitly surface a "you're already connected"
card for "connect to X" intent.
- `_build_setup_requirements` accepts a `connected` flag to populate
`UserReadiness` correctly (`has_all_credentials`, `ready_to_run`,
`missing_credentials`).
- `blocks/mcp/helpers.py`: new `invalidate_mcp_credential(user_id,
credential_id)` via `IntegrationCredentialsManager.delete` (not
`store.delete_creds_by_id`) so the per-credential lock +
`_invoke_creds_changed_hook` fire (evicts cached provider token).
Top-level imports hoisted per AGENTS.md.
- `prompting.py`: strengthened the "Tool Discovery Priority" section.
After `find_block` returns nothing, the model MUST load `mcp_tool_guide`
and search the registry
(`https://registry.modelcontextprotocol.io/v0/servers?q=<service>`) via
`SendWebRequestBlock` (public, no creds) before pivoting to REST. Added
hostname-vendor-match safety check before any `run_mcp_tool` call on a
registry-returned URL. Added a hint for `surface_connect_card=true` on
"connect" intent.
### Frontend
- `oauth-popup.ts`: 1.5 s → **3 s** grace window after `popup.closed`
flips true, with a **final synchronous localStorage sweep** on close.
Covers the case where BroadcastChannel never delivers due to
storage-partitioning / BCG isolation, and the poll tick hasn't run yet.
AbortController cleanup tears down the grace timer on completion. Inline
comments document the timeout hierarchy (5-min outer, 500ms close poll,
3s close grace) so the next reader doesn't mistake them.
- `MCPSetupCard.tsx`:
- New **"Connected to {service}. Reconnect"** render branch when the
backend reports `has_all_credentials=true`. Reconnect runs the same
OAuth dance — lets users swap accounts without deleting first.
- Reset `showManualToken` on retry so a stale 400-fallback doesn't
shadow a later non-400 error.
- Drop the connected view on Reconnect failure so the error /
manual-token input render correctly.
- `settings/integrations/helpers.ts`:
- `PROVIDER_DISPLAY_NAME_OVERRIDES["mcp"] = "MCP"` — keeps the acronym
uppercase.
- New `stripProviderPrefix` helper (generic, case-insensitive). Any
provider whose backend-set title starts with `<DisplayName>: ` gets the
prefix stripped so the row label doesn't double up the group label.
Works for MCP today, applies to any future provider with the same
backend convention.
- `public/integrations/mcp.png`: official MCP "linked tape" logo
(MIT-licensed, sourced from Wikimedia Commons), content-bbox cropped +
10% padding → 256×256, visually centered for the 24×24 avatar circle.
### Tests
- `run_mcp_tool` tests: 3 new (stale-cred fires setup card + invalidate,
surface_connect_card connected/disconnected). All 32 tests pass.
- `helpers.test.ts`: 16 tests pass — added `mcp: "MCP"` override
coverage + prefix-strip generic test (mcp + github + non-prefixed
examples).
- `oauth-popup.test.ts` (new): 5 tests covering the popup grace logic +
abort cleanup.
## How
The stale-cred path is the load-bearing fix for John's bug: dropping the
`not creds` clause is one line, but pairing it with invalidation
prevents the user re-attempting the same call and looping on the dead
token. The OAuth grace window + final localStorage sweep is the smallest
possible fix for the Sentry popup-close race — no protocol changes, no
extra postMessage round-trips. The prompt change is complementary to PR
#13117 (still open) but tighter; if #13117 lands first I'll rebase. The
Connected/Reconnect UI is a backend-feature-flag-style toggle: model can
now request the card on demand without a discovery round-trip.
Integrations page UI is purely cosmetic + display-side — no migration,
no data change.
## Test plan
- [x] `pytest backend/copilot/tools/test_run_mcp_tool.py` — 32 passed (3
new)
- [x] `pnpm vitest run helpers.test.ts oauth-popup.test.ts` — 21 passed
- [x] `poetry run black + isort + ruff` clean
- [x] `pnpm types && pnpm lint && pnpm format` clean
- [x] **`/pr-test` local — full Sentry MCP flow end-to-end:** setup card
→ OAuth popup → Connected/Reconnect → auto-retry → tool execution. **See
the test report comment below for screenshots + scenario breakdown.**
- [x] **John's bug E2E:** inserted OAuth-shape stale cred → triggered
run_mcp_tool → setup card surfaced + stale cred auto-deleted
(`/api/integrations/credentials` returned `[]` post-call).
- [ ] Re-verify on dev preview after merge (live Sentry against
`dev-builder.agpt.co`).
## Related
- Discord report:
https://discord.com/channels/1126875755960336515/1285234928367308821/1507504078278754414
- Bug session (langfuse trace `3c4b1e7832dbe8e5f2fdcca870496f47`):
https://dev-builder.agpt.co/copilot?sessionId=c79e0ad0-bbbc-475f-815f-cb8d73dc4c9d
- Adjacent / complementary: #13117 (open) — auto-discover MCP
integrations prompt Z
Zamil Majdy committed
97a7bc188814db6cda7a2221b241742034c569a5
Parent: a13e70f
Committed by GitHub <noreply@github.com>
on 5/25/2026, 9:12:41 AM