Skip to content

Commit 024d5a8

Browse files
authored
feat(lightspeed): [RHIDP-11659] Implementation of MCP Service Selection (#2581)
* feat(lightspeed): First draft implementation of MCP Service Selection Assisted-by: Claude Opus 4.6 Generated-by: Cursor Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com> * feat(lightspeed): Address PR reviews, add changeset Assisted-by: Claude Opus 4.6 Generated-by: Cursor Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com> * feat(lightspeed): Add debug log for MCP notification Assisted-by: Claude Opus 4.6 Generated-by: Cursor Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com> * feat(lightspeed): Fix API reports, SonarQube warnings Assisted-by: Claude Opus 4.6 Generated-by: Cursor Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com> Made-with: Cursor --------- Signed-off-by: Maysun J Faisal <maysunaneek@gmail.com>
1 parent 2bf13d3 commit 024d5a8

File tree

21 files changed

+1548
-67
lines changed

21 files changed

+1548
-67
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-lightspeed-backend': minor
3+
'@red-hat-developer-hub/backstage-plugin-lightspeed-common': minor
4+
---
5+
6+
Added MCP Server management backend APIs with per-user preferences, on-demand validation, and new permissions (lightspeed.mcp.read, lightspeed.mcp.manage)

workspaces/lightspeed/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,7 @@ site
5252

5353
# E2E test reports
5454
e2e-test-report/
55+
56+
# Local SQLite database files
57+
sqlite-data/
58+
*.sqlite

workspaces/lightspeed/app-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ backend:
3535
database:
3636
client: better-sqlite3
3737
connection: ':memory:'
38+
# To persist the database to a file for local development, replace the above with:
39+
# connection:
40+
# directory: './sqlite-data'
41+
# OpenShift / RHDH production — uses the PostgreSQL instance managed by the
42+
# RHDH Helm chart or Operator. These env vars are injected from the
43+
# PostgreSQL Secret into the Backstage container by the deployment config.
44+
# client: pg
45+
# connection:
46+
# host: ${POSTGRES_HOST}
47+
# port: ${POSTGRES_PORT}
48+
# user: ${POSTGRES_USER}
49+
# password: ${POSTGRES_PASSWORD}
3850
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
3951

4052
integrations:

workspaces/lightspeed/plugins/lightspeed-backend/__fixtures__/lcsHandlers.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,33 @@ export const lcsHandlers: HttpHandler[] = [
237237
return HttpResponse.json(mockModelRes);
238238
}),
239239

