Skip to content

Commit a37d3e6

Browse files
committed
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>
1 parent 4961a22 commit a37d3e6

File tree

18 files changed

+1351
-20
lines changed

18 files changed

+1351
-20
lines changed

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
@@ -30,6 +30,18 @@ backend:
3030
database:
3131
client: better-sqlite3
3232
connection: ':memory:'
33+
# Use the directory property to store the database in a file for local development
34+
# The database is used for the user preferences and MCP servers settings
35+
# directory: './sqlite-data'
36+
# OpenShift / RHDH production — uses the PostgreSQL instance managed by the
37+
# RHDH Helm chart or Operator. These env vars are injected from the
38+
# PostgreSQL Secret into the Backstage container by the deployment config.
39+
# client: pg
40+
# connection:
41+
# host: ${POSTGRES_HOST}
42+
# port: ${POSTGRES_PORT}
43+
# user: ${POSTGRES_USER}
44+
# password: ${POSTGRES_PASSWORD}
3345
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
3446

3547
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: 'http://mock-mcp-server:9999',
248+
provider_id: 'model-context-protocol',
249+
source: 'config',
250+
},
251+
{
252+
name: 'no-token-server',
253+
url: 'http://mock-mcp-server:9999',
254+
provider_id: 'model-context-protocol',
255+
source: 'config',
256+
},
257+
{
258+
name: 'lcs-only-server',
259+
url: 'http://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 = 'http://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
}
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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"@langchain/openai": "^0.6.0",
5252
"@red-hat-developer-hub/backstage-plugin-lightspeed-common": "workspace:^",
5353
"express": "^4.21.1",
54-
"http-proxy-middleware": "^3.0.2"
54+
"http-proxy-middleware": "^3.0.2",
55+
"knex": "^3.1.0"
5556
},
5657
"devDependencies": {
5758
"@backstage/backend-test-utils": "^1.10.4",
@@ -68,7 +69,8 @@
6869
"files": [
6970
"dist",
7071
"config.d.ts",
71-
"app-config.yaml"
72+
"app-config.yaml",
73+
"migrations"
7274
],
7375
"configSchema": "config.d.ts",
7476
"repository": {
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+
}

workspaces/lightspeed/plugins/lightspeed-backend/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ export { lightspeedPlugin as default } from './plugin';
1818
export * from './service/router';
1919

2020
export type { RouterOptions } from './service/types';
21+
export type {
22+
McpServerResponse,
23+
McpServerStatus,
24+
McpToolInfo,
25+
McpValidationResult,
26+
} from './service/mcp-server-types';

workspaces/lightspeed/plugins/lightspeed-backend/src/plugin.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
createBackendPlugin,
2020
} from '@backstage/backend-plugin-api';
2121

22+
import { migrate } from './database/migration';
2223
import { createRouter } from './service/router';
2324

2425
/**
@@ -36,14 +37,26 @@ export const lightspeedPlugin = createBackendPlugin({
3637
httpAuth: coreServices.httpAuth,
3738
userInfo: coreServices.userInfo,
3839
permissions: coreServices.permissions,
40+
database: coreServices.database,
3941
},
40-
async init({ logger, config, http, httpAuth, userInfo, permissions }) {
42+
async init({
43+
logger,
44+
config,
45+
http,
46+
httpAuth,
47+
userInfo,
48+
permissions,
49+
database,
50+
}) {
51+
await migrate(database);
52+
4153
http.use(
4254
await createRouter({
43-
config: config,
44-
logger: logger,
45-
httpAuth: httpAuth,
46-
userInfo: userInfo,
55+
config,
56+
logger,
57+
database,
58+
httpAuth,
59+
userInfo,
4760
permissions,
4861
}),
4962
);

0 commit comments

Comments
 (0)