Skip to content

feat(lightspeed): add DCR authentication support for MCP servers#3347

Open
maysunfaisal wants to merge 3 commits into
redhat-developer:mainfrom
maysunfaisal:RHDHPLAN-390-1
Open

feat(lightspeed): add DCR authentication support for MCP servers#3347
maysunfaisal wants to merge 3 commits into
redhat-developer:mainfrom
maysunfaisal:RHDHPLAN-390-1

Conversation

@maysunfaisal

@maysunfaisal maysunfaisal commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Hey, I just made a Pull Request!

✔️ 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)
    • See screenshots below

Issue

https://redhat.atlassian.net/browse/RHIDP-11657

Summary

Adds Dynamic Client Registration (DCR) authentication for MCP servers in Lightspeed, allowing the backend to mint per-user plugin request tokens instead of requiring static tokens.

What changed

Backend (lightspeed-backend)

  • New auth: dcr config option for MCP servers — when set, the backend mints a Backstage service token on behalf of the user via getPluginRequestToken
  • Added AuthService injection into the router
  • GET /mcp-servers and PATCH /mcp-servers/:name responses now include the auth field
  • DCR servers report hasToken: true always (tokens are minted per-request, no manual entry needed)
  • buildMcpHeaders sends the minted token in MCP-HEADERS for DCR servers

Frontend (lightspeed)

  • MCP Settings modal shows a read-only message for DCR servers explaining tokens are automatic
  • New translation strings for DCR status/description

Bug fixes along the way

  • Fixed kebab menu not auto-closing after clicking "MCP Settings" — caused by duplicate @patternfly/react-core versions (added resolution to force 6.4.3)
  • Removed stale attachButtonPosition prop that no longer exists in @patternfly/chatbot
  • Fixed RBAC backend v7+ requiring @backstage/plugin-permission-backend registered separately (breaking change from v5/v6)

Other

  • Added @backstage/plugin-auth to frontend for DCR OAuth2 consent page
  • Added dist-dynamic to .prettierignore
  • Reference rbac-policy.csv with Lightspeed permissions for local RBAC testing
  • Updated API reports

How to test

  1. Unit testscd workspaces/lightspeed && yarn test:all (769 tests pass)
  2. Local DCR testing — Requires:
    • @backstage/plugin-mcp-actions-backend added to index.ts
    • experimentalDynamicClientRegistration.enabled: true in app-config.yaml
    • mcpServers: [{ name: "...", auth: dcr }] in lightspeed config
    • An MCP client (Cursor/VS Code) connecting to http://localhost:7007/api/mcp-actions/v1 — DCR OAuth consent should pop up in browser
  3. OpenShift — Deploy custom OCI images with DCR changes; set auth: dcr in values.yaml under lightspeed.mcpServers

Config example

lightspeed:
  mcpServers:
    - name: mcp-integration-tools
      auth: dcr          # tokens minted automatically
    - name: legacy-server
      token: ${TOKEN}    # static token (existing behavior)

@rhdh-gh-app

rhdh-gh-app Bot commented Jun 9, 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
app workspaces/lightspeed/packages/app none v0.0.27
backend workspaces/lightspeed/packages/backend none v0.0.59
@red-hat-developer-hub/backstage-plugin-lightspeed-backend workspaces/lightspeed/plugins/lightspeed-backend minor v2.9.1
@red-hat-developer-hub/backstage-plugin-lightspeed workspaces/lightspeed/plugins/lightspeed minor v2.9.1

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

hey @maysunfaisal - so claude and I looked at two error scenarios:

  1. If the user's RBAC policy denies a specific tool invocation

for this, claude's analysis was that the rejection happens inside @backstage/plugin-mcp-actions-backend, not in this code. Lightspeed streams back whatever LCS returns — so the user would see whatever error or refusal the LCS/MCP Actions layer produces in the chat stream. The Lightspeed backend is fully opaque to per-tool authorization outcomes.

Assuming this is correct, I'm curious if it makes sense to catch the LCS/MCP actions layer error and display a potentially more user friendly indication of permission issues.

