Skip to content

feat: virtual server add source step - #131

Open
a-effort wants to merge 29 commits into
mainfrom
6404-source-picker-status
Open

a-effort wants to merge 29 commits into
mainfrom
6404-source-picker-status

Conversation

@a-effort

@a-effort a-effort commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #130, which adds the shared status indicator this uses. Please review that one first; this PR targets its branch so the diff stays readable.

The source picker now uses the shared derivation from #130. A source reads the same here as on the servers page, including OAuth sources the caller has not authorized yet. Auth note: this version includes a popover vs connecting to the OAuth authorize flow (IBM/mcp-context-forge#6859), because no authorize handler is wired into the picker.

Sources stay selectable in every state and components discovered from a source remain in the catalog when it goes unreachable, so a virtual server built from them still resolves. The health loop takes up to three intervals to mark a source unreachable, so this approach enables the user to move forward and fix any issues later, if there are any.

Tools, resources and prompts now fail independently, each with its own message (popover).

131.mp4

Part of IBM/mcp-context-forge#6404

Adds one availability derivation and one indicator for MCP server health,
starting with the servers page.

The "Authorization needed" state is new. OAuth tokens are held per user per
gateway, so an authorization_code server that a colleague authorized is still
unusable for everyone else. The health check treats the resulting 401 as
healthy on purpose, so those rows read Active with zero components today.
GET /oauth/status reports the caller's own token state, and the indicator
maps a missing or expired token to that state.

Retires the "warning" status. Two conflicting definitions existed: unreachable
but seen before, and reachable but stale over five minutes. The first is
Offline by another name; the second fires on any server the health loop has
not reached yet. Unreachable now splits into Offline and Connecting on whether
the server has ever responded.

Deletes ServerStatusBadge, which nothing rendered.

Closes #6464
Closes #6465

Signed-off-by: Anna Effort <anna.effort@ibm.com>
The sources step listed every MCP server with a status it did not act on, so
a user could select a server that contributes nothing and get an empty virtual
server with no explanation.

Sources stay selectable in every state. Marking a gateway unreachable skips the
cascade that disables its components, so an offline source keeps a usable
catalog and adding it produces a working virtual server whose calls fail until
the server returns. Instead of blocking, each row carries the shared status
indicator and a submit-time note names any selected source with nothing to add.

Splits the zero-component empty state, which covered three situations with one
sentence. A failed list now names itself and offers a retry, an unavailable
source explains why it is empty, and a healthy source exposing nothing still
reads as empty.

Attributes a failed component fetch to the source that caused it, rather than
failing the whole create with one unattributed error.

Extracts useOAuthTokenStatuses, shared with the servers page.

Closes #6404

Signed-off-by: Anna Effort <anna.effort@ibm.com>
@a-effort a-effort changed the title feat: explain source availability in the create virtual server picker feat: virtual server add source step Sep 16, 2026
@a-effort
a-effort marked this pull request as ready for review September 16, 2026 19:15
a-effort and others added 8 commits September 16, 2026 13:22
The unreachable detail text says components stay listed from the last
sync, which reads as a contradiction when the list is empty. Adds an
empty-state message per availability, and drops the status keys and the
ServerStatus type left unused by the shared indicator.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
States with nothing to resolve open a popover rather than a modal,
matching the visibility info affordance. Authorization needed starts the
OAuth flow on click instead of describing it behind a dialog, and falls
back to the popover where the caller has no way to run it. Turn on goes,
since Activate is already in the row menu.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
…r-status

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Recast the status explanations and empty states around the server rather than
ContextForge, and stop showing the last error on inactive servers, where the
health loop leaves it behind from an earlier outage.

Narrow the popover to max-w-xs and anchor it to the end of the status label.
The source picker also needed justify-self-start: its trigger is a grid item,
so it stretched to the full column and the panel anchored to the column edge.

Shorten the compact label to Auth, keeping the full word for screen readers.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Stack the status label and the authorizing label in one grid cell so the
wider of the two sets the width. Reserving it rather than setting a min-width
keeps it right per locale, where the two labels differ by different amounts.

