Skip to content

fix(servers): complete MCP OAuth callback UX - #199

Open
vishu-bh wants to merge 2 commits into
mainfrom
fix/4684-mcp-oauth-callback-ux
Open

vishu-bh wants to merge 2 commits into
mainfrom
fix/4684-mcp-oauth-callback-ux

Conversation

@vishu-bh

@vishu-bh vishu-bh commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

OAuth authorization on the MCP Servers page opens the server's Components view, shows success inline, and reloads tools, resources, and prompts. Failures display the provider's bounded, validated plain-text reason in a dialog with retry. Dismissal preserves the shared Authorization required or Authorization expired status with an adjacent authorization action.

  • Only offer Authorize when the caller's token is missing or expired; preserve shared status icons, explanatory popovers, and compact table labels.
  • Keep authorization and component refresh independent. Panel refresh failures use inline feedback with retry, while table refresh failures use toasts. Refresh conflicts show a conflict toast without an ineffective retry action.
  • Handle cancellation, duplicate clicks, and late callbacks scoped to the initiating server; restore dialog focus to Authorize or the panel close control.
  • Reuse existing localized status text and preserve main's targeted refresh updates that avoid table flicker. Manual creation/editing and catalog OAuth flows retain their existing behavior.

Fixes IBM/mcp-context-forge#4684.

Validation

  • Full unit suite: 227 files passed; 3,817 tests passed and 1 skipped.
  • npm run build and npm run lint passed.
  • Formatting and git diff --check passed for the changes.
  • Four focused OAuth browser tests passed using installed Google Chrome, including failure → dismissal → retry → success with fresh components and the existing creation flow.
  • Regression coverage includes single-message refresh failures, table-versus-panel feedback, 409 without inline retry, expired and compact status presentation, valid/loading token action visibility, bounded/malformed provider descriptions, and dialog focus restoration.
  • Standalone e2e type checking previously reported errors in unchanged e2e/tools.spec.ts and src/utils/errors.ts.

Manual verification

On an existing OAuth authorization-code server without a valid caller token, deny consent, dismiss the failure dialog, retry from Authorization required, and complete authorization. Confirm inline success and freshly loaded components, then use Refresh components without reopening OAuth. Check that expired tokens retain Authorization expired, valid tokens do not offer Authorize, Escape dismisses only the dialog, and closing a pending popup produces no failure dialog.

@vishu-bh
vishu-bh marked this pull request as ready for review October 21, 2026 14:09
@vishu-bh
vishu-bh force-pushed the fix/4684-mcp-oauth-callback-ux branch from ceb9a29 to 54fada7 Compare October 21, 2026 14:10

@a-effort a-effort left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change requests inline; lmk if you want to discuss any of these proposals. 🙌

Comment thread src/components/servers/MCPServerDetailsPanel.tsx Outdated
Comment thread src/pages/Servers.tsx
Comment thread src/components/servers/MCPServerDetailsPanel.tsx Outdated
Comment thread src/components/servers/ServerStatusIndicator.tsx Outdated
Comment thread src/i18n/locales/en-US/mcpServer.json Outdated
Comment thread src/pages/Servers.tsx
Comment thread src/components/servers/MCPServerDetailsPanel.tsx Outdated
Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
@vishu-bh
vishu-bh force-pushed the fix/4684-mcp-oauth-callback-ux branch from 54fada7 to c7605dc Compare October 22, 2026 12:16
@vishu-bh
vishu-bh requested a review from a-effort October 22, 2026 12:19

@a-effort a-effort left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed again; nice work on addressing change requests! I notice that IBM/mcp-context-forge#4684 which was created in May had gone stale and the proposed interaction for servers that need authentication was established afterwards in #130. We should use that pattern. Details:

Here's a quick demo of the desired interaction: #130 (comment) (0:23 clicking "Auth" triggers a popup for OAuth without triggering the drawer to open). After auth is complete, the status changes to active (or remains the same if it was unsuccessful as the popup communicates failure).

We should use this pattern in the drawer, also. I created a couple of mockups to show the desired changes: authorization required / auth succeeded

I also moved the button for refresh to next to the search icon and changed the styling to match that. This should persist and uses the same mechanism as the refresh overflow menu item for the server in the server table.

Lmk if there are questions / things to discuss and apologies for any confusion coming from that stale issue.

