Skip to content

feat: add shared OAuth connection validation - #220

Merged
vishu-bh merged 4 commits into
mainfrom
feat/5722-oauth-validation-core
Nov 5, 2026
Merged

vishu-bh merged 4 commits into
mainfrom
feat/5722-oauth-validation-core

Conversation

@vishu-bh

@vishu-bh vishu-bh commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Use the handwritten compatibility contract from #219 without changing openapi.json. Add the shared React foundation for stored-token connection validation: the exact-gateway API helper, cancellable per-gateway state, strict stage-result checks, reusable result panel, and localized error guidance in English, Spanish, and Portuguese. Preserve the structured OAuth callback fields and cover BFF forwarding.

Part 2 of the frontend split for IBM/mcp-context-forge#5722. Based on #219 in native GitHub stack #223. This PR adds the reusable implementation; page-specific controls follow in the MCP Servers and Catalog PRs.

Validation: production build, ESLint, and full frontend coverage suite. API, hook, panel, callback, and locale tests cover compatibility, failures, cancellation, stale responses, and permission loss. The BFF callback regression suite passed (6 tests).

Native GitHub stack: #223, rooted at main.

  1. feat: define OAuth connection validation contract #219 — Handwritten OAuth compatibility contract; base: main.
  2. feat: add shared OAuth connection validation #220 — Shared validation; base: feat: define OAuth connection validation contract #219's branch.
  3. feat: surface OAuth validation on MCP Servers #221 — MCP Servers UI; base: feat: add shared OAuth connection validation #220's branch.
  4. feat: surface OAuth validation on Catalog cards #222 — Catalog UI; base: feat: surface OAuth validation on MCP Servers #221's branch.

#218 contains the independent E2E typing fix and can merge separately. These PRs replace #216.

@vishu-bh
vishu-bh changed the base branch from chore/5722-openapi-contract to main November 3, 2026 12:15
@vishu-bh
vishu-bh changed the base branch from main to chore/5722-openapi-contract November 3, 2026 12:31
@vishu-bh
vishu-bh added this pull request to stack #223 November 3, 2026 12:31
@vishu-bh
vishu-bh marked this pull request as ready for review November 3, 2026 12:45

@marekdano marekdano 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.

Findings

Suggestions

  1. A gateway ID mismatch is reported as a network error (src/hooks/useOAuthConnectionValidation.ts:673, :689)
    throw new Error("Gateway mismatch") isn't an ApiError, so the catch block maps it to network. The user sees "could not reach the backend", which is wrong. Any non-ApiError thrown by a resolver function ends up the same way. Map it to a distinct code, such as gateway_unavailable or request_invalid, and add a test for it.

  2. A response with an unknown stage value can show a missing translation (src/components/servers/OAuthConnectionValidationPanel.tsx:393)
    If the backend returns a stage value outside ok/failed/skipped/unsupported, the hook correctly flags the result as incompatible. The panel still renders result.stages, and outcome.${status} has no key for that value. react-intl then shows the raw message id and logs an error. Fall back to skipped, or hide the stage list when the stages aren't valid.

  3. A "success" response with a non-ok stage gets a generic error (src/hooks/useOAuthConnectionValidation.ts:679)
    When success: true but tools_list is skipped or unsupported, result.errorCode is usually undefined. The user then sees the generic "OAuth connection failed…" message, even though tools_unsupported exists. Consider deriving the code from the failing stage when the backend sends none. The test at line 481 only asserts state === "failure".

  4. oauthConfigurationKey depends on key order (src/lib/oauthConnectionValidation.ts:962)
    JSON.stringify(server.oauthConfig) changes if the backend reorders keys between fetches, which would reset a valid result for no reason. oauthConfigurationKey and canValidateOAuthConnection are also unused and untested in this PR. If #221 and #222 are the only consumers, either note that in the description or add small unit tests here.

Nits

  • success! in the hook tests is unnecessary because success is a typed constant.
  • The BFF test in server/test/oauth-callback.test.ts only shows that a mocked backend body passes through the proxy. Rename it, or add a case with an unsafe field, since "safe" isn't actually exercised.
  • test() replaces the entry with { state: "running" }, so the last result disappears while retesting. Confirm that's intended.

