Skip to content

fix(lightspeed): Update Lightspeed router for handling multiple MCP Servers#2408

Merged
maysunfaisal merged 3 commits into
redhat-developer:mainfrom
maysunfaisal:RHIDP-10127-1
Mar 3, 2026
Merged

fix(lightspeed): Update Lightspeed router for handling multiple MCP Servers#2408
maysunfaisal merged 3 commits into
redhat-developer:mainfrom
maysunfaisal:RHIDP-10127-1

Conversation

@maysunfaisal

@maysunfaisal maysunfaisal commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

User description

Hey, I just made a Pull Request!

  • Updated the Developer Lightspeed Plugin's Router for handling multiple MCP Servers, rather than only 1
  • Added test coverage
  • Add option for dangerouslyAllowSignInWithoutUserInCatalog in app-config for local dev without a User entity, so local dev can use Github Auth with a Github OAuth application

Addresses https://issues.redhat.com/browse/RHIDP-10127

✔️ 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)

How to test changes?

Llama Stack 0.3.5

No changes, just run podman run -it -p 8321:8321 --env-file ./env/values.env -v ./embeddings_model:/rag-content/embeddings_model:Z -v ./vector_db/rhdh_product_docs:/rag-content/vector_db/rhdh_product_docs:Z quay.io/redhat-ai-dev/llama-stack:0.1.4 from https://github.com/redhat-ai-dev/llama-stack

Lightspeed Stack (LCORE 0.4.0)

In your lightspeed-stack.yaml, add the MCP Server config:

mcp_servers:
  - name: mcp-integration-tools
    provider_id: "model-context-protocol"
    url: "http://host.docker.internal:7008/api/mcp-actions/v1"
    authorization_headers:
      Authorization: "client"
  - name: test-mcp-server
    provider_id: "model-context-protocol"
    url: "http://host.docker.internal:8888/mcp"
    authorization_headers:
      Authorization: "client"
  1. For the mcp-integration-tools server, I basically port forwarded the RHDH AI Backstage MCP Server from OpenShift with oc port-forward -n rolling-demo-ns svc/rolling-demo-backstage 7008:7007 (dont use 7007 on localhost as Backstage backend uses it)
  2. For the test-mcp-server, use this simple test MCP server at https://github.com/maysunfaisal/test-mcp-server

Run podman run -it -p 8080:8080 -v ./lightspeed-stack.yaml:/app-root/lightspeed-stack.yaml:Z quay.io/lightspeed-core/lightspeed-stack:0.4.0 from https://github.com/redhat-ai-dev/llama-stack

Backstage

In the lightspeed app-config.yaml, add MCP config for lightspeed:

# Lightspeed configuration
lightspeed:
  mcpServers:
    - name: mcp-integration-tools
      token: ${MCP_TOKEN_1}
    - name: test-mcp-server
      token: ${MCP_TOKEN_2}

PR Type

Enhancement, Tests


Description

  • Support multiple MCP servers with individual authentication headers

  • Build MCP headers map from all configured servers during router initialization

  • Add comprehensive test coverage for single, multiple, and no MCP servers

  • Document optional GitHub auth configuration for local development


Diagram Walkthrough

flowchart LR
  A["MCP Servers Config"] -->|"Parse all servers"| B["Build Headers Map"]
  B -->|"Store in memory"| C["Router Initialization"]
  C -->|"Include in request"| D["Lightspeed Stack Query"]
  E["Test Cases"] -->|"Validate"| B
  E -->|"Validate"| D
Loading

File Walkthrough

Relevant files
Enhancement
router.ts
Enable multiple MCP servers with unified header handling 

workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts

  • Replaced single MCP server support with loop-based multi-server
    configuration
  • Build mcpHeaders map containing all configured servers with Bearer
    token authorization
  • Simplify MCP headers serialization logic to use pre-built map instead
    of conditional string formatting
  • Initialize headers during router creation for efficiency
+15/-11 
Tests
router.test.ts
Add comprehensive MCP server configuration tests                 

workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.test.ts

  • Add test for multiple MCP servers validating correct header format and
    structure
  • Add test for empty MCP servers configuration returning empty headers
  • Add test for single MCP server ensuring backward compatibility
  • All tests verify proper Authorization Bearer token formatting
+162/-0 
Documentation
dirty-mayflies-walk.md
Changelog entry for multiple MCP servers                                 

workspaces/lightspeed/.changeset/dirty-mayflies-walk.md

  • Document minor version bump for lightspeed backend plugin
  • Describe feature addition of multiple MCP servers support with
    authentication