The stack is start-aligned so the slack falls after the label, leaving the
icon and text where every other row has them.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
a-effort and others added 5 commits September 16, 2026 18:04
Recast the status explanations around the server rather than ContextForge,
and stop showing the last error on inactive servers, where the health loop
leaves it behind from an earlier outage.

Narrow the popover to max-w-xs and anchor it to the end of the status label.

Shorten the compact label to Auth, keeping the full word for screen readers.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Signed-off-by: Anna Effort <anna.effort@ibm.com>
…r-status

Signed-off-by: Anna Effort <anna.effort@ibm.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

OAuth status loading fails once the accumulated server list exceeds the API’s 100-ID limit.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Uses shared MCP availability states throughout virtual-server source selection and improves component-loading feedback.

Changes:

  • Adds shared OAuth status loading and availability-aware empty states.
  • Keeps unavailable sources selectable while warning about empty sources.
  • Adds independent component errors, retries, and source-attributed creation errors.
File summaries
File Description
src/types/server.ts Removes the obsolete status type.
src/pages/Servers.tsx Adopts the shared OAuth-status hook.
src/pages/CreateServer.tsx Adds shared statuses, retries, and attributed errors.
src/pages/CreateServer.test.tsx Updates status and error tests.
src/lib/serverStatus.ts Adds availability-specific empty-state messages.
src/lib/serverStatus.test.ts Validates presentation message IDs.
src/hooks/useOAuthTokenStatuses.ts Centralizes OAuth token-status loading.
src/components/gateways/SourceSelection.tsx Adds shared statuses and empty-source warnings.
src/components/gateways/SourceSelection.test.tsx Tests selection, warnings, and source names.
src/i18n/locales/en-US/mcpServer.json Updates English status messages.
src/i18n/locales/en-US/gateways.json Adds English source and loading messages.
src/i18n/locales/es-ES/mcpServer.json Updates Spanish status messages.
src/i18n/locales/es-ES/gateways.json Adds Spanish source and loading messages.
src/i18n/locales/pt-BR/mcpServer.json Updates Portuguese status messages.
src/i18n/locales/pt-BR/gateways.json Adds Portuguese source and loading messages.
Review details
  • Files reviewed: 15/15 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/hooks/useOAuthTokenStatuses.ts
The /oauth/status route rejects more than 100 ids, and a paged-through
server list can exceed that, so getOAuthStatus splits and merges.

A rejected refetch after authorization skipped the status reload and
surfaced as an unhandled rejection at the click handler.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
…r-status

# Conflicts:
#	src/pages/Servers.tsx
Load More issues a second lookup while the first may still be running,
and an earlier response carrying fewer ids would drop the rows it omits
back to reachability.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
@a-effort
a-effort force-pushed the 6404-source-picker-status branch from 182a4aa to 730f2f2 Compare September 18, 2026 00:09
@a-effort a-effort self-assigned this Sep 18, 2026
@a-effort

a-effort commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

LLM review feedback:

Reviewed the diff and the Copilot thread.

The Copilot comment about the 100-ID cap is resolved correctly — the fix belongs in getOAuthStatus itself rather than in the hook, so callers pass the full list and batching is handled once.

Extracting useOAuthTokenStatuses was the right call. The versioned-request pattern needed to live in a hook rather than inlined in the page, and Servers.tsx is cleaner for it.

The idsKey join as a memo dependency avoids array reference churn without needing a deep-equality comparison.

SourceComponentsError wrapping the per-server fetch failure is a clean way to attribute errors without surfacing them as unattributed strings at the creation level.

Per-list independent errors with partial success (components still render when only some lists fail) is better than the previous single componentError. The retry wiring is straightforward.

The emptySelectedSources warning being non-blocking is correct per the stated intent.

The race condition test in useOAuthTokenStatuses.test.ts covers the stale-result scenario that motivated the versioning.

@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

🟡 Medium — Silent partial failure on multi-source component fetch

File: src/pages/CreateServer.tsx:229

getComponentsForSelectedMCPServers wraps each source's component-fetch failure in a SourceComponentsError, but since the fetches run under Promise.all, it rejects on the first source to fail — any other failing sources are never surfaced.