@vishu-bh
vishu-bh force-pushed the feat/5722-oauth-validation-core branch from 98e8ef7 to 27b885e Compare November 3, 2026 14:34
return (
<li key={stage} className="flex items-center gap-1.5">
<Icon
className={`size-4 ${status === "ok" ? "text-success" : status === "failed" ? "text-destructive" : "text-muted-foreground"}`}

@gcgoncalves gcgoncalves Nov 3, 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.

use STATUS_ICON and STATUS_TONE_CLASS (from @/lib/status) for setting the colours for consistency.

Comment on lines +38 to +46
<Button type="button" variant="outline" size="xs" disabled={running} onClick={onTest}>
{running && <Loader2 className="size-3.5 animate-spin" aria-hidden="true" />}
{message(running ? "testing" : entry ? "retest" : "test")}
</Button>
{running && (
<Button type="button" variant="ghost" size="xs" onClick={onCancel}>
{message("cancel")}
</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.

If a user clicks Test connection, the button becomes disabled, and focus drops to <body>. Clicking Cancel unmounts that button as well. In both cases, a11y users have to navigate back from the top of the page. Keep the button focusable with aria-disabled and ignore clicks while running, or move focus to Cancel on start and back to the Test button on cancel/finish:

<Button aria-disabled={running} onClick={running ? undefined : onTest}>

@gcgoncalves
gcgoncalves force-pushed the feat/5722-oauth-validation-core branch from 27b885e to 7a0744e Compare November 4, 2026 09:09
Comment thread src/lib/oauthConnectionValidation.ts Resolved
Base automatically changed from chore/5722-openapi-contract to main November 4, 2026 13:15
@vishu-bh
vishu-bh force-pushed the feat/5722-oauth-validation-core branch 2 times, most recently from bbb6554 to cdd676b Compare November 4, 2026 13:19
@gcgoncalves
gcgoncalves force-pushed the feat/5722-oauth-validation-core branch from a589e7b to bb2b09e Compare November 5, 2026 10:52
@marekdano
marekdano force-pushed the feat/5722-oauth-validation-core branch from bb2b09e to 1adbba2 Compare November 5, 2026 11:27

@marekdano marekdano 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.

Couple of findings listed below

Comment on lines +102 to +106
401: "unauthenticated",
403: "permission_denied",
422: "request_invalid",
429: "rate_limited",
404: "gateway_unavailable",

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.

Only 401, 403, 404, 422 and 429 get specific codes. Any other ApiError, such as a 502, 503 or 504 from the gateway or BFF, falls through to "generic" (line 108).
gateway_unavailable and timeout already exist and have localized messages. Consider mapping 502 and 503 to gateway_unavailable and 504 to timeout, with a test for each.

Comment on lines +66 to +71
let resolvingTarget = true;
try {
const resolved = typeof target === "function" ? await target(controller.signal) : target;
if (!isCurrent()) return;
if (resolved.gatewayId !== id) throw new ApiError(422, null, "Gateway mismatch");
resolvingTarget = false;

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.

lines 66,71,109 (cause: src/api/servers.ts:225)
A non-ApiError thrown after resolvingTarget = false is shown as "network". validateServerId(gatewayId) runs inside testOAuthConnection (servers.ts:225), so an invalid ID throws a plain Error that is reported as a network failure. A gateway mismatch (line 70) reports request_invalid, so the two are inconsistent. Validate the ID before the request, or catch it separately.

Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
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 feat/5722-oauth-validation-core branch from 1adbba2 to 3eb8aa6 Compare November 5, 2026 13:07

@marekdano marekdano 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.

The PR looks good now!

LGTM 🚀

Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
@vishu-bh
vishu-bh force-pushed the feat/5722-oauth-validation-core branch from 3eb8aa6 to 6cf2f1f Compare November 5, 2026 13:14
@vishu-bh
vishu-bh merged commit 1c7a4a5 into main Nov 5, 2026
5 checks passed
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.

3 participants