Comment thread src/pages/Servers.tsx
Comment on lines +286 to +288
setSelectedServerIdForDetails(id);
setDetailsInitialTab("components");
setIsDetailsDrawerOpen(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines open the drawer and set its tab before triggerOAuthAuthorization runs, so the drawer opens even when the user cancels the popup or the provider never posts back. Remove them so the row chip only starts the popup. detailsInitialTab then has no writer for the components case.

Comment thread src/pages/Servers.tsx
onRetryOAuthStatus={panelServer ? () => void retryOAuthStatus(panelServer.id) : undefined}
/>
<ConfirmDialog
open={isDetailsDrawerOpen && panelAttempt?.state === "failed" && panelAttempt.dialogOpen}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dialog only opens when isDetailsDrawerOpen is true, and panelAttempt comes from panelServer at lines 109 to 111, which comes from selectedServerIdForDetails. Once the row click no longer opens the drawer, a failure started from a row gets no dialog. Key the dialog and the dismissed-failure state off the server being authorized instead.

Comment thread src/pages/Servers.tsx
Comment on lines +745 to +755
onCloseAutoFocus={(event) => {
const authorize = document.getElementById(
`oauth-authorize-${panelServer?.id}`,
) as HTMLButtonElement | null;
const target =
authorize && !authorize.disabled
? authorize
: document.getElementById(`mcp-server-close-${panelServer?.id}`);
if (target) {
event.preventDefault();
target.focus();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both ids here are rendered inside the drawer, so neither exists for an attempt started from a row. Return focus to the status chip that started the attempt. The close button fallback is then unused.

Comment on lines +433 to +449
<div className="mb-5 space-y-3">
<div className="flex items-center gap-2">
{onAuthorize && needsAuthorization && (
<Button
id={`oauth-authorize-${server.id}`}
variant="outline"
size="sm"
disabled={isAuthorizing}
onClick={() => void onAuthorize()}
>
{intl.formatMessage({
id: isAuthorizing
? "mcpServer.status.authorizing"
: "mcpServer.oauth.authorize",
})}
</Button>
)}

@a-effort a-effort Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this button. In the details panel, the user can click on "Authorization required" to authorize.

Comment on lines +450 to +463
{onRefresh && (
<Button
variant="outline"
size="sm"
disabled={isRefreshing || isAuthorizing}
onClick={() => void onRefresh()}
>
{intl.formatMessage({
id: isRefreshing
? "mcpServer.oauth.refreshing"
: "mcpServer.oauth.refresh",
})}
</Button>
)}

@a-effort a-effort Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move refresh next to search (icon style button). Line 530 already has the container with the search button. It should stay visible and enabled regardless of OAuth state, so drop isAuthorizing from the disabled condition and move the labels out of the mcpServer.oauth.* namespace.

Comment on lines +465 to +468
{oauthAttempt?.state === "succeeded" && (
<InlineNotification
type="success"
message={intl.formatMessage({ id: "mcpServer.oauth.success" })}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this. The status changing to Active is the success signal. mcpServer.oauth.success can be deleted from each locale file, nothing else uses it. The succeeded attempt state then has no reader; it is declared at src/api/oauth.ts:108 and set at Servers.tsx:304. It still clears a prior failure, so it can stay.

Comment on lines +684 to +688
<ServerStatusIndicator
server={server}
oauthStatus={oauthStatus}
onRetry={onRetryOAuthStatus}
authorizing={isAuthorizing}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass onAuthorize here. ServerStatusIndicator renders the chip as the authorize trigger when it has that prop and the status needs authorization, which is the pattern from #130.

Comment on lines +690 to +707
{needsAuthorization && onAuthorize && (
<Button
variant="outline"
size="xs"
disabled={isAuthorizing}
aria-label={intl.formatMessage(
{ id: "mcpServer.status.authorizeTrigger" },
{ name: server.name },
)}
onClick={() => void onAuthorize()}
>
{intl.formatMessage({
id: isAuthorizing
? "mcpServer.status.authorizing"
: "mcpServer.oauth.authorize",
})}
</Button>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this button once the indicator above takes onAuthorize. It also duplicates the label: line 688 puts the indicator into its authorizing state and line 703 sets this button to the same text, so both read Authorizing at once. The indicator tracks authorizing itself, so line 688 can go too.

ShaoLOWu added a commit to ShaoLOWu/contextforge-web-ui that referenced this pull request Oct 23, 2026
Closing the OAuth popup without completing authorization is a deliberate
user action, not a failure, so ServerCatalog no longer reports it.
Previously both `handleAuthorize` (page-level notice above the grid) and
`handleOAuthSubmit` (notice inside the Add-server dialog) passed the raw
`OAuthCancelledError` message ("OAuth authorization was cancelled",
untranslated) straight into their notification state.

Both catch blocks now return early on `OAuthCancelledError` instead of
notifying, mirroring the MCP Servers page (Servers.tsx), which already
discards the cancellation silently. Genuine authorization failures keep
surfacing their reason, and the dialog stays open for a retry in both
cases.

Tests: the cancelled-authorization test now uses `OAuthCancelledError`
and asserts no cancellation notice appears anywhere (previously it
rejected with a plain `Error` carrying the same text, covering cancel
and failure identically); a new dialog test covers the real-failure path
(provider reason still shown), and a new page-level test covers the
Authorize menu action staying quiet on cancellation.

Fixes IBM/mcp-context-forge#7303 (the catalog UI lives in this repo).

Note: surfacing the provider's denial reason via `OAuthAuthorizationError`
(the remaining suggestion in that issue) lands naturally after contextforge-org#199 adds
the class to `src/api/servers.ts`; this PR intentionally does not
duplicate it.

Verification: `vitest run src/pages/ServerCatalog.test.tsx` 78/78 green;
`tsc -b tsconfig.app.json` clean (after `orval` generation); eslint and
prettier clean on both changed files.

Assisted-by: AI (Claude via WorkBuddy) — diff reviewed and verified by
the human contributor before submission.
Signed-off-by: ShaoLOWu <3034261766@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CHORE]: MCP server OAuth callback ux

2 participants