Failure scenario: User selects two unreachable sources, A and B, and clicks Submit/Skip. Both component fetches reject, but Promise.all settles with whichever rejects first (say A). The UI shows only "A: <error>" — there's no indication B also failed to contribute components to the new virtual server.

Suggested fix: Use Promise.allSettled and aggregate all failing sources into the error message.


🟢 Low — Stale id can lose its friendly name on submit

File: src/components/gateways/SourceSelection.tsx:144

toggleMCPServerSelection builds namesById only from the current availableMCPServers. If that list is refetched and a previously selected id drops out before submit, selectedSourceIds still holds the stale id, but namesById has no entry for it.

Failure scenario: User selects a source; the sources list refetches and that source's id is no longer present in availableMCPServers (e.g. reassigned/removed upstream). The id stays in selectedMCPServerIds. On submit, if that source's component fetch fails, getCreateServerError falls back to showing the raw id instead of the source's friendly name.

Suggested fix: Edge case, minor UX regression only — low priority, but worth tracking namesById as accumulated state rather than deriving fresh from the latest fetch.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
The guard tested the availability state, but auth outranks inactive, so a
disabled server whose token had also expired kept rendering the error the
health loop left behind before it was turned off.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
a-effort and others added 3 commits September 23, 2026 09:37
Component fetches ran under Promise.all, so only the first source to reject
was named. They now settle and every failure is reported.

Source names were derived from the current list, which lost the name of a
selected source that a refetch had dropped. They are kept from selection time.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
@a-effort

Copy link
Copy Markdown
Contributor Author

Both fixed in b0a9a01.

getComponentsForSelectedMCPServers now uses Promise.allSettled and collects every rejected source, so the error names all of them rather than whichever failed first. SourceComponentsError carries the list and getCreateServerError joins them.

SourceSelection keeps each source's name from when it was selected instead of deriving the map from the current list, so an id that drops out still reports by name.

…r-status

Signed-off-by: Anna Effort <anna.effort@ibm.com>
@a-effort
a-effort added this pull request to stack #152 September 23, 2026 17:31

@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 🚀

Base automatically changed from 6404-shared-server-status-badge to main September 24, 2026 10:31

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

Since you'll have to sort out the merge conflicts anyway, here are a minor a11y suggestion on src/components/servers/ServerStatusIndicator.tsx:

  <PopoverContent
    align="end"
    className="w-auto max-w-xs p-3"
    aria-label={intl.formatMessage(
      { id: "mcpServer.status.trigger" },
      { name: server.name, status: fullLabel },
    )}
  >
    <ServerStatusDetail ... />
  </PopoverContent>

Adding a label to popover content to match WCAG rules (let's screen readers know the popup purpose)

@a-effort

Copy link
Copy Markdown
Contributor Author

Suggest holding this until #150 merges, then rebasing on top of it.

#150 adds useOAuthStatuses, which covers what useOAuthTokenStatuses does here and also handles request cancellation, versioning per id so one refresh cannot overwrite another, explicit loading and unavailable states, a retry path, and servers whose grant type is not authorization_code.

It also rewrites SourceSelection to drop the local status helpers and use ServerStatusIndicator and that hook instead, which is most of what this PR changes in that file.

If this merges first, we end up with two hooks under nearly the same name doing the same job, and #150 has to resolve against it.

This branch needs a rebase regardless, since #130 squash-merged and main now carries that content under a different commit. Rebasing once on top of #150 is less work than rebasing onto main now and repeating it afterwards.

The parts that do not overlap #150 carry over unchanged: the empty state strings for sources with no components, the active voice wording, and naming every failing source while keeping the names of dropped ones.

cc @vishu-bh, since the hook overlap sits mostly on your side.

@a-effort

Copy link
Copy Markdown
Contributor Author

@gcgoncalves , good call re the popover A11Y improvement - ty!

Added the fix to #153, which owns that popover now and created IBM/mcp-context-forge#6989 to improve this for other popovers, also.

Main carries #130 squashed and #153's refactor of ServerStatusIndicator
onto the shared status-indicator, so those files take main's version.
Servers.tsx keeps main's inline OAuth status block rather than this
branch's hook, which #150 replaces.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
@marekdano

Copy link
Copy Markdown
Contributor

@a-effort - please look at the failing Playwright tests.

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.

4 participants