+5/-0     
app-config.yaml
Document GitHub auth configuration for local dev                 

workspaces/lightspeed/app-config.yaml

  • Add commented example for dangerouslyAllowSignInWithoutUserInCatalog
    GitHub auth option
  • Enable local development without User entity in catalog
  • Clarify configuration for developers using GitHub OAuth
+2/-0     

Assisted-by: Claude Opus 4.5

Generated-by: Cursor
Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com>
@rhdh-gh-app

rhdh-gh-app Bot commented Feb 27, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

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

@maysunfaisal

Copy link
Copy Markdown
Contributor Author

Test Coverage:

 PASS  src/service/router.test.ts
  lightspeed router tests
    GET /health
      ✓ returns ok (25 ms)
    GET v1/models
      ✓ should load available models (33 ms)
    GET /v2/conversations
      ✓ load conversations list with summary (10 ms)
    PUT /v2/conversations/:conversation_id
      ✓ should successfully update topic summary (17 ms)
      ✓ should fail with unauthorized error while updating topic summary (4 ms)
      ✓ should return 500 error when conversation does not exist (7 ms)
      ✓ should handle upstream server errors properly (8 ms)
    GET and DELETE /v2/conversations/:conversation_id
      ✓ load history (6 ms)
      ✓ should fail with unauthorized error while fetching conversation history (3 ms)
      ✓ delete history (5 ms)
      ✓ should fail with unauthorized error while deleting a conversation history (4 ms)
      ✓ load history with deleted conversation_id (5 ms)
      ✓ load history from a non-exist conversation_id should error out (5 ms)
    POST /v1/feedback
      ✓ should send the feedback successfully (7 ms)
      ✓ should fail with unauthorized error in feedback API (2 ms)
    GET /v1/feedback/status
      ✓ should send the feedback successfully (4 ms)
      ✓ should fail with unauthorized error in feedback staus API (2 ms)
    POST /v1/query
      ✓ chat completions (7 ms)
      ✓ should send MCP headers for multiple MCP servers (6 ms)
      ✓ should send empty MCP headers when no MCP servers configured (4 ms)
      ✓ should send MCP headers for single MCP server (6 ms)
      ✓ should fail with unauthorized error in chat completion API (3 ms)
      ✓ returns 400 if provider is missing (2 ms)
      ✓ returns 400 if model is missing (2 ms)
      ✓ returns 400 if query is missing (2 ms)
      ✓ returns 500 if unexpected error (6 ms)

Test Suites: 1 passed, 1 total
Tests:       26 passed, 26 total
Snapshots:   0 total
Time:        2.663 s
Ran all test suites matching /router.test.ts/i.

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Feb 27, 2026

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Token exposure over HTTP