240+
// LCS MCP server list — returns registered servers so the backend can
241+
// resolve URLs for validation without requiring url in app-config.
242+
http.get(`${LOCAL_LCS_ADDR}/v1/mcp-servers`, () => {
243+
return HttpResponse.json({
244+
servers: [
245+
{
246+
name: 'static-mcp',
247+
url: 'https://mock-mcp-server:9999',
248+
provider_id: 'model-context-protocol',
249+
source: 'config',
250+
},
251+
{
252+
name: 'no-token-server',
253+
url: 'https://mock-mcp-server:9999',
254+
provider_id: 'model-context-protocol',
255+
source: 'config',
256+
},
257+
{
258+
name: 'lcs-only-server',
259+
url: 'https://mock-mcp-server:9999',
260+
provider_id: 'model-context-protocol',
261+
source: 'api',
262+
},
263+
],
264+
});
265+
}),
266+
240267
// Catch-all handler for unknown paths
241268
http.all(`${LOCAL_LCS_ADDR}/*`, ({ request }) => {
242269
console.log(`Caught request to unknown path: ${request.url}`);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { http, HttpResponse, type HttpHandler } from 'msw';
18+
19+
export const MOCK_MCP_ADDR = 'https://mock-mcp-server:9999';
20+
export const MOCK_MCP_VALID_TOKEN = 'valid-mcp-token';
21+
22+
const MOCK_TOOLS = [
23+
{ name: 'create_issue', description: 'Create a GitHub issue' },
24+
{ name: 'list_repos', description: 'List repositories' },
25+
{ name: 'get_user', description: 'Get user profile' },
26+
];
27+
28+
export const mcpHandlers: HttpHandler[] = [
29+
http.post(MOCK_MCP_ADDR, async ({ request }) => {
30+
const auth = request.headers.get('Authorization');
31+
if (auth !== `Bearer ${MOCK_MCP_VALID_TOKEN}`) {
32+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
33+
}
34+
35+
const body = (await request.json()) as { method: string; id?: number };
36+
37+
if (body.method === 'initialize') {
38+
return HttpResponse.json(
39+
{
40+
jsonrpc: '2.0',
41+
result: {
42+
protocolVersion: '2024-11-05',
43+
capabilities: { tools: {} },
44+
serverInfo: { name: 'mock-mcp-server', version: '1.0.0' },
45+
},
46+
id: body.id,
47+
},
48+
{ headers: { 'Mcp-Session-Id': 'mock-session-123' } },
49+
);
50+
}
51+
52+
if (body.method === 'notifications/initialized') {
53+
return new HttpResponse(null, { status: 204 });
54+
}
55+
56+
if (body.method === 'tools/list') {
57+
return HttpResponse.json({
58+
jsonrpc: '2.0',
59+
result: { tools: MOCK_TOOLS },
60+
id: body.id,
61+
});
62+
}
63+
64+
return HttpResponse.json(
65+
{
66+
jsonrpc: '2.0',
67+
error: { code: -32601, message: 'Method not found' },
68+
id: body.id,
69+
},
70+
{ status: 200 },
71+
);
72+
}),
73+
];

workspaces/lightspeed/plugins/lightspeed-backend/config.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,18 @@ export interface Config {
3636
*/
3737
mcpServers?: Array<{
3838
/**
39-
* The name of the mcp server.
39+
* The name of the MCP server. Must match the name registered in LCS config.
40+
* The URL is fetched from LCS (GET /v1/mcp-servers) at startup.
4041
* @visibility backend
4142
*/
4243
name: string;
4344
/**
44-
* The access token for authenticating MCP server.
45+
* The default access token for authenticating with this MCP server.
46+
* Optional — if omitted, users must provide their own token via the UI.
47+
* Users can also override this with a personal token via PATCH /mcp-servers/:name.
4548
* @visibility secret
4649
*/
47-
token: string;
50+
token?: string;
4851
}>;
4952
/**
5053
* Configuration for AI Notebooks (Developer Preview)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* @param {import('knex').Knex} knex
19+
*/
20+
exports.up = async function up(knex) {
21+
await knex.schema.createTable('lightspeed_mcp_user_settings', table => {
22+
table.string('id').primary().notNullable();
23+
table.string('server_name').notNullable();
24+
table.string('user_entity_ref').notNullable();
25+
table.boolean('enabled').notNullable().defaultTo(true);
26+
table.text('token'); // nullable — user override for admin default
27+
table.string('status').notNullable().defaultTo('unknown');
28+
table.integer('tool_count').notNullable().defaultTo(0);
29+
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
30+
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
31+
32+
table.unique(['server_name', 'user_entity_ref']);
33+
});
34+
};
35+
36+
/**
37+
* @param {import('knex').Knex} knex
38+
*/
39+
exports.down = async function down(knex) {
40+
await knex.schema.dropTableIfExists('lightspeed_mcp_user_settings');
41+
};

workspaces/lightspeed/plugins/lightspeed-backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"htmlparser2": "^9.1.0",
5656
"http-proxy-middleware": "^3.0.2",
5757
"js-yaml": "^4.1.1",
58+
"knex": "^3.1.0",
5859
"llama-stack-client": "^0.5.0",
5960
"multer": "^1.4.5-lts.1",
6061
"pdfjs-dist": "^4.10.38"
@@ -75,7 +76,8 @@
7576
"files": [
7677
"dist",
7778
"config.d.ts",
78-
"app-config.yaml"
79+
"app-config.yaml",
80+
"migrations"
7981
],
8082
"configSchema": "config.d.ts",
8183
"repository": {

workspaces/lightspeed/plugins/lightspeed-backend/report.api.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { BackendFeature } from '@backstage/backend-plugin-api';
88
import type { Config } from '@backstage/config';
9+
import type { DatabaseService } from '@backstage/backend-plugin-api';
910
import express from 'express';
1011
import type { HttpAuthService } from '@backstage/backend-plugin-api';
1112
import type { LoggerService } from '@backstage/backend-plugin-api';
@@ -19,10 +20,52 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
1920
const lightspeedPlugin: BackendFeature;
2021
export default lightspeedPlugin;
2122

23+
// @public
24+
export interface McpServerResponse {
25+
// (undocumented)
26+
enabled: boolean;
27+
// (undocumented)
28+
hasToken: boolean;
29+
// (undocumented)
30+
hasUserToken: boolean;
31+
// (undocumented)
32+
name: string;
33+
// (undocumented)
34+
status: McpServerStatus;
35+
// (undocumented)
36+
toolCount: number;
37+
// (undocumented)
38+
url?: string;
39+
}
40+
41+
// @public (undocumented)
42+
export type McpServerStatus = 'connected' | 'error' | 'unknown';
43+
44+
// @public (undocumented)
45+
export interface McpToolInfo {
46+
// (undocumented)
47+
description: string;
48+
// (undocumented)
49+
name: string;
50+
}
51+
52+
// @public (undocumented)
53+
export interface McpValidationResult {
54+
// (undocumented)
55+
error?: string;
56+
// (undocumented)
57+
toolCount: number;
58+
// (undocumented)
59+
tools: McpToolInfo[];
60+
// (undocumented)
61+
valid: boolean;
62+
}
63+
2264
// @public
2365
export type RouterOptions = {
2466
logger: LoggerService;
2567
config: Config;
68+
database: DatabaseService;
2669
httpAuth: HttpAuthService;
2770
userInfo: UserInfoService;
2871
permissions: PermissionsService;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
DatabaseService,
19+
resolvePackagePath,
20+
} from '@backstage/backend-plugin-api';
21+
22+
const migrationsDir = resolvePackagePath(
23+
'@red-hat-developer-hub/backstage-plugin-lightspeed-backend',
24+
'migrations',
25+
);
26+
27+
export async function migrate(databaseManager: DatabaseService) {
28+
const knex = await databaseManager.getClient();
29+
30+
if (!databaseManager.migrations?.skip) {
31+
await knex.migrate.latest({
32+
directory: migrationsDir,
33+
});
34+
}
35+
}

0 commit comments

Comments
 (0)