WDYT?

  1. Unexpected error minting a Backstage token (getPluginRequestToken throws)

Claude claims the entire chat query fails (I'll post the details in a moment). It then goes on that is a chat query results in querying multiple MCP servers, none of those servers gets called.

I suppose a user prompt could result in multiple queries, but even if so, it seems unpredictable whether calling the other mcp servers is productive i.e. how dependent would the output of the backstage mcp server be.

I'm inclined to leave this be, but thought I would surface it in the review as due diligence to get your and everyone else's take.

Here is the code analysis from claude on gitPluginRequestToken

  There are two call sites for getPluginRequestToken, and neither has a local try/catch around it:

  A) During /v1/query (chat flow) — buildMcpHeaders() calls getPluginRequestToken at ~line 133. If it throws, the exception propagates unhandled out of buildMcpHeaders and is caught by the outer catch in the /v1/query handler (~line 775):

  } catch (error) {
      const errormsg = `Error fetching completions from ${provider}: ${error}`;
      logger.error(errormsg);
      response.status(500).json({ error: errormsg });
  }

  The entire chat query fails with a 500. Even if only one of several MCP servers uses auth: dcr, a token-minting failure for that one server aborts the whole request — the error is not isolated per-server. The other MCP servers (static-token or other DCR servers) never get their headers built.

  B) During POST /mcp-servers/:name/validate (~line 393) — same pattern: no local catch, the outer handler returns 500 with "Validation failed".

  Note: there is a guard for when authService/credentials are absent (logs a warning, skips the server), but that only handles the case where the services weren't injected — not a runtime failure in getPluginRequestToken itself.

@maysunfaisal maysunfaisal force-pushed the RHDHPLAN-390-1 branch 4 times, most recently from 763129e to 077a7a0 Compare June 10, 2026 21:22
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 41.50943% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.57%. Comparing base (607817c) to head (2e91112).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3347      +/-   ##
==========================================
- Coverage   53.57%   53.57%   -0.01%     
==========================================
  Files        2250     2250              
  Lines       85744    85766      +22     
  Branches    24147    24156       +9     
==========================================
+ Hits        45938    45949      +11     
- Misses      39560    39571      +11     
  Partials      246      246              
Flag Coverage Δ *Carryforward flag
adoption-insights 83.70% <ø> (ø) Carriedforward from 607817c
ai-integrations 67.95% <ø> (ø) Carriedforward from 607817c
app-defaults 69.79% <ø> (ø) Carriedforward from 607817c
augment 46.39% <ø> (ø) Carriedforward from 607817c
bulk-import 72.46% <ø> (ø) Carriedforward from 607817c
cost-management 14.10% <ø> (ø) Carriedforward from 607817c
dcm 61.79% <ø> (ø) Carriedforward from 607817c
extensions 61.53% <ø> (ø) Carriedforward from 607817c
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 607817c
global-header 59.71% <ø> (ø) Carriedforward from 607817c
homepage 49.92% <ø> (ø) Carriedforward from 607817c
install-dynamic-plugins 56.23% <ø> (ø) Carriedforward from 607817c
konflux 91.49% <ø> (ø) Carriedforward from 607817c
lightspeed 68.50% <41.50%> (-0.08%) ⬇️
mcp-integrations 85.46% <ø> (ø) Carriedforward from 607817c
orchestrator 37.70% <ø> (ø) Carriedforward from 607817c
quickstart 63.76% <ø> (ø) Carriedforward from 607817c
sandbox 79.56% <ø> (ø) Carriedforward from 607817c
scorecard 83.83% <ø> (ø) Carriedforward from 607817c
theme 61.26% <ø> (ø) Carriedforward from 607817c
translations 6.55% <ø> (ø) Carriedforward from 607817c
x2a 78.68% <ø> (ø) Carriedforward from 607817c

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 607817c...2e91112. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts Outdated
Comment thread workspaces/lightspeed/plugins/lightspeed-backend/src/service/mcp-server-types.ts Outdated
Comment thread workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts Outdated
Comment thread workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts Outdated
Comment thread workspaces/lightspeed/plugins/lightspeed-backend/src/service/router.ts Outdated
@github-actions

