Skip to content

feat(lightspeed): add MCP servers settings panel#2582

Open
ciiay wants to merge 12 commits intoredhat-developer:mainfrom
ciiay:rhidp-12076-add-mcp-servers-page
Open

feat(lightspeed): add MCP servers settings panel#2582
ciiay wants to merge 12 commits intoredhat-developer:mainfrom
ciiay:rhidp-12076-add-mcp-servers-page

Conversation

@ciiay
Copy link
Copy Markdown
Member

@ciiay ciiay commented Mar 21, 2026

Hey, I just made a Pull Request!

For RHIDP-12076

Changeset and documentation updates will be included in the following pr for RHIDP-12079.

✔️ Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes
  • Screenshots attached (for UI changes)
rhidp_12076.mp4

How to test

  1. follow steps on LightspeedAI but use code base from the following steps
  2. use latest main branch code from https://github.com/lightspeed-core/lightspeed-stack
  3. update run.yaml
apis:
  - agents
  - inference
  - safety
  - tool_runtime
  - vector_io
  - files
container_image: null
distro_name: developer-lightspeed-lls-0.5.x
external_providers_dir: null
providers:
  agents:
    - config:
        persistence:
          agent_state:
            backend: kv_default
            namespace: agents
          responses:
            backend: sql_default
            table_name: responses
      provider_id: meta-reference
      provider_type: inline::meta-reference
  files:
    - config:
        metadata_store:
          backend: sql_default
          table_name: files_metadata
        storage_dir: /tmp/llama-stack-files
      provider_id: localfs
      provider_type: inline::localfs
  inference:
    - config:
        api_token: ${env.VLLM_API_KEY:=}
        base_url: ${env.VLLM_URL:=}
        max_tokens: ${env.VLLM_MAX_TOKENS:=4096}
        network:
          tls:
            verify: ${env.VLLM_TLS_VERIFY:=true}
      provider_id: ${env.ENABLE_VLLM:+vllm}
      provider_type: remote::vllm
    - config:
        base_url: ${env.OLLAMA_URL:=http://localhost:11434/v1}
      provider_id: ${env.ENABLE_OLLAMA:+ollama}
      provider_type: remote::vllm
    - config:
        api_key: ${env.OPENAI_API_KEY:=}
      provider_id: ${env.ENABLE_OPENAI:+openai}
      provider_type: remote::openai
    - config:
        location: ${env.VERTEX_AI_LOCATION:=global}
        project: ${env.VERTEX_AI_PROJECT:=}
      provider_id: ${env.ENABLE_VERTEX_AI:+vertexai}
      provider_type: remote::vertexai
    - config: {}
      provider_id: sentence-transformers
      provider_type: inline::sentence-transformers
    - config:
        api_token: ${env.SAFETY_API_KEY:=}
        base_url: ${env.SAFETY_URL:=http://ollama:11434/v1}
      provider_id: ${env.ENABLE_SAFETY:+safety-guard}
      provider_type: remote::vllm
  safety:
    - config:
        excluded_categories: []
      provider_id: ${env.ENABLE_SAFETY:+llama-guard}
      provider_type: inline::llama-guard
  tool_runtime:
    - config: {}
      provider_id: model-context-protocol
      provider_type: remote::model-context-protocol
    - config: {}
      provider_id: rag-runtime
      provider_type: inline::rag-runtime
  vector_io:
    - config:
        persistence:
          backend: kv_default
          namespace: vector_io::faiss
      provider_id: rhdh-docs
      provider_type: inline::faiss
registered_resources:
  models:
    - metadata: {}
      model_id: ${env.SAFETY_MODEL:=llama-guard3:8b}
      model_type: llm
      provider_id: ${env.ENABLE_SAFETY:+safety-guard}
      provider_model_id: ${env.SAFETY_MODEL:=llama-guard3:8b}
  shields:
    - provider_id: ${env.ENABLE_SAFETY:+llama-guard}
      provider_shield_id: safety-guard/${env.SAFETY_MODEL:=llama-guard3:8b}
      shield_id: llama-guard-shield
  tool_groups:
    - provider_id: rag-runtime
      toolgroup_id: builtin::rag
server:
  auth: null
  host: null
  port: 8321
  quota: null
  tls_cafile: null
  tls_certfile: null
  tls_keyfile: null
storage:
  backends:
    kv_default:
      db_path: /tmp/kvstore.db
      type: kv_sqlite
    sql_default:
      db_path: /tmp/sql_store.db
      type: sql_sqlite
  stores:
    conversations:
      backend: sql_default
      table_name: openai_conversations
    inference:
      backend: sql_default
      max_write_queue_size: 10000
      num_writers: 4
      table_name: inference_store
    metadata:
      backend: kv_default
      namespace: registry
version: 3
  1. update lightspeed-stack.yaml as following:
name: Lightspeed Core Service (LCS)
service:
  host: 0.0.0.0
  port: 8080
  auth_enabled: false
  workers: 1
  color_log: true
  access_log: true
llama_stack:
  use_as_library_client: false
  url: http://localhost:8321
user_data_collection:
  feedback_enabled: false
  feedback_storage: '/tmp/data/feedback'
authentication:
  module: 'noop'
conversation_cache:
  type: 'sqlite'
  sqlite:
    db_path: '/tmp/cache.db'
mcp_servers:
  - name: mcp-integration-tools
    provider_id: "model-context-protocol"
    url: "http://localhost:7008/api/mcp-actions/v1"
    authorization_headers:
      Authorization: "client"
  - name: test-mcp-server
    provider_id: "model-context-protocol"
    url: "http://localhost:8888/mcp"
    authorization_headers:
      Authorization: "client"

@rhdh-gh-app
Copy link
Copy Markdown

rhdh-gh-app bot commented Mar 21, 2026

Missing Changesets

The following package(s) are changed by this PR but do not have a changeset:

  • @red-hat-developer-hub/backstage-plugin-lightspeed-backend
  • @red-hat-developer-hub/backstage-plugin-lightspeed

See CONTRIBUTING.md for more information about how to add changesets.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-lightspeed-backend workspaces/lightspeed/plugins/lightspeed-backend none v1.4.0
@red-hat-developer-hub/backstage-plugin-lightspeed workspaces/lightspeed/plugins/lightspeed none v1.4.0

@ciiay ciiay force-pushed the rhidp-12076-add-mcp-servers-page branch 3 times, most recently from 4d9b9ea to dec2e83 Compare March 31, 2026 18:40
@ciiay ciiay marked this pull request as ready for review March 31, 2026 18:49
@rhdh-qodo-merge
Copy link
Copy Markdown

Review Summary by Qodo

Add MCP servers settings panel with management UI

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add MCP servers settings panel with table UI for managing servers
• Implement network error handling with detailed error messages
• Add Bearer token validation and formatting in MCP server validator
• Support fullscreen and embedded layout modes for settings panel
• Add PatternFly React Table dependency for server list display
Diagram
flowchart LR
  A["MCP Server Validator"] -->|Enhanced Error Handling| B["Network Error Messages"]
  A -->|Token Formatting| C["Bearer Token Validation"]
  D["LightspeedChat Component"] -->|Settings State| E["MCP Settings Panel"]
  E -->|Display| F["MCP Servers Table"]
  D -->|Layout Modes| G["Fullscreen/Embedded View"]
  H["LightspeedChatBoxHeader"] -->|Settings Click| E
Loading

Grey Divider

File Changes

1. workspaces/lightspeed/plugins/lightspeed-backend/src/service/mcp-server-validator.ts Error handling +80/-15

Enhanced error handling and token validation

• Add getEndpointLabel() helper to extract hostname:port from URLs
• Add getNestedError() helper to extract nested error causes
• Add getNetworkErrorMessage() function with comprehensive error detection for timeout, connection
 refused, host not found, connection reset, and host unreachable scenarios
• Replace generic timeout error handling with detailed network error messages
• Add Bearer token validation and formatting to ensure proper Authorization header format

workspaces/lightspeed/plugins/lightspeed-backend/src/service/mcp-server-validator.ts


2. workspaces/lightspeed/plugins/lightspeed/package.json Dependencies +1/-0

Add PatternFly React Table dependency

• Add @patternfly/react-table dependency version ^6.4.1 for table component support

workspaces/lightspeed/plugins/lightspeed/package.json


3. workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChat.tsx ✨ Enhancement +179/-64

Integrate MCP settings panel with layout modes

• Import Settings component from PatternFly and McpServersSettings component
• Add isMcpSettingsOpen state to manage settings panel visibility
• Add CSS classes for fullscreen layout, MCP panes, and settings styling
• Extract chat content into chatMainContent variable for reusability
• Implement conditional rendering for MCP settings panel in fullscreen and embedded modes
• Add drawer panel style configuration based on fullscreen mode and settings state
• Close MCP settings when creating new chat or selecting conversation
• Pass onMcpSettingsClick callback to header component

workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChat.tsx


View more (3)
4. workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx ✨ Enhancement +30/-0

Add MCP settings menu item to header

• Add onMcpSettingsClick prop to component interface
• Import SvgIcon from Material-UI for custom icon rendering
• Add MCP settings dropdown menu item with custom SVG icon
• Wire up MCP settings click handler to open settings panel

workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx


5. workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx ✨ Enhancement +570/-0

New MCP servers settings management component

• Create new component for managing MCP servers with table-based UI
• Implement server list fetching from backend API endpoint
• Add server validation with automatic retry for enabled servers
• Implement enable/disable toggle for each server with PATCH request
• Display server status with icons and detailed information
• Support sorting servers by name in ascending/descending order
• Show count of selected/enabled servers
• Handle loading states, errors, and validation failures
• Provide tooltips for failed server validation errors

workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx


6. workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx Additional files +179/-64

...

workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx


Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge bot commented Mar 31, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX Issues (0)

Grey Divider


Action required

1. Tokenless server toggle enabled📎 Requirement gap ⛨ Security
Description
Servers without a valid token can still be toggled on because getDisplayStatus() returns
disabled before checking hasToken, so the switch is not disabled when the server is currently
disabled. This violates the requirement that auth-required servers cannot be enabled without a valid
token.
Code

workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[R211-216]

+const getDisplayStatus = (server: McpServer): ServerStatus => {
+  if (!server.enabled) return 'disabled';
+  if (!server.hasToken) return 'tokenRequired';
+  if (server.status === 'error') return 'failed';
+  if (server.status === 'connected') return 'ok';
+  return 'unknown';
Evidence
The checklist requires token-required servers to be blocked from enabling without a valid token. In
getDisplayStatus(), the token check is skipped whenever enabled is false, which keeps
displayStatus as disabled and thus the toggle remains enabled (since isUnavailable only checks
failed/tokenRequired).

Auth-required MCP servers cannot be enabled without a valid token
workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[211-216]
workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[499-515]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Token-required MCP servers can be enabled because the toggle is not disabled when a server is currently disabled but lacks a token.

## Issue Context
`getDisplayStatus()` returns `disabled` whenever `server.enabled` is `false`, which prevents the UI from classifying the server as `tokenRequired` and disabling the switch.

## Fix Focus Areas
- workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[211-216]
- workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[499-515]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. MCP settings permission mismatch🐞 Bug ☼ Reliability
Description
McpServersSettings automatically calls POST /mcp-servers/:name/validate on load and allows
PATCH /mcp-servers/:name from the toggle, but both backend routes require lightspeed.mcp.manage
permission. Users who can only list MCP servers (lightspeed.mcp.read) will hit 403s and the
settings panel will error or be non-functional.
Code

workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[R291-346]

+  const validateServer = useCallback(
+    async (serverName: string) => {
+      const baseUrl = getBaseUrl();
+      const data = await fetchJson<McpServersValidateResponse>(
+        `${baseUrl}/mcp-servers/${encodeURIComponent(serverName)}/validate`,
+        {
+          method: 'POST',
+        },
+      );
+
+      setServers(prev =>
+        prev.map(server =>
+          server.name === serverName
+            ? {
+                ...server,
+                status: data.status,
+                toolCount: data.toolCount,
+                validationError:
+                  data.status === 'error'
+                    ? (data.validation?.error ?? 'Validation failed')
+                    : undefined,
+              }
+            : server,
+        ),
+      );
+    },
+    [fetchJson, getBaseUrl],
+  );
+
+  const loadServers = useCallback(async () => {
+    setIsLoading(true);
+    setError(null);
+    try {
+      const baseUrl = getBaseUrl();
+      const data = await fetchJson<McpServersListResponse>(
+        `${baseUrl}/mcp-servers`,
+      );
+      const uiServers = (data.servers ?? []).map(server => toUiServer(server));
+      setServers(uiServers);
+
+      const serversToValidate = uiServers.filter(server => server.hasToken);
+      void Promise.allSettled(
+        serversToValidate.map(async server => {
+          try {
+            await validateServer(server.name);
+          } catch (validationError) {
+            setError(
+              prev =>
+                prev ??
+                (validationError instanceof Error
+                  ? validationError.message
+                  : `Failed to validate ${server.name}`),
+            );
+          }
+        }),
+      );
Evidence
Backend route authorization differs: listing is guarded by MCP read permission, while
validate-by-name and patch are guarded by MCP manage permission. The UI calls validate-by-name
unconditionally in loadServers for every server with a token and wires PATCH directly to the
switch, with no permission gating; additionally, the header always exposes the MCP settings
entrypoint.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts[180-200]
workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts[250-256]
workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts[315-321]
workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[320-346]
workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[506-515]
workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx[236-262]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new MCP settings UI calls manage-protected endpoints (`POST /mcp-servers/:name/validate` and `PATCH /mcp-servers/:name`) without checking whether the user has `lightspeed.mcp.manage`. In permissioned deployments where `lightspeed.mcp.read` is granted but `lightspeed.mcp.manage` is not, opening the settings panel will produce 403 errors (auto-validation on load) and toggles/actions will fail.

### Issue Context
Backend authorization:
- `GET /mcp-servers` requires `lightspeedMcpReadPermission`.
- `POST /mcp-servers/:name/validate` and `PATCH /mcp-servers/:name` require `lightspeedMcpManagePermission`.

Frontend behavior:
- `loadServers()` triggers validation calls for all servers with tokens.
- The toggle triggers PATCH.
- The MCP settings entrypoint is always visible in the header dropdown.

### Fix Focus Areas
- workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[291-346]
- workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[506-515]
- workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBoxHeader.tsx[236-262]

### Suggested change
1. Use `usePermission` (from `@backstage/plugin-permission-react`) to check:
  - `lightspeedMcpReadPermission` before showing the MCP settings entry.
  - `lightspeedMcpManagePermission` before enabling actions that call validate/patch.
2. Do not auto-call `POST /mcp-servers/:name/validate` on mount unless the user has manage permission; instead rely on the stored `status/toolCount` from `GET /mcp-servers`, and make validation an explicit user action.
3. If the user lacks manage permission, render the table in a read-only state (disable switches/edit) and show a clear inline message rather than surfacing raw 403 errors.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Bearer prefix breaks auth🐞 Bug ≡ Correctness
Description
McpServerValidator.validate now rewrites the provided token into Bearer <token>, changing the
established contract where the token is treated as the full Authorization header value. This will
cause MCP validation to fail against servers (and in-repo tests/fixtures) that expect the raw token
string or a different auth scheme.
Code

workspaces/lightspeed/plugins/lightspeed-backend/src/service/mcp-server-validator.ts[R109-116]

+    const trimmedToken = token.trim();
+    const authorizationHeader = /^Bearer\s+/i.test(trimmedToken)
+      ? trimmedToken
+      : `Bearer ${trimmedToken}`;
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
-      Authorization: `${token}`,
+      Authorization: authorizationHeader,
      Accept: 'application/json, text/event-stream',
Evidence
The validator now forces a Bearer prefix. The repo’s MCP mock server (used by backend tests)
explicitly expects Authorization to equal the raw token, and the backend router’s MCP header
builder documents/passes the token verbatim as an Authorization header value—both are incompatible
with forced Bearer prefixing.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/mcp-server-validator.ts[108-117]
workspaces/lightspeed/plugins/lightspeed-backend/fixtures/mcpHandlers.ts[28-33]
workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts[60-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`McpServerValidator.validate` currently coerces any token into a `Bearer` token unless it already starts with `Bearer`. This breaks the existing contract in this repo where `token` is treated as the full `Authorization` header value (tests/fixtures expect raw token values).

### Issue Context
- Current tests/fixtures for the mock MCP server expect `Authorization` to exactly equal the raw token value.
- `buildMcpHeaders` in the backend router also treats `token` as the full Authorization header value.

### Fix Focus Areas
- workspaces/lightspeed/plugins/lightspeed-backend/src/service/mcp-server-validator.ts[108-117]

### Suggested change
- Use the token verbatim (optionally trimmed) as `Authorization`.
- If you want to support both "raw" and "Bearer" formats, do not mutate the token; instead document that callers should provide the full header value (e.g., "Bearer ...") or introduce an explicit config option to control scheme.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Missing empty MCP state📎 Requirement gap ☼ Reliability
Description
When sortedServers is empty and isLoading is false, the table body renders no rows and shows
no empty-state message. This violates the requirement to display a clear empty state when no MCP
servers are available.
Code

workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[R478-565]

+        <Tbody>
+          {isLoading && (
+            <Tr>
+              <Td colSpan={4}>Loading MCP servers...</Td>
+            </Tr>
+          )}
+          {sortedServers.map(server => {
+            const displayStatus = getDisplayStatus(server);
+            const displayDetail = getDisplayDetail(server, displayStatus);
+            let statusClass = classes.statusWarn;
+            if (displayStatus === 'ok') {
+              statusClass = classes.statusOk;
+            } else if (displayStatus === 'tokenRequired') {
+              statusClass = classes.statusToken;
+            } else if (displayStatus === 'disabled') {
+              statusClass = classes.statusDisabled;
+            }
+
+            return (
+              <Tr key={server.id}>
+                <Td width={10} className={classes.toggleCell}>
+                  {(() => {
+                    const isUnavailable =
+                      displayStatus === 'failed' ||
+                      displayStatus === 'tokenRequired';
+                    const isChecked = isUnavailable ? false : server.enabled;
+                    const isRowSaving = Boolean(isSaving[server.name]);
+
+                    return (
+                      <Switch
+                        id={`mcp-switch-${server.id}`}
+                        aria-label={`Toggle ${server.name}`}
+                        isChecked={isChecked}
+                        isDisabled={isUnavailable || isRowSaving}
+                        onChange={(_event, checked) => {
+                          patchServer(server.name, { enabled: checked });
+                        }}
+                      />
+                    );
+                  })()}
+                </Td>
+                <Td
+                  width={35}
+                  className={`${classes.rowName} ${classes.nameCell}`}
+                >
+                  <Typography component="span" className={classes.nameValue}>
+                    {server.name}
+                  </Typography>
+                </Td>
+                <Td width={40} className={classes.statusColumnCell}>
+                  <div className={classes.statusCell}>
+                    {getStatusIcon(displayStatus, statusClass)}
+                    {displayStatus === 'failed' ? (
+                      <Tooltip
+                        content={
+                          server.validationError ??
+                          'Validation failed. Check server URL and token.'
+                        }
+                      >
+                        <Typography
+                          component="span"
+                          className={classes.statusValue}
+                        >
+                          {displayDetail}
+                        </Typography>
+                      </Tooltip>
+                    ) : (
+                      <Typography
+                        component="span"
+                        className={classes.statusValue}
+                      >
+                        {displayDetail}
+                      </Typography>
+                    )}
+                  </div>
+                </Td>
+                <Td width={15} isActionCell style={{ textAlign: 'right' }}>
+                  <Button
+                    aria-label={`Edit ${server.name}`}
+                    icon={<ModeEditOutlineOutlinedIcon fontSize="small" />}
+                    variant="plain"
+                    className={classes.actionButton}
+                    onClick={onEditClick}
+                  />
+                </Td>
+              </Tr>
+            );
+          })}
Evidence
The checklist requires an explicit empty state when no servers are returned. The component only
renders a loading row and otherwise maps sortedServers; there is no conditional rendering for the
empty list case.

Empty state is shown when no MCP servers are available
workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[478-565]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The MCP servers panel shows a blank table when there are zero servers.

## Issue Context
`<Tbody>` only renders a loading row or `sortedServers.map(...)`. When `sortedServers.length === 0` and `isLoading === false`, nothing is rendered to explain the empty list.

## Fix Focus Areas
- workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[478-565]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Status shows tool count 📎 Requirement gap ≡ Correctness
Description
For enabled/connected servers, the status text shows ${toolCount} tools (and can also show
Failed/Unknown) instead of reflecting only the required set: Enabled, Disabled, or Token
required. This does not match the specified status indicator values.
Code

workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[R219-231]

+const getDisplayDetail = (
+  server: McpServer,
+  displayStatus: ServerStatus,
+): string => {
+  if (displayStatus === 'disabled') return 'Disabled';
+  if (displayStatus === 'tokenRequired') return 'Token required';
+  if (displayStatus === 'failed') return 'Failed';
+  if (displayStatus === 'ok') {
+    const suffix = server.toolCount === 1 ? 'tool' : 'tools';
+    return `${server.toolCount} ${suffix}`;
+  }
+  return 'Unknown';
+};
Evidence
The checklist defines the allowed status indicator values. getDisplayDetail() returns `Token
required and Disabled`, but for the enabled case it returns a tool-count string and also
introduces other labels like Failed and Unknown, which do not match the required set.

MCP servers list displays required server attributes and status indicators
workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[219-231]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The MCP server status display does not conform to the required status label set (Enabled/Disabled/Token required).

## Issue Context
`getDisplayDetail()` uses tool counts for the "ok" case and exposes additional labels like "Failed" and "Unknown".

## Fix Focus Areas
- workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[219-231]
- workspaces/lightspeed/plugins/lightspeed/src/components/McpServersSettings.tsx[484-552]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +211 to +216
const getDisplayStatus = (server: McpServer): ServerStatus => {
if (!server.enabled) return 'disabled';
if (!server.hasToken) return 'tokenRequired';
if (server.status === 'error') return 'failed';
if (server.status === 'connected') return 'ok';
return 'unknown';

This comment was marked as resolved.

Comment on lines +478 to +565
<Tbody>
{isLoading && (
<Tr>
<Td colSpan={4}>Loading MCP servers...</Td>
</Tr>
)}
{sortedServers.map(server => {
const displayStatus = getDisplayStatus(server);
const displayDetail = getDisplayDetail(server, displayStatus);
let statusClass = classes.statusWarn;
if (displayStatus === 'ok') {
statusClass = classes.statusOk;
} else if (displayStatus === 'tokenRequired') {
statusClass = classes.statusToken;
} else if (displayStatus === 'disabled') {
statusClass = classes.statusDisabled;
}

return (
<Tr key={server.id}>
<Td width={10} className={classes.toggleCell}>
{(() => {
const isUnavailable =
displayStatus === 'failed' ||
displayStatus === 'tokenRequired';
const isChecked = isUnavailable ? false : server.enabled;
const isRowSaving = Boolean(isSaving[server.name]);

return (
<Switch
id={`mcp-switch-${server.id}`}
aria-label={`Toggle ${server.name}`}
isChecked={isChecked}
isDisabled={isUnavailable || isRowSaving}
onChange={(_event, checked) => {
patchServer(server.name, { enabled: checked });
}}
/>
);
})()}
</Td>
<Td
width={35}
className={`${classes.rowName} ${classes.nameCell}`}
>
<Typography component="span" className={classes.nameValue}>
{server.name}
</Typography>
</Td>
<Td width={40} className={classes.statusColumnCell}>
<div className={classes.statusCell}>
{getStatusIcon(displayStatus, statusClass)}
{displayStatus === 'failed' ? (
<Tooltip
content={
server.validationError ??
'Validation failed. Check server URL and token.'
}
>
<Typography
component="span"
className={classes.statusValue}
>
{displayDetail}
</Typography>
</Tooltip>
) : (
<Typography
component="span"
className={classes.statusValue}
>
{displayDetail}
</Typography>
)}
</div>
</Td>
<Td width={15} isActionCell style={{ textAlign: 'right' }}>
<Button
aria-label={`Edit ${server.name}`}
icon={<ModeEditOutlineOutlinedIcon fontSize="small" />}
variant="plain"
className={classes.actionButton}
onClick={onEditClick}
/>
</Td>
</Tr>
);
})}

This comment was marked as resolved.

Comment on lines +109 to 116
const trimmedToken = token.trim();
const authorizationHeader = /^Bearer\s+/i.test(trimmedToken)
? trimmedToken
: `Bearer ${trimmedToken}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `${token}`,
Authorization: authorizationHeader,
Accept: 'application/json, text/event-stream',

This comment was marked as resolved.

Comment on lines +291 to +346
const validateServer = useCallback(
async (serverName: string) => {
const baseUrl = getBaseUrl();
const data = await fetchJson<McpServersValidateResponse>(
`${baseUrl}/mcp-servers/${encodeURIComponent(serverName)}/validate`,
{
method: 'POST',
},
);

setServers(prev =>
prev.map(server =>
server.name === serverName
? {
...server,
status: data.status,
toolCount: data.toolCount,
validationError:
data.status === 'error'
? (data.validation?.error ?? 'Validation failed')
: undefined,
}
: server,
),
);
},
[fetchJson, getBaseUrl],
);

const loadServers = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const baseUrl = getBaseUrl();
const data = await fetchJson<McpServersListResponse>(
`${baseUrl}/mcp-servers`,
);
const uiServers = (data.servers ?? []).map(server => toUiServer(server));
setServers(uiServers);

const serversToValidate = uiServers.filter(server => server.hasToken);
void Promise.allSettled(
serversToValidate.map(async server => {
try {
await validateServer(server.name);
} catch (validationError) {
setError(
prev =>
prev ??
(validationError instanceof Error
? validationError.message
: `Failed to validate ${server.name}`),
);
}
}),
);

This comment was marked as resolved.

@ciiay ciiay force-pushed the rhidp-12076-add-mcp-servers-page branch from aa21675 to 50cbb36 Compare April 1, 2026 16:44
Copy link
Copy Markdown
Member

@debsmita1 debsmita1 left a comment

Choose a reason for hiding this comment

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

Could we add a tooltip for why the toggle is disabled ?

Although you have added a comment for the edit button, it would be better to hide it if it is non-functional

Screen.Recording.2026-04-02.at.3.57.08.PM.mov

lgtm !

@maysunfaisal

This comment was marked as resolved.

jest.clearAllMocks();
});

it('tries raw token first, then falls back to Bearer on 401/403', async () => {
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.

I have added tests related to the Bearer issue on MCP Server Validation with #2671

We are always prepending Bearer, so raw token will never be used to auth with a MCP Server, you can see the comment on my PR here https://github.com/redhat-developer/rhdh-plugins/pull/2671/changes#diff-8573ea31dc3990a1eab86a5f321d47a7b95b4c7574c7092b90ca1c5203cf1178R35

@ciiay ciiay force-pushed the rhidp-12076-add-mcp-servers-page branch from 455ea69 to a213278 Compare April 3, 2026 02:30
@ciiay
Copy link
Copy Markdown
Member Author

ciiay commented Apr 3, 2026

Hi @debsmita1 , thanks for the review. Good callout, currently the reason is displayed in the Status cell, but it's kind of far from the toggle button, so I added a tooltip to display the same.
image

The Edit button will be ready in my very next PR(#2657 ) for RHIDP-12079.

ciiay added 9 commits April 3, 2026 11:24
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
ciiay added 3 commits April 3, 2026 11:40
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
Signed-off-by: Yi Cai <yicai@redhat.com>
@ciiay ciiay force-pushed the rhidp-12076-add-mcp-servers-page branch from ef30077 to 98a7a23 Compare April 3, 2026 15:56
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 3, 2026

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