Skip to content

Commit 484583b

Browse files
authored
feat(x2a): add x2a-list-modules MCP tool (#2838)
* feat(x2a): add x2a-list-modules MCP tool Signed-off-by: Marek Libra <marek.libra@gmail.com> * Add MCP tool output schema documentation * Sanitize MCP tools output * fix ISO time format --------- Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent 6aea10f commit 484583b

31 files changed

Lines changed: 1176 additions & 351 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-x2a-backend': patch
3+
'@red-hat-developer-hub/backstage-plugin-x2a-mcp-extras': patch
4+
'@red-hat-developer-hub/backstage-plugin-x2a-node': patch
5+
---
6+
7+
Added x2a-list-modules MCP tool listing modules by projectId.

workspaces/x2a/app-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ auth:
121121
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
122122
signIn:
123123
resolvers:
124+
- resolver: emailMatchingUserEntityProfileEmail
124125
- resolver: usernameMatchingUserEntityName
125126
bitbucket:
126127
# Follow https://backstage.io/docs/auth/bitbucket/provider/

workspaces/x2a/plugins/x2a-backend/src/router/common.test.ts

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ import {
2929
isUserOfAdminViewPermission,
3030
isUserOfAdminWritePermission,
3131
isUserOfX2AUserPermission,
32-
removeSensitiveFromJob,
33-
UnsecureJob,
3432
useEnforceProjectPermissions,
3533
useEnforceX2APermissions,
3634
} from './common';
@@ -78,64 +76,6 @@ describe('common', () => {
7876
credentials: jest.fn().mockResolvedValue(mockCredentials.user(userRef)),
7977
});
8078

81-
describe('removeSensitiveFromJob', () => {
82-
it('returns undefined when job is undefined', () => {
83-
expect(removeSensitiveFromJob(undefined)).toBeUndefined();
84-
});
85-
86-
it('returns undefined when job is null', () => {
87-
expect(removeSensitiveFromJob(null as any)).toBeUndefined();
88-
});
89-
90-
it('removes callbackToken from job', () => {
91-
const job: UnsecureJob = {
92-
id: 'job-1',
93-
projectId: 'proj-1',
94-
moduleId: 'module-1',
95-
phase: 'init' as const,
96-
status: 'pending' as const,
97-
callbackToken: 'secret-token',
98-
startedAt: new Date(),
99-
k8sJobName: 'job-1',
100-
};
101-
const result = removeSensitiveFromJob(job);
102-
expect(result).toBeDefined();
103-
expect(result).not.toHaveProperty('callbackToken');
104-
expect(result).toMatchObject({
105-
id: 'job-1',
106-
projectId: 'proj-1',
107-
phase: 'init',
108-
status: 'pending',
109-
});
110-
});
111-
112-
it('returns job unchanged when it has no callbackToken', () => {
113-
const job = {
114-
id: 'job-1',
115-
projectId: 'proj-1',
116-
moduleId: 'module-1',
117-
phase: 'analyze' as const,
118-
status: 'success' as const,
119-
startedAt: new Date(),
120-
k8sJobName: 'job-1',
121-
};
122-
const result = removeSensitiveFromJob(job);
123-
expect(result).toEqual(job);
124-
});
125-
126-
it('does not mutate the original job object', () => {
127-
const job = {
128-
id: 'job-1',
129-
callbackToken: 'secret',
130-
startedAt: new Date(),
131-
k8sJobName: 'job-1',
132-
} as UnsecureJob;
133-
const result = removeSensitiveFromJob(job);
134-
expect(job).toHaveProperty('callbackToken', 'secret');
135-
expect(result).not.toHaveProperty('callbackToken');
136-
});
137-
});
138-
13979
describe('getGroupsOfUser', () => {
14080
it('returns empty array when user entity is null', async () => {
14181
const catalog = getCatalogMock();

workspaces/x2a/plugins/x2a-backend/src/router/common.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
BasicPermission,
2828
} from '@backstage/plugin-permission-common';
2929
import {
30-
Job,
3130
Project,
3231
x2aAdminViewPermission,
3332
x2aAdminWritePermission,
@@ -47,8 +46,11 @@ export {
4746
getGroupsOfUser,
4847
reconcileJobStatus,
4948
generateCallbackToken,
49+
removeSensitiveFromJob,
5050
} from '@red-hat-developer-hub/backstage-plugin-x2a-node';
5151

52+
export type { UnsecureJob } from '@red-hat-developer-hub/backstage-plugin-x2a-node';
53+
5254
/**
5355
* Checks if the user has the x2aAdminViewPermission.
5456
*/
@@ -232,14 +234,3 @@ export const useEnforceProjectPermissions = async (
232234
canWriteAll,
233235
};
234236
};
235-
236-
export type UnsecureJob = Job & { callbackToken?: string };
237-
export const removeSensitiveFromJob = (job?: UnsecureJob): Job | undefined => {
238-
if (!job) {
239-
return undefined;
240-
}
241-
242-
const newJob: UnsecureJob = { ...job };
243-
delete newJob.callbackToken;
244-
return newJob;
245-
};

workspaces/x2a/plugins/x2a-backend/src/router/modules.ts

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,17 @@ import { z } from 'zod';
1818
import express from 'express';
1919
import { InputError, NotFoundError } from '@backstage/errors';
2020

21-
import type { Module } from '@red-hat-developer-hub/backstage-plugin-x2a-common';
22-
2321
import type { RouterDeps } from './types';
2422
import {
2523
generateCallbackToken,
2624
reconcileJobStatus,
2725
useEnforceProjectPermissions,
2826
} from './common';
29-
import { calculateModuleStatus } from '../services/X2ADatabaseService/status';
30-
31-
/**
32-
* Reconcile any pending/running phase jobs on a module against K8s state.
33-
* Mutates the module in-place and returns it.
34-
*/
35-
async function reconcileModuleJobs(
36-
module: Module,
37-
deps: Pick<RouterDeps, 'kubeService' | 'x2aDatabase' | 'logger'>,
38-
): Promise<Module> {
39-
const phases = ['analyze', 'migrate', 'publish'] as const;
40-
for (const phase of phases) {
41-
const job = module[phase];
42-
if (job && ['pending', 'running'].includes(job.status)) {
43-
module[phase] = await reconcileJobStatus(job, deps);
44-
}
45-
}
46-
return module;
47-
}
27+
import {
28+
calculateModuleStatus,
29+
listModulesWithReconciledStatuses,
30+
reconcileModuleJobs,
31+
} from '@red-hat-developer-hub/backstage-plugin-x2a-node';
4832

4933
export function registerModuleRoutes(
5034
router: express.Router,
@@ -76,26 +60,12 @@ export function registerModuleRoutes(
7660
catalog,
7761
});
7862

79-
// List modules
8063
const modules = await x2aDatabase.listModules({ projectId });
81-
82-
// Reconcile any pending/running jobs against K8s
83-
await Promise.all(
84-
modules.map(m =>
85-
reconcileModuleJobs(m, { kubeService, x2aDatabase, logger }),
86-
),
87-
);
88-
89-
// Recalculate status for each module after reconciliation
90-
for (const m of modules) {
91-
const { status, errorDetails } = calculateModuleStatus({
92-
analyze: m.analyze,
93-
migrate: m.migrate,
94-
publish: m.publish,
95-
});
96-
m.status = status;
97-
m.errorDetails = errorDetails;
98-
}
64+
await listModulesWithReconciledStatuses(modules, {
65+
kubeService,
66+
x2aDatabase,
67+
logger,
68+
});
9969

10070
res.json(modules);
10171
});

workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import {
3939
} from '@red-hat-developer-hub/backstage-plugin-x2a-common';
4040
import {
4141
x2aDatabaseServiceRef,
42+
removeSensitiveFromJob,
43+
calculateModuleStatus,
4244
type X2ADatabaseServiceApi,
4345
type CreateJobInput,
4446
} from '@red-hat-developer-hub/backstage-plugin-x2a-node';
@@ -47,11 +49,10 @@ import { JobOperations } from './jobOperations';
4749
import { ModuleOperations } from './moduleOperations';
4850
import { ProjectOperations } from './projectOperations';
4951
import { isNonDbSortField } from './queryHelpers';
50-
import { removeSensitiveFromJob } from '../../router/common';
5152
import { MAX_CONCURRENT_ENRICHMENT_JOBS } from '../constants';
5253
import { migrate } from '../dbMigrate';
5354
import { maxConcurrency } from '../../utils';
54-
import { calculateModuleStatus, calculateProjectStatus } from './status';
55+
import { calculateProjectStatus } from './projectStatus';
5556

5657
export class X2ADatabaseService implements X2ADatabaseServiceApi {
5758
readonly #logger: LoggerService;

0 commit comments

Comments
 (0)