This comment was marked as off-topic.

@maysunfaisal maysunfaisal force-pushed the RHDHPLAN-390-1 branch 2 times, most recently from 3b2db5a to 5f5b935 Compare June 15, 2026 22:19
@maysunfaisal

maysunfaisal commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

hey @maysunfaisal - so claude and I looked at two error scenarios:

  1. If the user's RBAC policy denies a specific tool invocation

for this, claude's analysis was that the rejection happens inside @backstage/plugin-mcp-actions-backend, not in this code. Lightspeed streams back whatever LCS returns — so the user would see whatever error or refusal the LCS/MCP Actions layer produces in the chat stream. The Lightspeed backend is fully opaque to per-tool authorization outcomes.

Assuming this is correct, I'm curious if it makes sense to catch the LCS/MCP actions layer error and display a potentially more user friendly indication of permission issues.

WDYT?

  1. Unexpected error minting a Backstage token (getPluginRequestToken throws)

Claude claims the entire chat query fails (I'll post the details in a moment). It then goes on that is a chat query results in querying multiple MCP servers, none of those servers gets called.

I suppose a user prompt could result in multiple queries, but even if so, it seems unpredictable whether calling the other mcp servers is productive i.e. how dependent would the output of the backstage mcp server be.

I'm inclined to leave this be, but thought I would surface it in the review as due diligence to get your and everyone else's take.

Here is the code analysis from claude on gitPluginRequestToken

  There are two call sites for getPluginRequestToken, and neither has a local try/catch around it:

  A) During /v1/query (chat flow) — buildMcpHeaders() calls getPluginRequestToken at ~line 133. If it throws, the exception propagates unhandled out of buildMcpHeaders and is caught by the outer catch in the /v1/query handler (~line 775):

  } catch (error) {
      const errormsg = `Error fetching completions from ${provider}: ${error}`;
      logger.error(errormsg);
      response.status(500).json({ error: errormsg });
  }

  The entire chat query fails with a 500. Even if only one of several MCP servers uses auth: dcr, a token-minting failure for that one server aborts the whole request — the error is not isolated per-server. The other MCP servers (static-token or other DCR servers) never get their headers built.

  B) During POST /mcp-servers/:name/validate (~line 393) — same pattern: no local catch, the outer handler returns 500 with "Validation failed".

  Note: there is a guard for when authService/credentials are absent (logs a warning, skips the server), but that only handles the case where the services weren't injected — not a runtime failure in getPluginRequestToken itself.

@gabemontero Thanks for flagging this. You're correct, RBAC tool-level denials happen inside @backstage/plugin-mcp-actions-backend, and Lightspeed is opaque to those outcomes; it streams back whatever LCS returns. Surfacing a user-friendly permission error would require parsing the LCS response stream for authorization-specific error patterns and rendering them differently in the chat UI and that touches the frontend streaming logic and response parsing, which is a bigger scope than this PR. I'll capture it as a follow-up enhancement (parsing MCP/RBAC errors from LCS and rendering actionable messages in the Lightspeed chat). For now, the user will see whatever LCS surfaces in the streamed response.

Addressed your second concern

@gabemontero

Copy link
Copy Markdown
Contributor

@gabemontero Thanks for flagging this. You're correct, RBAC tool-level denials happen inside @backstage/plugin-mcp-actions-backend, and Lightspeed is opaque to those outcomes; it streams back whatever LCS returns. Surfacing a user-friendly permission error would require parsing the LCS response stream for authorization-specific error patterns and rendering them differently in the chat UI and that touches the frontend streaming logic and response parsing, which is a bigger scope than this PR. I'll capture it as a follow-up enhancement (parsing MCP/RBAC errors from LCS and rendering actionable messages in the Lightspeed chat). For now, the user will see whatever LCS surfaces in the streamed response.

Addressed your second concern

sounds good @maysunfaisal thanks

@maysunfaisal

Copy link
Copy Markdown
Contributor Author

@gabemontero Here is the issue to address surfacing user friendly MCP permission denial msgs https://redhat.atlassian.net/browse/RHIDP-14907

@maysunfaisal

maysunfaisal commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Screenshots - Local

RBAC Example - catalog.example.read = deny

rbac policy file

Screenshot 2026-06-08 at 5 08 32 PM

Lightspeed (prior to rebrand) interacting with Backstage/RHDH MCP with DCR - does not return Catalog resources

Screenshot 2026-06-08 at 5 06 47 PM Screenshot 2026-06-08 at 5 08 21 PM

MCP Settings for DCR auth server does not let you edit token from the UI

Screenshot 2026-06-08 at 5 06 33 PM

Cursor MCP Settings - backstage-actions pointing to Backstage/RHDH MCP with DCR

Screenshot 2026-06-08 at 5 11 01 PM

Redirected to Backstage/RHDH for DCR auth and successful auth

Screenshot 2026-06-08 at 5 10 48 PM

Cursor MCP backstage-actions now shows a successful connection

Screenshot 2026-06-08 at 5 11 17 PM Screenshot 2026-06-08 at 5 11 11 PM

Query the Backstage/RHDH Catalog, denied due to rbac policy

Screenshot 2026-06-08 at 5 12 19 PM

RBAC Example - catalog.example.read = allow

rbac policy file

Screenshot 2026-06-08 at 5 13 38 PM

Lightspeed query now passes for the same prompt using MCP with DCR

Screenshot 2026-06-08 at 5 13 44 PM

Cursor query now passes for the same prompt using MCP with DCR

Screenshot 2026-06-08 at 5 14 27 PM

@maysunfaisal

maysunfaisal commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Screenshots - OpenShift

RBAC Example - catalog.example.read = allow

Lightspeed (prior to rebrand) MCP Settings does not let you edit for MCP Server with DCR

Screenshot 2026-06-09 at 3 26 56 PM

Lightspeed successfully returns Catalog resources for MCP with DCR

Screenshot 2026-06-09 at 3 30 37 PM

Cursor DCR auth with MCP from OpenShift

Screenshot 2026-06-09 at 3 28 25 PM Screenshot 2026-06-09 at 3 28 08 PM

Cursor successfully returns catalog resources for Backstage/RHDH MCP with DCR

Screenshot 2026-06-09 at 3 29 28 PM

RBAC Example - catalog.example.read = deny

Updated rbac policy config map on OpenShift for deny

Screenshot 2026-06-09 at 3 45 55 PM

Lightspeed does not return catalog entities for the same prompt

Screenshot 2026-06-09 at 3 45 41 PM

Cursor does not return catalog entities for the same prompt

Screenshot 2026-06-09 at 3 45 36 PM

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

generally looks good to me. my previous comments have been resolved.
please remove the duplicate migrate call.

others are some minor comments., and I'm open to merge this PR without those changes.

Comment thread workspaces/lightspeed/plugins/lightspeed-backend/src/plugin.ts Outdated
Comment thread workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts Outdated
maysunfaisal and others added 2 commits June 18, 2026 14:41
Co-authored-by: Cursor <cursoragent@cursor.com>
- Wrap getPluginRequestToken in per-server try/catch so one failing DCR
  server does not break all MCP integration
- Return 502 with clear error on token mint failure in /validate endpoint
- Bundle authService+credentials into dcrAuth object to prevent partial
  provision at compile time
- Export McpServerAuth type and use it instead of raw string
- Remove redundant per-request warning for dual auth+token config

Co-authored-by: Cursor <cursoragent@cursor.com>
@maysunfaisal maysunfaisal force-pushed the RHDHPLAN-390-1 branch 2 times, most recently from fdafa30 to fed77e1 Compare June 18, 2026 20:00
- Remove duplicate migrate(database) call in plugin.ts
- Remove unused 'mcp.settings.status.autoManaged' translation key
- Regenerate API report

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

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.

generally lgtm

@maysunfaisal

Copy link
Copy Markdown
Contributor Author

@redhat-developer/rhdh-ui @redhat-developer/rhdh-plugins-maintainers Could you PTAL at this PR? Thanks!

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.

3 participants