Description: The router builds Authorization: Bearer values from lightspeed.mcpServers and forwards
them in the custom MCP-HEADERS request header over plain HTTP (http://0.0.0.0:${port}),
which could expose MCP bearer tokens to interception or unintended logging/propagation if
traffic leaves the local host/network namespace or headers are logged by intermediaries.
router.ts [57-235]

Referred Code
const mcpServersConfig = config.getOptionalConfigArray(
  'lightspeed.mcpServers',
);
const mcpHeaders: Record<string, { Authorization: string }> = {};
if (mcpServersConfig) {
  for (const mcpServer of mcpServersConfig) {
    const name = mcpServer.getString('name');
    const token = mcpServer.getString('token');
    mcpHeaders[name] = { Authorization: `Bearer ${token}` };
  }
}

router.get('/health', (_, response) => {
  response.json({ status: 'ok' });
});

const permissionIntegrationRouter = createPermissionIntegrationRouter({
  permissions: lightspeedPermissions,
});
router.use(permissionIntegrationRouter);



 ... (clipped 158 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected:
- it: should send MCP headers for multiple MCP servers
- it: should send empty MCP headers when no MCP servers configured
- it: should send MCP headers for single MCP server
Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing config validation: MCP server entries are read with required name/token fields and silently overwrite on
duplicate names without explicit validation or clear error handling, which may cause
hard-to-diagnose runtime misconfiguration.

Referred Code
const mcpServersConfig = config.getOptionalConfigArray(
  'lightspeed.mcpServers',
);
const mcpHeaders: Record<string, { Authorization: string }> = {};
if (mcpServersConfig) {
  for (const mcpServer of mcpServersConfig) {
    const name = mcpServer.getString('name');
    const token = mcpServer.getString('token');
    mcpHeaders[name] = { Authorization: `Bearer ${token}` };
  }

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Token in header: The code serializes MCP bearer tokens into the MCP-HEADERS request header, and it cannot
be verified from the diff whether downstream request/HTTP logging is guaranteed to redact
this sensitive header.

Referred Code
const mcpHeadersValue =
  Object.keys(mcpHeaders).length > 0 ? JSON.stringify(mcpHeaders) : '';
const fetchResponse = await fetch(
  `http://0.0.0.0:${port}/v1/streaming_query?${userQueryParam}`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'MCP-HEADERS': mcpHeadersValue,
    },

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Secret handling review: MCP tokens from configuration are embedded into an outbound header without additional
guardrails (e.g., rejecting empty tokens, preventing duplicate server names, or ensuring
redaction), requiring confirmation that secrets are managed and protected end-to-end.

Referred Code
const mcpServersConfig = config.getOptionalConfigArray(
  'lightspeed.mcpServers',
);
const mcpHeaders: Record<string, { Authorization: string }> = {};
if (mcpServersConfig) {
  for (const mcpServer of mcpServersConfig) {
    const name = mcpServer.getString('name');
    const token = mcpServer.getString('token');
    mcpHeaders[name] = { Authorization: `Bearer ${token}` };
  }

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@maysunfaisal

Copy link
Copy Markdown
Contributor Author

Screenshots

Backstage MCP Server

Screenshot 2026-02-27 at 3 28 27 PM

Test MCP Server

Screenshot 2026-02-27 at 3 28 36 PM

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Feb 27, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Improve resilience to configuration errors

Replace getString with getOptionalString when reading MCP server configuration
to prevent crashes from missing name or token properties. Instead, log a warning
and skip the misconfigured entry.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts [57-67]

 const mcpServersConfig = config.getOptionalConfigArray(
   'lightspeed.mcpServers',
 );
 const mcpHeaders: Record<string, { Authorization: string }> = {};
 if (mcpServersConfig) {
   for (const mcpServer of mcpServersConfig) {
-    const name = mcpServer.getString('name');
-    const token = mcpServer.getString('token');
-    mcpHeaders[name] = { Authorization: `Bearer ${token}` };
+    const name = mcpServer.getOptionalString('name');
+    const token = mcpServer.getOptionalString('token');
+    if (name && token) {
+      mcpHeaders[name] = { Authorization: `Bearer ${token}` };
+    } else {
+      logger.warn(
+        'Skipping misconfigured lightspeed.mcpServers entry. Both `name` and `token` must be non-empty strings.',
+      );
+    }
   }
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies that using getString can cause the service to crash on startup due to a misconfiguration. Switching to getOptionalString and handling missing values improves the service's robustness, which is a valuable improvement.

Medium
General
Precompute MCP header value

Pre-compute the serialized mcpHeaders value once at startup instead of on every
request to improve performance.

workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts [57-225]

 const mcpServersConfig = config.getOptionalConfigArray(
   'lightspeed.mcpServers',
 );
 const mcpHeaders: Record<string, { Authorization: string }> = {};
 if (mcpServersConfig) {
   for (const mcpServer of mcpServersConfig) {
     const name = mcpServer.getString('name');
     const token = mcpServer.getString('token');
     mcpHeaders[name] = { Authorization: `Bearer ${token}` };
   }
 }
-// ...
-// inside each request handler:
 const mcpHeadersValue =
   Object.keys(mcpHeaders).length > 0 ? JSON.stringify(mcpHeaders) : '';
+// ...
+// inside each request handler, use the precomputed `mcpHeadersValue`
+headers: {
+  'Content-Type': 'application/json',
+  ...(mcpHeadersValue && { 'MCP-HEADERS': mcpHeadersValue }),
+},

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 4

__

Why: This is a valid micro-optimization that avoids re-serializing the headers object on every request by computing the value once at startup. While correct, the performance impact is likely negligible given the context.

Low
  • Update

@maysunfaisal maysunfaisal changed the title Update Lightspeed router for handling multiple MCP Servers fix(lightspeed): Update Lightspeed router for handling multiple MCP Servers Feb 27, 2026
@sonarqubecloud

sonarqubecloud Bot commented Mar 2, 2026

Copy link
Copy Markdown

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

changes lgtm

@maysunfaisal maysunfaisal merged commit 5ad82d7 into redhat-developer:main Mar 3, 2026
9 checks passed
rohitratannagar pushed a commit to rohitratannagar/rhdh-plugins that referenced this pull request Mar 12, 2026
…veloper#2408)

Assisted-by: Claude Opus 4.5

Generated-by: Cursor

Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants